##// END OF EJS Templates
Makes related issues available for display and filtering on the issue list (#3239, #3265)....
Jean-Philippe Lang -
r10303:1b6da80e16dd
parent child
Show More

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

@@ -1,1274 +1,1276
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 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 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Displays a link to user's account page if active
46 # Displays a link to user's account page if active
47 def link_to_user(user, options={})
47 def link_to_user(user, options={})
48 if user.is_a?(User)
48 if user.is_a?(User)
49 name = h(user.name(options[:format]))
49 name = h(user.name(options[:format]))
50 if user.active?
50 if user.active?
51 link_to name, :controller => 'users', :action => 'show', :id => user
51 link_to name, :controller => 'users', :action => 'show', :id => user
52 else
52 else
53 name
53 name
54 end
54 end
55 else
55 else
56 h(user.to_s)
56 h(user.to_s)
57 end
57 end
58 end
58 end
59
59
60 # Displays a link to +issue+ with its subject.
60 # Displays a link to +issue+ with its subject.
61 # Examples:
61 # Examples:
62 #
62 #
63 # link_to_issue(issue) # => Defect #6: This is the subject
63 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :subject => false) # => Defect #6
65 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
67 #
68 #
68 def link_to_issue(issue, options={})
69 def link_to_issue(issue, options={})
69 title = nil
70 title = nil
70 subject = nil
71 subject = nil
72 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
71 if options[:subject] == false
73 if options[:subject] == false
72 title = truncate(issue.subject, :length => 60)
74 title = truncate(issue.subject, :length => 60)
73 else
75 else
74 subject = issue.subject
76 subject = issue.subject
75 if options[:truncate]
77 if options[:truncate]
76 subject = truncate(subject, :length => options[:truncate])
78 subject = truncate(subject, :length => options[:truncate])
77 end
79 end
78 end
80 end
79 s = link_to "#{h(issue.tracker)} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
81 s = link_to text, {:controller => "issues", :action => "show", :id => issue},
80 :class => issue.css_classes,
82 :class => issue.css_classes,
81 :title => title
83 :title => title
82 s << h(": #{subject}") if subject
84 s << h(": #{subject}") if subject
83 s = h("#{issue.project} - ") + s if options[:project]
85 s = h("#{issue.project} - ") + s if options[:project]
84 s
86 s
85 end
87 end
86
88
87 # Generates a link to an attachment.
89 # Generates a link to an attachment.
88 # Options:
90 # Options:
89 # * :text - Link text (default to attachment filename)
91 # * :text - Link text (default to attachment filename)
90 # * :download - Force download (default: false)
92 # * :download - Force download (default: false)
91 def link_to_attachment(attachment, options={})
93 def link_to_attachment(attachment, options={})
92 text = options.delete(:text) || attachment.filename
94 text = options.delete(:text) || attachment.filename
93 action = options.delete(:download) ? 'download' : 'show'
95 action = options.delete(:download) ? 'download' : 'show'
94 opt_only_path = {}
96 opt_only_path = {}
95 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
97 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
96 options.delete(:only_path)
98 options.delete(:only_path)
97 link_to(h(text),
99 link_to(h(text),
98 {:controller => 'attachments', :action => action,
100 {:controller => 'attachments', :action => action,
99 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
101 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
100 options)
102 options)
101 end
103 end
102
104
103 # Generates a link to a SCM revision
105 # Generates a link to a SCM revision
104 # Options:
106 # Options:
105 # * :text - Link text (default to the formatted revision)
107 # * :text - Link text (default to the formatted revision)
106 def link_to_revision(revision, repository, options={})
108 def link_to_revision(revision, repository, options={})
107 if repository.is_a?(Project)
109 if repository.is_a?(Project)
108 repository = repository.repository
110 repository = repository.repository
109 end
111 end
110 text = options.delete(:text) || format_revision(revision)
112 text = options.delete(:text) || format_revision(revision)
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
113 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 link_to(
114 link_to(
113 h(text),
115 h(text),
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
116 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 :title => l(:label_revision_id, format_revision(revision))
117 :title => l(:label_revision_id, format_revision(revision))
116 )
118 )
117 end
119 end
118
120
119 # Generates a link to a message
121 # Generates a link to a message
120 def link_to_message(message, options={}, html_options = nil)
122 def link_to_message(message, options={}, html_options = nil)
121 link_to(
123 link_to(
122 h(truncate(message.subject, :length => 60)),
124 h(truncate(message.subject, :length => 60)),
123 { :controller => 'messages', :action => 'show',
125 { :controller => 'messages', :action => 'show',
124 :board_id => message.board_id,
126 :board_id => message.board_id,
125 :id => (message.parent_id || message.id),
127 :id => (message.parent_id || message.id),
126 :r => (message.parent_id && message.id),
128 :r => (message.parent_id && message.id),
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
129 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
128 }.merge(options),
130 }.merge(options),
129 html_options
131 html_options
130 )
132 )
131 end
133 end
132
134
133 # Generates a link to a project if active
135 # Generates a link to a project if active
134 # Examples:
136 # Examples:
135 #
137 #
136 # link_to_project(project) # => link to the specified project overview
138 # link_to_project(project) # => link to the specified project overview
137 # link_to_project(project, :action=>'settings') # => link to project settings
139 # link_to_project(project, :action=>'settings') # => link to project settings
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
140 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
141 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 #
142 #
141 def link_to_project(project, options={}, html_options = nil)
143 def link_to_project(project, options={}, html_options = nil)
142 if project.archived?
144 if project.archived?
143 h(project)
145 h(project)
144 else
146 else
145 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
147 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
146 link_to(h(project), url, html_options)
148 link_to(h(project), url, html_options)
147 end
149 end
148 end
150 end
149
151
150 def thumbnail_tag(attachment)
152 def thumbnail_tag(attachment)
151 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
153 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
152 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
154 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
153 :title => attachment.filename
155 :title => attachment.filename
154 end
156 end
155
157
156 def toggle_link(name, id, options={})
158 def toggle_link(name, id, options={})
157 onclick = "$('##{id}').toggle(); "
159 onclick = "$('##{id}').toggle(); "
158 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
160 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
159 onclick << "return false;"
161 onclick << "return false;"
160 link_to(name, "#", :onclick => onclick)
162 link_to(name, "#", :onclick => onclick)
161 end
163 end
162
164
163 def image_to_function(name, function, html_options = {})
165 def image_to_function(name, function, html_options = {})
164 html_options.symbolize_keys!
166 html_options.symbolize_keys!
165 tag(:input, html_options.merge({
167 tag(:input, html_options.merge({
166 :type => "image", :src => image_path(name),
168 :type => "image", :src => image_path(name),
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
169 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 }))
170 }))
169 end
171 end
170
172
171 def format_activity_title(text)
173 def format_activity_title(text)
172 h(truncate_single_line(text, :length => 100))
174 h(truncate_single_line(text, :length => 100))
173 end
175 end
174
176
175 def format_activity_day(date)
177 def format_activity_day(date)
176 date == User.current.today ? l(:label_today).titleize : format_date(date)
178 date == User.current.today ? l(:label_today).titleize : format_date(date)
177 end
179 end
178
180
179 def format_activity_description(text)
181 def format_activity_description(text)
180 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
182 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
181 ).gsub(/[\r\n]+/, "<br />").html_safe
183 ).gsub(/[\r\n]+/, "<br />").html_safe
182 end
184 end
183
185
184 def format_version_name(version)
186 def format_version_name(version)
185 if version.project == @project
187 if version.project == @project
186 h(version)
188 h(version)
187 else
189 else
188 h("#{version.project} - #{version}")
190 h("#{version.project} - #{version}")
189 end
191 end
190 end
192 end
191
193
192 def due_date_distance_in_words(date)
194 def due_date_distance_in_words(date)
193 if date
195 if date
194 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
196 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
195 end
197 end
196 end
198 end
197
199
198 # Renders a tree of projects as a nested set of unordered lists
200 # Renders a tree of projects as a nested set of unordered lists
199 # The given collection may be a subset of the whole project tree
201 # The given collection may be a subset of the whole project tree
200 # (eg. some intermediate nodes are private and can not be seen)
202 # (eg. some intermediate nodes are private and can not be seen)
201 def render_project_nested_lists(projects)
203 def render_project_nested_lists(projects)
202 s = ''
204 s = ''
203 if projects.any?
205 if projects.any?
204 ancestors = []
206 ancestors = []
205 original_project = @project
207 original_project = @project
206 projects.sort_by(&:lft).each do |project|
208 projects.sort_by(&:lft).each do |project|
207 # set the project environment to please macros.
209 # set the project environment to please macros.
208 @project = project
210 @project = project
209 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
211 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
210 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
212 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
211 else
213 else
212 ancestors.pop
214 ancestors.pop
213 s << "</li>"
215 s << "</li>"
214 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
216 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
215 ancestors.pop
217 ancestors.pop
216 s << "</ul></li>\n"
218 s << "</ul></li>\n"
217 end
219 end
218 end
220 end
219 classes = (ancestors.empty? ? 'root' : 'child')
221 classes = (ancestors.empty? ? 'root' : 'child')
220 s << "<li class='#{classes}'><div class='#{classes}'>"
222 s << "<li class='#{classes}'><div class='#{classes}'>"
221 s << h(block_given? ? yield(project) : project.name)
223 s << h(block_given? ? yield(project) : project.name)
222 s << "</div>\n"
224 s << "</div>\n"
223 ancestors << project
225 ancestors << project
224 end
226 end
225 s << ("</li></ul>\n" * ancestors.size)
227 s << ("</li></ul>\n" * ancestors.size)
226 @project = original_project
228 @project = original_project
227 end
229 end
228 s.html_safe
230 s.html_safe
229 end
231 end
230
232
231 def render_page_hierarchy(pages, node=nil, options={})
233 def render_page_hierarchy(pages, node=nil, options={})
232 content = ''
234 content = ''
233 if pages[node]
235 if pages[node]
234 content << "<ul class=\"pages-hierarchy\">\n"
236 content << "<ul class=\"pages-hierarchy\">\n"
235 pages[node].each do |page|
237 pages[node].each do |page|
236 content << "<li>"
238 content << "<li>"
237 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
239 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
238 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
240 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
239 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
241 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
240 content << "</li>\n"
242 content << "</li>\n"
241 end
243 end
242 content << "</ul>\n"
244 content << "</ul>\n"
243 end
245 end
244 content.html_safe
246 content.html_safe
245 end
247 end
246
248
247 # Renders flash messages
249 # Renders flash messages
248 def render_flash_messages
250 def render_flash_messages
249 s = ''
251 s = ''
250 flash.each do |k,v|
252 flash.each do |k,v|
251 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
253 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
252 end
254 end
253 s.html_safe
255 s.html_safe
254 end
256 end
255
257
256 # Renders tabs and their content
258 # Renders tabs and their content
257 def render_tabs(tabs)
259 def render_tabs(tabs)
258 if tabs.any?
260 if tabs.any?
259 render :partial => 'common/tabs', :locals => {:tabs => tabs}
261 render :partial => 'common/tabs', :locals => {:tabs => tabs}
260 else
262 else
261 content_tag 'p', l(:label_no_data), :class => "nodata"
263 content_tag 'p', l(:label_no_data), :class => "nodata"
262 end
264 end
263 end
265 end
264
266
265 # Renders the project quick-jump box
267 # Renders the project quick-jump box
266 def render_project_jump_box
268 def render_project_jump_box
267 return unless User.current.logged?
269 return unless User.current.logged?
268 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
270 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
269 if projects.any?
271 if projects.any?
270 options =
272 options =
271 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
273 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
272 '<option value="" disabled="disabled">---</option>').html_safe
274 '<option value="" disabled="disabled">---</option>').html_safe
273
275
274 options << project_tree_options_for_select(projects, :selected => @project) do |p|
276 options << project_tree_options_for_select(projects, :selected => @project) do |p|
275 { :value => project_path(:id => p, :jump => current_menu_item) }
277 { :value => project_path(:id => p, :jump => current_menu_item) }
276 end
278 end
277
279
278 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
280 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
279 end
281 end
280 end
282 end
281
283
282 def project_tree_options_for_select(projects, options = {})
284 def project_tree_options_for_select(projects, options = {})
283 s = ''
285 s = ''
284 project_tree(projects) do |project, level|
286 project_tree(projects) do |project, level|
285 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
287 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
286 tag_options = {:value => project.id}
288 tag_options = {:value => project.id}
287 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
289 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
288 tag_options[:selected] = 'selected'
290 tag_options[:selected] = 'selected'
289 else
291 else
290 tag_options[:selected] = nil
292 tag_options[:selected] = nil
291 end
293 end
292 tag_options.merge!(yield(project)) if block_given?
294 tag_options.merge!(yield(project)) if block_given?
293 s << content_tag('option', name_prefix + h(project), tag_options)
295 s << content_tag('option', name_prefix + h(project), tag_options)
294 end
296 end
295 s.html_safe
297 s.html_safe
296 end
298 end
297
299
298 # Yields the given block for each project with its level in the tree
300 # Yields the given block for each project with its level in the tree
299 #
301 #
300 # Wrapper for Project#project_tree
302 # Wrapper for Project#project_tree
301 def project_tree(projects, &block)
303 def project_tree(projects, &block)
302 Project.project_tree(projects, &block)
304 Project.project_tree(projects, &block)
303 end
305 end
304
306
305 def principals_check_box_tags(name, principals)
307 def principals_check_box_tags(name, principals)
306 s = ''
308 s = ''
307 principals.sort.each do |principal|
309 principals.sort.each do |principal|
308 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
310 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
309 end
311 end
310 s.html_safe
312 s.html_safe
311 end
313 end
312
314
313 # Returns a string for users/groups option tags
315 # Returns a string for users/groups option tags
314 def principals_options_for_select(collection, selected=nil)
316 def principals_options_for_select(collection, selected=nil)
315 s = ''
317 s = ''
316 if collection.include?(User.current)
318 if collection.include?(User.current)
317 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
319 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
318 end
320 end
319 groups = ''
321 groups = ''
320 collection.sort.each do |element|
322 collection.sort.each do |element|
321 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
323 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
322 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
324 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
323 end
325 end
324 unless groups.empty?
326 unless groups.empty?
325 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
327 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
326 end
328 end
327 s.html_safe
329 s.html_safe
328 end
330 end
329
331
330 # Truncates and returns the string as a single line
332 # Truncates and returns the string as a single line
331 def truncate_single_line(string, *args)
333 def truncate_single_line(string, *args)
332 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
334 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
333 end
335 end
334
336
335 # Truncates at line break after 250 characters or options[:length]
337 # Truncates at line break after 250 characters or options[:length]
336 def truncate_lines(string, options={})
338 def truncate_lines(string, options={})
337 length = options[:length] || 250
339 length = options[:length] || 250
338 if string.to_s =~ /\A(.{#{length}}.*?)$/m
340 if string.to_s =~ /\A(.{#{length}}.*?)$/m
339 "#{$1}..."
341 "#{$1}..."
340 else
342 else
341 string
343 string
342 end
344 end
343 end
345 end
344
346
345 def anchor(text)
347 def anchor(text)
346 text.to_s.gsub(' ', '_')
348 text.to_s.gsub(' ', '_')
347 end
349 end
348
350
349 def html_hours(text)
351 def html_hours(text)
350 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
352 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
351 end
353 end
352
354
353 def authoring(created, author, options={})
355 def authoring(created, author, options={})
354 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
356 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
355 end
357 end
356
358
357 def time_tag(time)
359 def time_tag(time)
358 text = distance_of_time_in_words(Time.now, time)
360 text = distance_of_time_in_words(Time.now, time)
359 if @project
361 if @project
360 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
362 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
361 else
363 else
362 content_tag('acronym', text, :title => format_time(time))
364 content_tag('acronym', text, :title => format_time(time))
363 end
365 end
364 end
366 end
365
367
366 def syntax_highlight_lines(name, content)
368 def syntax_highlight_lines(name, content)
367 lines = []
369 lines = []
368 syntax_highlight(name, content).each_line { |line| lines << line }
370 syntax_highlight(name, content).each_line { |line| lines << line }
369 lines
371 lines
370 end
372 end
371
373
372 def syntax_highlight(name, content)
374 def syntax_highlight(name, content)
373 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
375 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
374 end
376 end
375
377
376 def to_path_param(path)
378 def to_path_param(path)
377 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
379 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
378 str.blank? ? nil : str
380 str.blank? ? nil : str
379 end
381 end
380
382
381 def pagination_links_full(paginator, count=nil, options={})
383 def pagination_links_full(paginator, count=nil, options={})
382 page_param = options.delete(:page_param) || :page
384 page_param = options.delete(:page_param) || :page
383 per_page_links = options.delete(:per_page_links)
385 per_page_links = options.delete(:per_page_links)
384 url_param = params.dup
386 url_param = params.dup
385
387
386 html = ''
388 html = ''
387 if paginator.current.previous
389 if paginator.current.previous
388 # \xc2\xab(utf-8) = &#171;
390 # \xc2\xab(utf-8) = &#171;
389 html << link_to_content_update(
391 html << link_to_content_update(
390 "\xc2\xab " + l(:label_previous),
392 "\xc2\xab " + l(:label_previous),
391 url_param.merge(page_param => paginator.current.previous)) + ' '
393 url_param.merge(page_param => paginator.current.previous)) + ' '
392 end
394 end
393
395
394 html << (pagination_links_each(paginator, options) do |n|
396 html << (pagination_links_each(paginator, options) do |n|
395 link_to_content_update(n.to_s, url_param.merge(page_param => n))
397 link_to_content_update(n.to_s, url_param.merge(page_param => n))
396 end || '')
398 end || '')
397
399
398 if paginator.current.next
400 if paginator.current.next
399 # \xc2\xbb(utf-8) = &#187;
401 # \xc2\xbb(utf-8) = &#187;
400 html << ' ' + link_to_content_update(
402 html << ' ' + link_to_content_update(
401 (l(:label_next) + " \xc2\xbb"),
403 (l(:label_next) + " \xc2\xbb"),
402 url_param.merge(page_param => paginator.current.next))
404 url_param.merge(page_param => paginator.current.next))
403 end
405 end
404
406
405 unless count.nil?
407 unless count.nil?
406 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
408 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
407 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
409 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
408 html << " | #{links}"
410 html << " | #{links}"
409 end
411 end
410 end
412 end
411
413
412 html.html_safe
414 html.html_safe
413 end
415 end
414
416
415 def per_page_links(selected=nil, item_count=nil)
417 def per_page_links(selected=nil, item_count=nil)
416 values = Setting.per_page_options_array
418 values = Setting.per_page_options_array
417 if item_count && values.any?
419 if item_count && values.any?
418 if item_count > values.first
420 if item_count > values.first
419 max = values.detect {|value| value >= item_count} || item_count
421 max = values.detect {|value| value >= item_count} || item_count
420 else
422 else
421 max = item_count
423 max = item_count
422 end
424 end
423 values = values.select {|value| value <= max || value == selected}
425 values = values.select {|value| value <= max || value == selected}
424 end
426 end
425 if values.empty? || (values.size == 1 && values.first == selected)
427 if values.empty? || (values.size == 1 && values.first == selected)
426 return nil
428 return nil
427 end
429 end
428 links = values.collect do |n|
430 links = values.collect do |n|
429 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
431 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
430 end
432 end
431 l(:label_display_per_page, links.join(', '))
433 l(:label_display_per_page, links.join(', '))
432 end
434 end
433
435
434 def reorder_links(name, url, method = :post)
436 def reorder_links(name, url, method = :post)
435 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
437 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
436 url.merge({"#{name}[move_to]" => 'highest'}),
438 url.merge({"#{name}[move_to]" => 'highest'}),
437 :method => method, :title => l(:label_sort_highest)) +
439 :method => method, :title => l(:label_sort_highest)) +
438 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
440 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
439 url.merge({"#{name}[move_to]" => 'higher'}),
441 url.merge({"#{name}[move_to]" => 'higher'}),
440 :method => method, :title => l(:label_sort_higher)) +
442 :method => method, :title => l(:label_sort_higher)) +
441 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
443 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
442 url.merge({"#{name}[move_to]" => 'lower'}),
444 url.merge({"#{name}[move_to]" => 'lower'}),
443 :method => method, :title => l(:label_sort_lower)) +
445 :method => method, :title => l(:label_sort_lower)) +
444 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
446 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
445 url.merge({"#{name}[move_to]" => 'lowest'}),
447 url.merge({"#{name}[move_to]" => 'lowest'}),
446 :method => method, :title => l(:label_sort_lowest))
448 :method => method, :title => l(:label_sort_lowest))
447 end
449 end
448
450
449 def breadcrumb(*args)
451 def breadcrumb(*args)
450 elements = args.flatten
452 elements = args.flatten
451 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
453 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
452 end
454 end
453
455
454 def other_formats_links(&block)
456 def other_formats_links(&block)
455 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
457 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
456 yield Redmine::Views::OtherFormatsBuilder.new(self)
458 yield Redmine::Views::OtherFormatsBuilder.new(self)
457 concat('</p>'.html_safe)
459 concat('</p>'.html_safe)
458 end
460 end
459
461
460 def page_header_title
462 def page_header_title
461 if @project.nil? || @project.new_record?
463 if @project.nil? || @project.new_record?
462 h(Setting.app_title)
464 h(Setting.app_title)
463 else
465 else
464 b = []
466 b = []
465 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
467 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
466 if ancestors.any?
468 if ancestors.any?
467 root = ancestors.shift
469 root = ancestors.shift
468 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
470 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
469 if ancestors.size > 2
471 if ancestors.size > 2
470 b << "\xe2\x80\xa6"
472 b << "\xe2\x80\xa6"
471 ancestors = ancestors[-2, 2]
473 ancestors = ancestors[-2, 2]
472 end
474 end
473 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
475 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
474 end
476 end
475 b << h(@project)
477 b << h(@project)
476 b.join(" \xc2\xbb ").html_safe
478 b.join(" \xc2\xbb ").html_safe
477 end
479 end
478 end
480 end
479
481
480 def html_title(*args)
482 def html_title(*args)
481 if args.empty?
483 if args.empty?
482 title = @html_title || []
484 title = @html_title || []
483 title << @project.name if @project
485 title << @project.name if @project
484 title << Setting.app_title unless Setting.app_title == title.last
486 title << Setting.app_title unless Setting.app_title == title.last
485 title.select {|t| !t.blank? }.join(' - ')
487 title.select {|t| !t.blank? }.join(' - ')
486 else
488 else
487 @html_title ||= []
489 @html_title ||= []
488 @html_title += args
490 @html_title += args
489 end
491 end
490 end
492 end
491
493
492 # Returns the theme, controller name, and action as css classes for the
494 # Returns the theme, controller name, and action as css classes for the
493 # HTML body.
495 # HTML body.
494 def body_css_classes
496 def body_css_classes
495 css = []
497 css = []
496 if theme = Redmine::Themes.theme(Setting.ui_theme)
498 if theme = Redmine::Themes.theme(Setting.ui_theme)
497 css << 'theme-' + theme.name
499 css << 'theme-' + theme.name
498 end
500 end
499
501
500 css << 'controller-' + controller_name
502 css << 'controller-' + controller_name
501 css << 'action-' + action_name
503 css << 'action-' + action_name
502 css.join(' ')
504 css.join(' ')
503 end
505 end
504
506
505 def accesskey(s)
507 def accesskey(s)
506 Redmine::AccessKeys.key_for s
508 Redmine::AccessKeys.key_for s
507 end
509 end
508
510
509 # Formats text according to system settings.
511 # Formats text according to system settings.
510 # 2 ways to call this method:
512 # 2 ways to call this method:
511 # * with a String: textilizable(text, options)
513 # * with a String: textilizable(text, options)
512 # * with an object and one of its attribute: textilizable(issue, :description, options)
514 # * with an object and one of its attribute: textilizable(issue, :description, options)
513 def textilizable(*args)
515 def textilizable(*args)
514 options = args.last.is_a?(Hash) ? args.pop : {}
516 options = args.last.is_a?(Hash) ? args.pop : {}
515 case args.size
517 case args.size
516 when 1
518 when 1
517 obj = options[:object]
519 obj = options[:object]
518 text = args.shift
520 text = args.shift
519 when 2
521 when 2
520 obj = args.shift
522 obj = args.shift
521 attr = args.shift
523 attr = args.shift
522 text = obj.send(attr).to_s
524 text = obj.send(attr).to_s
523 else
525 else
524 raise ArgumentError, 'invalid arguments to textilizable'
526 raise ArgumentError, 'invalid arguments to textilizable'
525 end
527 end
526 return '' if text.blank?
528 return '' if text.blank?
527 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
529 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
528 only_path = options.delete(:only_path) == false ? false : true
530 only_path = options.delete(:only_path) == false ? false : true
529
531
530 text = text.dup
532 text = text.dup
531 macros = catch_macros(text)
533 macros = catch_macros(text)
532 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
534 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
533
535
534 @parsed_headings = []
536 @parsed_headings = []
535 @heading_anchors = {}
537 @heading_anchors = {}
536 @current_section = 0 if options[:edit_section_links]
538 @current_section = 0 if options[:edit_section_links]
537
539
538 parse_sections(text, project, obj, attr, only_path, options)
540 parse_sections(text, project, obj, attr, only_path, options)
539 text = parse_non_pre_blocks(text, obj, macros) do |text|
541 text = parse_non_pre_blocks(text, obj, macros) do |text|
540 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
542 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
541 send method_name, text, project, obj, attr, only_path, options
543 send method_name, text, project, obj, attr, only_path, options
542 end
544 end
543 end
545 end
544 parse_headings(text, project, obj, attr, only_path, options)
546 parse_headings(text, project, obj, attr, only_path, options)
545
547
546 if @parsed_headings.any?
548 if @parsed_headings.any?
547 replace_toc(text, @parsed_headings)
549 replace_toc(text, @parsed_headings)
548 end
550 end
549
551
550 text.html_safe
552 text.html_safe
551 end
553 end
552
554
553 def parse_non_pre_blocks(text, obj, macros)
555 def parse_non_pre_blocks(text, obj, macros)
554 s = StringScanner.new(text)
556 s = StringScanner.new(text)
555 tags = []
557 tags = []
556 parsed = ''
558 parsed = ''
557 while !s.eos?
559 while !s.eos?
558 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
560 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
559 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
561 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
560 if tags.empty?
562 if tags.empty?
561 yield text
563 yield text
562 inject_macros(text, obj, macros) if macros.any?
564 inject_macros(text, obj, macros) if macros.any?
563 else
565 else
564 inject_macros(text, obj, macros, false) if macros.any?
566 inject_macros(text, obj, macros, false) if macros.any?
565 end
567 end
566 parsed << text
568 parsed << text
567 if tag
569 if tag
568 if closing
570 if closing
569 if tags.last == tag.downcase
571 if tags.last == tag.downcase
570 tags.pop
572 tags.pop
571 end
573 end
572 else
574 else
573 tags << tag.downcase
575 tags << tag.downcase
574 end
576 end
575 parsed << full_tag
577 parsed << full_tag
576 end
578 end
577 end
579 end
578 # Close any non closing tags
580 # Close any non closing tags
579 while tag = tags.pop
581 while tag = tags.pop
580 parsed << "</#{tag}>"
582 parsed << "</#{tag}>"
581 end
583 end
582 parsed
584 parsed
583 end
585 end
584
586
585 def parse_inline_attachments(text, project, obj, attr, only_path, options)
587 def parse_inline_attachments(text, project, obj, attr, only_path, options)
586 # when using an image link, try to use an attachment, if possible
588 # when using an image link, try to use an attachment, if possible
587 if options[:attachments] || (obj && obj.respond_to?(:attachments))
589 if options[:attachments] || (obj && obj.respond_to?(:attachments))
588 attachments = options[:attachments] || obj.attachments
590 attachments = options[:attachments] || obj.attachments
589 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
591 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
590 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
592 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
591 # search for the picture in attachments
593 # search for the picture in attachments
592 if found = Attachment.latest_attach(attachments, filename)
594 if found = Attachment.latest_attach(attachments, filename)
593 image_url = url_for :only_path => only_path, :controller => 'attachments',
595 image_url = url_for :only_path => only_path, :controller => 'attachments',
594 :action => 'download', :id => found
596 :action => 'download', :id => found
595 desc = found.description.to_s.gsub('"', '')
597 desc = found.description.to_s.gsub('"', '')
596 if !desc.blank? && alttext.blank?
598 if !desc.blank? && alttext.blank?
597 alt = " title=\"#{desc}\" alt=\"#{desc}\""
599 alt = " title=\"#{desc}\" alt=\"#{desc}\""
598 end
600 end
599 "src=\"#{image_url}\"#{alt}"
601 "src=\"#{image_url}\"#{alt}"
600 else
602 else
601 m
603 m
602 end
604 end
603 end
605 end
604 end
606 end
605 end
607 end
606
608
607 # Wiki links
609 # Wiki links
608 #
610 #
609 # Examples:
611 # Examples:
610 # [[mypage]]
612 # [[mypage]]
611 # [[mypage|mytext]]
613 # [[mypage|mytext]]
612 # wiki links can refer other project wikis, using project name or identifier:
614 # wiki links can refer other project wikis, using project name or identifier:
613 # [[project:]] -> wiki starting page
615 # [[project:]] -> wiki starting page
614 # [[project:|mytext]]
616 # [[project:|mytext]]
615 # [[project:mypage]]
617 # [[project:mypage]]
616 # [[project:mypage|mytext]]
618 # [[project:mypage|mytext]]
617 def parse_wiki_links(text, project, obj, attr, only_path, options)
619 def parse_wiki_links(text, project, obj, attr, only_path, options)
618 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
620 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
619 link_project = project
621 link_project = project
620 esc, all, page, title = $1, $2, $3, $5
622 esc, all, page, title = $1, $2, $3, $5
621 if esc.nil?
623 if esc.nil?
622 if page =~ /^([^\:]+)\:(.*)$/
624 if page =~ /^([^\:]+)\:(.*)$/
623 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
625 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
624 page = $2
626 page = $2
625 title ||= $1 if page.blank?
627 title ||= $1 if page.blank?
626 end
628 end
627
629
628 if link_project && link_project.wiki
630 if link_project && link_project.wiki
629 # extract anchor
631 # extract anchor
630 anchor = nil
632 anchor = nil
631 if page =~ /^(.+?)\#(.+)$/
633 if page =~ /^(.+?)\#(.+)$/
632 page, anchor = $1, $2
634 page, anchor = $1, $2
633 end
635 end
634 anchor = sanitize_anchor_name(anchor) if anchor.present?
636 anchor = sanitize_anchor_name(anchor) if anchor.present?
635 # check if page exists
637 # check if page exists
636 wiki_page = link_project.wiki.find_page(page)
638 wiki_page = link_project.wiki.find_page(page)
637 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
639 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
638 "##{anchor}"
640 "##{anchor}"
639 else
641 else
640 case options[:wiki_links]
642 case options[:wiki_links]
641 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
643 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
642 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
644 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
643 else
645 else
644 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
646 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
645 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
647 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
646 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
648 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
647 :id => wiki_page_id, :anchor => anchor, :parent => parent)
649 :id => wiki_page_id, :anchor => anchor, :parent => parent)
648 end
650 end
649 end
651 end
650 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
652 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
651 else
653 else
652 # project or wiki doesn't exist
654 # project or wiki doesn't exist
653 all
655 all
654 end
656 end
655 else
657 else
656 all
658 all
657 end
659 end
658 end
660 end
659 end
661 end
660
662
661 # Redmine links
663 # Redmine links
662 #
664 #
663 # Examples:
665 # Examples:
664 # Issues:
666 # Issues:
665 # #52 -> Link to issue #52
667 # #52 -> Link to issue #52
666 # Changesets:
668 # Changesets:
667 # r52 -> Link to revision 52
669 # r52 -> Link to revision 52
668 # commit:a85130f -> Link to scmid starting with a85130f
670 # commit:a85130f -> Link to scmid starting with a85130f
669 # Documents:
671 # Documents:
670 # document#17 -> Link to document with id 17
672 # document#17 -> Link to document with id 17
671 # document:Greetings -> Link to the document with title "Greetings"
673 # document:Greetings -> Link to the document with title "Greetings"
672 # document:"Some document" -> Link to the document with title "Some document"
674 # document:"Some document" -> Link to the document with title "Some document"
673 # Versions:
675 # Versions:
674 # version#3 -> Link to version with id 3
676 # version#3 -> Link to version with id 3
675 # version:1.0.0 -> Link to version named "1.0.0"
677 # version:1.0.0 -> Link to version named "1.0.0"
676 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
678 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
677 # Attachments:
679 # Attachments:
678 # attachment:file.zip -> Link to the attachment of the current object named file.zip
680 # attachment:file.zip -> Link to the attachment of the current object named file.zip
679 # Source files:
681 # Source files:
680 # source:some/file -> Link to the file located at /some/file in the project's repository
682 # source:some/file -> Link to the file located at /some/file in the project's repository
681 # source:some/file@52 -> Link to the file's revision 52
683 # source:some/file@52 -> Link to the file's revision 52
682 # source:some/file#L120 -> Link to line 120 of the file
684 # source:some/file#L120 -> Link to line 120 of the file
683 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
685 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
684 # export:some/file -> Force the download of the file
686 # export:some/file -> Force the download of the file
685 # Forum messages:
687 # Forum messages:
686 # message#1218 -> Link to message with id 1218
688 # message#1218 -> Link to message with id 1218
687 #
689 #
688 # Links can refer other objects from other projects, using project identifier:
690 # Links can refer other objects from other projects, using project identifier:
689 # identifier:r52
691 # identifier:r52
690 # identifier:document:"Some document"
692 # identifier:document:"Some document"
691 # identifier:version:1.0.0
693 # identifier:version:1.0.0
692 # identifier:source:some/file
694 # identifier:source:some/file
693 def parse_redmine_links(text, project, obj, attr, only_path, options)
695 def parse_redmine_links(text, project, obj, attr, only_path, options)
694 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
696 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
695 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
697 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
696 link = nil
698 link = nil
697 if project_identifier
699 if project_identifier
698 project = Project.visible.find_by_identifier(project_identifier)
700 project = Project.visible.find_by_identifier(project_identifier)
699 end
701 end
700 if esc.nil?
702 if esc.nil?
701 if prefix.nil? && sep == 'r'
703 if prefix.nil? && sep == 'r'
702 if project
704 if project
703 repository = nil
705 repository = nil
704 if repo_identifier
706 if repo_identifier
705 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
707 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
706 else
708 else
707 repository = project.repository
709 repository = project.repository
708 end
710 end
709 # project.changesets.visible raises an SQL error because of a double join on repositories
711 # project.changesets.visible raises an SQL error because of a double join on repositories
710 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
712 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
711 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
713 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
712 :class => 'changeset',
714 :class => 'changeset',
713 :title => truncate_single_line(changeset.comments, :length => 100))
715 :title => truncate_single_line(changeset.comments, :length => 100))
714 end
716 end
715 end
717 end
716 elsif sep == '#'
718 elsif sep == '#'
717 oid = identifier.to_i
719 oid = identifier.to_i
718 case prefix
720 case prefix
719 when nil
721 when nil
720 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
722 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
721 anchor = comment_id ? "note-#{comment_id}" : nil
723 anchor = comment_id ? "note-#{comment_id}" : nil
722 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
724 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
723 :class => issue.css_classes,
725 :class => issue.css_classes,
724 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
726 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
725 end
727 end
726 when 'document'
728 when 'document'
727 if document = Document.visible.find_by_id(oid)
729 if document = Document.visible.find_by_id(oid)
728 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
730 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
729 :class => 'document'
731 :class => 'document'
730 end
732 end
731 when 'version'
733 when 'version'
732 if version = Version.visible.find_by_id(oid)
734 if version = Version.visible.find_by_id(oid)
733 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
735 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
734 :class => 'version'
736 :class => 'version'
735 end
737 end
736 when 'message'
738 when 'message'
737 if message = Message.visible.find_by_id(oid, :include => :parent)
739 if message = Message.visible.find_by_id(oid, :include => :parent)
738 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
740 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
739 end
741 end
740 when 'forum'
742 when 'forum'
741 if board = Board.visible.find_by_id(oid)
743 if board = Board.visible.find_by_id(oid)
742 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
744 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
743 :class => 'board'
745 :class => 'board'
744 end
746 end
745 when 'news'
747 when 'news'
746 if news = News.visible.find_by_id(oid)
748 if news = News.visible.find_by_id(oid)
747 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
749 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
748 :class => 'news'
750 :class => 'news'
749 end
751 end
750 when 'project'
752 when 'project'
751 if p = Project.visible.find_by_id(oid)
753 if p = Project.visible.find_by_id(oid)
752 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
754 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
753 end
755 end
754 end
756 end
755 elsif sep == ':'
757 elsif sep == ':'
756 # removes the double quotes if any
758 # removes the double quotes if any
757 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
759 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
758 case prefix
760 case prefix
759 when 'document'
761 when 'document'
760 if project && document = project.documents.visible.find_by_title(name)
762 if project && document = project.documents.visible.find_by_title(name)
761 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
763 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
762 :class => 'document'
764 :class => 'document'
763 end
765 end
764 when 'version'
766 when 'version'
765 if project && version = project.versions.visible.find_by_name(name)
767 if project && version = project.versions.visible.find_by_name(name)
766 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
768 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
767 :class => 'version'
769 :class => 'version'
768 end
770 end
769 when 'forum'
771 when 'forum'
770 if project && board = project.boards.visible.find_by_name(name)
772 if project && board = project.boards.visible.find_by_name(name)
771 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
773 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
772 :class => 'board'
774 :class => 'board'
773 end
775 end
774 when 'news'
776 when 'news'
775 if project && news = project.news.visible.find_by_title(name)
777 if project && news = project.news.visible.find_by_title(name)
776 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
778 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
777 :class => 'news'
779 :class => 'news'
778 end
780 end
779 when 'commit', 'source', 'export'
781 when 'commit', 'source', 'export'
780 if project
782 if project
781 repository = nil
783 repository = nil
782 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
784 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
783 repo_prefix, repo_identifier, name = $1, $2, $3
785 repo_prefix, repo_identifier, name = $1, $2, $3
784 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
786 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
785 else
787 else
786 repository = project.repository
788 repository = project.repository
787 end
789 end
788 if prefix == 'commit'
790 if prefix == 'commit'
789 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
791 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
790 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
792 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
791 :class => 'changeset',
793 :class => 'changeset',
792 :title => truncate_single_line(h(changeset.comments), :length => 100)
794 :title => truncate_single_line(h(changeset.comments), :length => 100)
793 end
795 end
794 else
796 else
795 if repository && User.current.allowed_to?(:browse_repository, project)
797 if repository && User.current.allowed_to?(:browse_repository, project)
796 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
798 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
797 path, rev, anchor = $1, $3, $5
799 path, rev, anchor = $1, $3, $5
798 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
800 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param,
799 :path => to_path_param(path),
801 :path => to_path_param(path),
800 :rev => rev,
802 :rev => rev,
801 :anchor => anchor,
803 :anchor => anchor,
802 :format => (prefix == 'export' ? 'raw' : nil)},
804 :format => (prefix == 'export' ? 'raw' : nil)},
803 :class => (prefix == 'export' ? 'source download' : 'source')
805 :class => (prefix == 'export' ? 'source download' : 'source')
804 end
806 end
805 end
807 end
806 repo_prefix = nil
808 repo_prefix = nil
807 end
809 end
808 when 'attachment'
810 when 'attachment'
809 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
811 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
810 if attachments && attachment = attachments.detect {|a| a.filename == name }
812 if attachments && attachment = attachments.detect {|a| a.filename == name }
811 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
813 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
812 :class => 'attachment'
814 :class => 'attachment'
813 end
815 end
814 when 'project'
816 when 'project'
815 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
817 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
816 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
818 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
817 end
819 end
818 end
820 end
819 end
821 end
820 end
822 end
821 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
823 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
822 end
824 end
823 end
825 end
824
826
825 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
827 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
826
828
827 def parse_sections(text, project, obj, attr, only_path, options)
829 def parse_sections(text, project, obj, attr, only_path, options)
828 return unless options[:edit_section_links]
830 return unless options[:edit_section_links]
829 text.gsub!(HEADING_RE) do
831 text.gsub!(HEADING_RE) do
830 heading = $1
832 heading = $1
831 @current_section += 1
833 @current_section += 1
832 if @current_section > 1
834 if @current_section > 1
833 content_tag('div',
835 content_tag('div',
834 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
836 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
835 :class => 'contextual',
837 :class => 'contextual',
836 :title => l(:button_edit_section)) + heading.html_safe
838 :title => l(:button_edit_section)) + heading.html_safe
837 else
839 else
838 heading
840 heading
839 end
841 end
840 end
842 end
841 end
843 end
842
844
843 # Headings and TOC
845 # Headings and TOC
844 # Adds ids and links to headings unless options[:headings] is set to false
846 # Adds ids and links to headings unless options[:headings] is set to false
845 def parse_headings(text, project, obj, attr, only_path, options)
847 def parse_headings(text, project, obj, attr, only_path, options)
846 return if options[:headings] == false
848 return if options[:headings] == false
847
849
848 text.gsub!(HEADING_RE) do
850 text.gsub!(HEADING_RE) do
849 level, attrs, content = $2.to_i, $3, $4
851 level, attrs, content = $2.to_i, $3, $4
850 item = strip_tags(content).strip
852 item = strip_tags(content).strip
851 anchor = sanitize_anchor_name(item)
853 anchor = sanitize_anchor_name(item)
852 # used for single-file wiki export
854 # used for single-file wiki export
853 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
855 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
854 @heading_anchors[anchor] ||= 0
856 @heading_anchors[anchor] ||= 0
855 idx = (@heading_anchors[anchor] += 1)
857 idx = (@heading_anchors[anchor] += 1)
856 if idx > 1
858 if idx > 1
857 anchor = "#{anchor}-#{idx}"
859 anchor = "#{anchor}-#{idx}"
858 end
860 end
859 @parsed_headings << [level, anchor, item]
861 @parsed_headings << [level, anchor, item]
860 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
862 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
861 end
863 end
862 end
864 end
863
865
864 MACROS_RE = /(
866 MACROS_RE = /(
865 (!)? # escaping
867 (!)? # escaping
866 (
868 (
867 \{\{ # opening tag
869 \{\{ # opening tag
868 ([\w]+) # macro name
870 ([\w]+) # macro name
869 (\(([^\n\r]*?)\))? # optional arguments
871 (\(([^\n\r]*?)\))? # optional arguments
870 ([\n\r].*?[\n\r])? # optional block of text
872 ([\n\r].*?[\n\r])? # optional block of text
871 \}\} # closing tag
873 \}\} # closing tag
872 )
874 )
873 )/mx unless const_defined?(:MACROS_RE)
875 )/mx unless const_defined?(:MACROS_RE)
874
876
875 MACRO_SUB_RE = /(
877 MACRO_SUB_RE = /(
876 \{\{
878 \{\{
877 macro\((\d+)\)
879 macro\((\d+)\)
878 \}\}
880 \}\}
879 )/x unless const_defined?(:MACRO_SUB_RE)
881 )/x unless const_defined?(:MACRO_SUB_RE)
880
882
881 # Extracts macros from text
883 # Extracts macros from text
882 def catch_macros(text)
884 def catch_macros(text)
883 macros = {}
885 macros = {}
884 text.gsub!(MACROS_RE) do
886 text.gsub!(MACROS_RE) do
885 all, macro = $1, $4.downcase
887 all, macro = $1, $4.downcase
886 if macro_exists?(macro) || all =~ MACRO_SUB_RE
888 if macro_exists?(macro) || all =~ MACRO_SUB_RE
887 index = macros.size
889 index = macros.size
888 macros[index] = all
890 macros[index] = all
889 "{{macro(#{index})}}"
891 "{{macro(#{index})}}"
890 else
892 else
891 all
893 all
892 end
894 end
893 end
895 end
894 macros
896 macros
895 end
897 end
896
898
897 # Executes and replaces macros in text
899 # Executes and replaces macros in text
898 def inject_macros(text, obj, macros, execute=true)
900 def inject_macros(text, obj, macros, execute=true)
899 text.gsub!(MACRO_SUB_RE) do
901 text.gsub!(MACRO_SUB_RE) do
900 all, index = $1, $2.to_i
902 all, index = $1, $2.to_i
901 orig = macros.delete(index)
903 orig = macros.delete(index)
902 if execute && orig && orig =~ MACROS_RE
904 if execute && orig && orig =~ MACROS_RE
903 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
905 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
904 if esc.nil?
906 if esc.nil?
905 h(exec_macro(macro, obj, args, block) || all)
907 h(exec_macro(macro, obj, args, block) || all)
906 else
908 else
907 h(all)
909 h(all)
908 end
910 end
909 elsif orig
911 elsif orig
910 h(orig)
912 h(orig)
911 else
913 else
912 h(all)
914 h(all)
913 end
915 end
914 end
916 end
915 end
917 end
916
918
917 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
919 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
918
920
919 # Renders the TOC with given headings
921 # Renders the TOC with given headings
920 def replace_toc(text, headings)
922 def replace_toc(text, headings)
921 text.gsub!(TOC_RE) do
923 text.gsub!(TOC_RE) do
922 # Keep only the 4 first levels
924 # Keep only the 4 first levels
923 headings = headings.select{|level, anchor, item| level <= 4}
925 headings = headings.select{|level, anchor, item| level <= 4}
924 if headings.empty?
926 if headings.empty?
925 ''
927 ''
926 else
928 else
927 div_class = 'toc'
929 div_class = 'toc'
928 div_class << ' right' if $1 == '>'
930 div_class << ' right' if $1 == '>'
929 div_class << ' left' if $1 == '<'
931 div_class << ' left' if $1 == '<'
930 out = "<ul class=\"#{div_class}\"><li>"
932 out = "<ul class=\"#{div_class}\"><li>"
931 root = headings.map(&:first).min
933 root = headings.map(&:first).min
932 current = root
934 current = root
933 started = false
935 started = false
934 headings.each do |level, anchor, item|
936 headings.each do |level, anchor, item|
935 if level > current
937 if level > current
936 out << '<ul><li>' * (level - current)
938 out << '<ul><li>' * (level - current)
937 elsif level < current
939 elsif level < current
938 out << "</li></ul>\n" * (current - level) + "</li><li>"
940 out << "</li></ul>\n" * (current - level) + "</li><li>"
939 elsif started
941 elsif started
940 out << '</li><li>'
942 out << '</li><li>'
941 end
943 end
942 out << "<a href=\"##{anchor}\">#{item}</a>"
944 out << "<a href=\"##{anchor}\">#{item}</a>"
943 current = level
945 current = level
944 started = true
946 started = true
945 end
947 end
946 out << '</li></ul>' * (current - root)
948 out << '</li></ul>' * (current - root)
947 out << '</li></ul>'
949 out << '</li></ul>'
948 end
950 end
949 end
951 end
950 end
952 end
951
953
952 # Same as Rails' simple_format helper without using paragraphs
954 # Same as Rails' simple_format helper without using paragraphs
953 def simple_format_without_paragraph(text)
955 def simple_format_without_paragraph(text)
954 text.to_s.
956 text.to_s.
955 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
957 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
956 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
958 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
957 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
959 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
958 html_safe
960 html_safe
959 end
961 end
960
962
961 def lang_options_for_select(blank=true)
963 def lang_options_for_select(blank=true)
962 (blank ? [["(auto)", ""]] : []) +
964 (blank ? [["(auto)", ""]] : []) +
963 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
965 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
964 end
966 end
965
967
966 def label_tag_for(name, option_tags = nil, options = {})
968 def label_tag_for(name, option_tags = nil, options = {})
967 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
969 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
968 content_tag("label", label_text)
970 content_tag("label", label_text)
969 end
971 end
970
972
971 def labelled_form_for(*args, &proc)
973 def labelled_form_for(*args, &proc)
972 args << {} unless args.last.is_a?(Hash)
974 args << {} unless args.last.is_a?(Hash)
973 options = args.last
975 options = args.last
974 if args.first.is_a?(Symbol)
976 if args.first.is_a?(Symbol)
975 options.merge!(:as => args.shift)
977 options.merge!(:as => args.shift)
976 end
978 end
977 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
979 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
978 form_for(*args, &proc)
980 form_for(*args, &proc)
979 end
981 end
980
982
981 def labelled_fields_for(*args, &proc)
983 def labelled_fields_for(*args, &proc)
982 args << {} unless args.last.is_a?(Hash)
984 args << {} unless args.last.is_a?(Hash)
983 options = args.last
985 options = args.last
984 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
986 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
985 fields_for(*args, &proc)
987 fields_for(*args, &proc)
986 end
988 end
987
989
988 def labelled_remote_form_for(*args, &proc)
990 def labelled_remote_form_for(*args, &proc)
989 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
991 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
990 args << {} unless args.last.is_a?(Hash)
992 args << {} unless args.last.is_a?(Hash)
991 options = args.last
993 options = args.last
992 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
994 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
993 form_for(*args, &proc)
995 form_for(*args, &proc)
994 end
996 end
995
997
996 def error_messages_for(*objects)
998 def error_messages_for(*objects)
997 html = ""
999 html = ""
998 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1000 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
999 errors = objects.map {|o| o.errors.full_messages}.flatten
1001 errors = objects.map {|o| o.errors.full_messages}.flatten
1000 if errors.any?
1002 if errors.any?
1001 html << "<div id='errorExplanation'><ul>\n"
1003 html << "<div id='errorExplanation'><ul>\n"
1002 errors.each do |error|
1004 errors.each do |error|
1003 html << "<li>#{h error}</li>\n"
1005 html << "<li>#{h error}</li>\n"
1004 end
1006 end
1005 html << "</ul></div>\n"
1007 html << "</ul></div>\n"
1006 end
1008 end
1007 html.html_safe
1009 html.html_safe
1008 end
1010 end
1009
1011
1010 def delete_link(url, options={})
1012 def delete_link(url, options={})
1011 options = {
1013 options = {
1012 :method => :delete,
1014 :method => :delete,
1013 :data => {:confirm => l(:text_are_you_sure)},
1015 :data => {:confirm => l(:text_are_you_sure)},
1014 :class => 'icon icon-del'
1016 :class => 'icon icon-del'
1015 }.merge(options)
1017 }.merge(options)
1016
1018
1017 link_to l(:button_delete), url, options
1019 link_to l(:button_delete), url, options
1018 end
1020 end
1019
1021
1020 def preview_link(url, form, target='preview', options={})
1022 def preview_link(url, form, target='preview', options={})
1021 content_tag 'a', l(:label_preview), {
1023 content_tag 'a', l(:label_preview), {
1022 :href => "#",
1024 :href => "#",
1023 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1025 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1024 :accesskey => accesskey(:preview)
1026 :accesskey => accesskey(:preview)
1025 }.merge(options)
1027 }.merge(options)
1026 end
1028 end
1027
1029
1028 def link_to_function(name, function, html_options={})
1030 def link_to_function(name, function, html_options={})
1029 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1031 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1030 end
1032 end
1031
1033
1032 # Helper to render JSON in views
1034 # Helper to render JSON in views
1033 def raw_json(arg)
1035 def raw_json(arg)
1034 arg.to_json.to_s.gsub('/', '\/').html_safe
1036 arg.to_json.to_s.gsub('/', '\/').html_safe
1035 end
1037 end
1036
1038
1037 def back_url
1039 def back_url
1038 url = params[:back_url]
1040 url = params[:back_url]
1039 if url.nil? && referer = request.env['HTTP_REFERER']
1041 if url.nil? && referer = request.env['HTTP_REFERER']
1040 url = CGI.unescape(referer.to_s)
1042 url = CGI.unescape(referer.to_s)
1041 end
1043 end
1042 url
1044 url
1043 end
1045 end
1044
1046
1045 def back_url_hidden_field_tag
1047 def back_url_hidden_field_tag
1046 url = back_url
1048 url = back_url
1047 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1049 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1048 end
1050 end
1049
1051
1050 def check_all_links(form_name)
1052 def check_all_links(form_name)
1051 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1053 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1052 " | ".html_safe +
1054 " | ".html_safe +
1053 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1055 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1054 end
1056 end
1055
1057
1056 def progress_bar(pcts, options={})
1058 def progress_bar(pcts, options={})
1057 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1059 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1058 pcts = pcts.collect(&:round)
1060 pcts = pcts.collect(&:round)
1059 pcts[1] = pcts[1] - pcts[0]
1061 pcts[1] = pcts[1] - pcts[0]
1060 pcts << (100 - pcts[1] - pcts[0])
1062 pcts << (100 - pcts[1] - pcts[0])
1061 width = options[:width] || '100px;'
1063 width = options[:width] || '100px;'
1062 legend = options[:legend] || ''
1064 legend = options[:legend] || ''
1063 content_tag('table',
1065 content_tag('table',
1064 content_tag('tr',
1066 content_tag('tr',
1065 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1067 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1066 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1068 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1067 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1069 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1068 ), :class => 'progress', :style => "width: #{width};").html_safe +
1070 ), :class => 'progress', :style => "width: #{width};").html_safe +
1069 content_tag('p', legend, :class => 'pourcent').html_safe
1071 content_tag('p', legend, :class => 'pourcent').html_safe
1070 end
1072 end
1071
1073
1072 def checked_image(checked=true)
1074 def checked_image(checked=true)
1073 if checked
1075 if checked
1074 image_tag 'toggle_check.png'
1076 image_tag 'toggle_check.png'
1075 end
1077 end
1076 end
1078 end
1077
1079
1078 def context_menu(url)
1080 def context_menu(url)
1079 unless @context_menu_included
1081 unless @context_menu_included
1080 content_for :header_tags do
1082 content_for :header_tags do
1081 javascript_include_tag('context_menu') +
1083 javascript_include_tag('context_menu') +
1082 stylesheet_link_tag('context_menu')
1084 stylesheet_link_tag('context_menu')
1083 end
1085 end
1084 if l(:direction) == 'rtl'
1086 if l(:direction) == 'rtl'
1085 content_for :header_tags do
1087 content_for :header_tags do
1086 stylesheet_link_tag('context_menu_rtl')
1088 stylesheet_link_tag('context_menu_rtl')
1087 end
1089 end
1088 end
1090 end
1089 @context_menu_included = true
1091 @context_menu_included = true
1090 end
1092 end
1091 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1093 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1092 end
1094 end
1093
1095
1094 def calendar_for(field_id)
1096 def calendar_for(field_id)
1095 include_calendar_headers_tags
1097 include_calendar_headers_tags
1096 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1098 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1097 end
1099 end
1098
1100
1099 def include_calendar_headers_tags
1101 def include_calendar_headers_tags
1100 unless @calendar_headers_tags_included
1102 unless @calendar_headers_tags_included
1101 @calendar_headers_tags_included = true
1103 @calendar_headers_tags_included = true
1102 content_for :header_tags do
1104 content_for :header_tags do
1103 start_of_week = Setting.start_of_week
1105 start_of_week = Setting.start_of_week
1104 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1106 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1105 # Redmine uses 1..7 (monday..sunday) in settings and locales
1107 # Redmine uses 1..7 (monday..sunday) in settings and locales
1106 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1108 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1107 start_of_week = start_of_week.to_i % 7
1109 start_of_week = start_of_week.to_i % 7
1108
1110
1109 tags = javascript_tag(
1111 tags = javascript_tag(
1110 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1112 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1111 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1113 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1112 path_to_image('/images/calendar.png') +
1114 path_to_image('/images/calendar.png') +
1113 "', showButtonPanel: true};")
1115 "', showButtonPanel: true};")
1114 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1116 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1115 unless jquery_locale == 'en'
1117 unless jquery_locale == 'en'
1116 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1118 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1117 end
1119 end
1118 tags
1120 tags
1119 end
1121 end
1120 end
1122 end
1121 end
1123 end
1122
1124
1123 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1125 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1124 # Examples:
1126 # Examples:
1125 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1127 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1126 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1128 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1127 #
1129 #
1128 def stylesheet_link_tag(*sources)
1130 def stylesheet_link_tag(*sources)
1129 options = sources.last.is_a?(Hash) ? sources.pop : {}
1131 options = sources.last.is_a?(Hash) ? sources.pop : {}
1130 plugin = options.delete(:plugin)
1132 plugin = options.delete(:plugin)
1131 sources = sources.map do |source|
1133 sources = sources.map do |source|
1132 if plugin
1134 if plugin
1133 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1135 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1134 elsif current_theme && current_theme.stylesheets.include?(source)
1136 elsif current_theme && current_theme.stylesheets.include?(source)
1135 current_theme.stylesheet_path(source)
1137 current_theme.stylesheet_path(source)
1136 else
1138 else
1137 source
1139 source
1138 end
1140 end
1139 end
1141 end
1140 super sources, options
1142 super sources, options
1141 end
1143 end
1142
1144
1143 # Overrides Rails' image_tag with themes and plugins support.
1145 # Overrides Rails' image_tag with themes and plugins support.
1144 # Examples:
1146 # Examples:
1145 # image_tag('image.png') # => picks image.png from the current theme or defaults
1147 # image_tag('image.png') # => picks image.png from the current theme or defaults
1146 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1148 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1147 #
1149 #
1148 def image_tag(source, options={})
1150 def image_tag(source, options={})
1149 if plugin = options.delete(:plugin)
1151 if plugin = options.delete(:plugin)
1150 source = "/plugin_assets/#{plugin}/images/#{source}"
1152 source = "/plugin_assets/#{plugin}/images/#{source}"
1151 elsif current_theme && current_theme.images.include?(source)
1153 elsif current_theme && current_theme.images.include?(source)
1152 source = current_theme.image_path(source)
1154 source = current_theme.image_path(source)
1153 end
1155 end
1154 super source, options
1156 super source, options
1155 end
1157 end
1156
1158
1157 # Overrides Rails' javascript_include_tag with plugins support
1159 # Overrides Rails' javascript_include_tag with plugins support
1158 # Examples:
1160 # Examples:
1159 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1161 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1160 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1162 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1161 #
1163 #
1162 def javascript_include_tag(*sources)
1164 def javascript_include_tag(*sources)
1163 options = sources.last.is_a?(Hash) ? sources.pop : {}
1165 options = sources.last.is_a?(Hash) ? sources.pop : {}
1164 if plugin = options.delete(:plugin)
1166 if plugin = options.delete(:plugin)
1165 sources = sources.map do |source|
1167 sources = sources.map do |source|
1166 if plugin
1168 if plugin
1167 "/plugin_assets/#{plugin}/javascripts/#{source}"
1169 "/plugin_assets/#{plugin}/javascripts/#{source}"
1168 else
1170 else
1169 source
1171 source
1170 end
1172 end
1171 end
1173 end
1172 end
1174 end
1173 super sources, options
1175 super sources, options
1174 end
1176 end
1175
1177
1176 def content_for(name, content = nil, &block)
1178 def content_for(name, content = nil, &block)
1177 @has_content ||= {}
1179 @has_content ||= {}
1178 @has_content[name] = true
1180 @has_content[name] = true
1179 super(name, content, &block)
1181 super(name, content, &block)
1180 end
1182 end
1181
1183
1182 def has_content?(name)
1184 def has_content?(name)
1183 (@has_content && @has_content[name]) || false
1185 (@has_content && @has_content[name]) || false
1184 end
1186 end
1185
1187
1186 def sidebar_content?
1188 def sidebar_content?
1187 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1189 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1188 end
1190 end
1189
1191
1190 def view_layouts_base_sidebar_hook_response
1192 def view_layouts_base_sidebar_hook_response
1191 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1193 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1192 end
1194 end
1193
1195
1194 def email_delivery_enabled?
1196 def email_delivery_enabled?
1195 !!ActionMailer::Base.perform_deliveries
1197 !!ActionMailer::Base.perform_deliveries
1196 end
1198 end
1197
1199
1198 # Returns the avatar image tag for the given +user+ if avatars are enabled
1200 # Returns the avatar image tag for the given +user+ if avatars are enabled
1199 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1201 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1200 def avatar(user, options = { })
1202 def avatar(user, options = { })
1201 if Setting.gravatar_enabled?
1203 if Setting.gravatar_enabled?
1202 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1204 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1203 email = nil
1205 email = nil
1204 if user.respond_to?(:mail)
1206 if user.respond_to?(:mail)
1205 email = user.mail
1207 email = user.mail
1206 elsif user.to_s =~ %r{<(.+?)>}
1208 elsif user.to_s =~ %r{<(.+?)>}
1207 email = $1
1209 email = $1
1208 end
1210 end
1209 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1211 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1210 else
1212 else
1211 ''
1213 ''
1212 end
1214 end
1213 end
1215 end
1214
1216
1215 def sanitize_anchor_name(anchor)
1217 def sanitize_anchor_name(anchor)
1216 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1218 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1217 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1219 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1218 else
1220 else
1219 # TODO: remove when ruby1.8 is no longer supported
1221 # TODO: remove when ruby1.8 is no longer supported
1220 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1222 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1221 end
1223 end
1222 end
1224 end
1223
1225
1224 # Returns the javascript tags that are included in the html layout head
1226 # Returns the javascript tags that are included in the html layout head
1225 def javascript_heads
1227 def javascript_heads
1226 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
1228 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
1227 unless User.current.pref.warn_on_leaving_unsaved == '0'
1229 unless User.current.pref.warn_on_leaving_unsaved == '0'
1228 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1230 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1229 end
1231 end
1230 tags
1232 tags
1231 end
1233 end
1232
1234
1233 def favicon
1235 def favicon
1234 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1236 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1235 end
1237 end
1236
1238
1237 def robot_exclusion_tag
1239 def robot_exclusion_tag
1238 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1240 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1239 end
1241 end
1240
1242
1241 # Returns true if arg is expected in the API response
1243 # Returns true if arg is expected in the API response
1242 def include_in_api_response?(arg)
1244 def include_in_api_response?(arg)
1243 unless @included_in_api_response
1245 unless @included_in_api_response
1244 param = params[:include]
1246 param = params[:include]
1245 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1247 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1246 @included_in_api_response.collect!(&:strip)
1248 @included_in_api_response.collect!(&:strip)
1247 end
1249 end
1248 @included_in_api_response.include?(arg.to_s)
1250 @included_in_api_response.include?(arg.to_s)
1249 end
1251 end
1250
1252
1251 # Returns options or nil if nometa param or X-Redmine-Nometa header
1253 # Returns options or nil if nometa param or X-Redmine-Nometa header
1252 # was set in the request
1254 # was set in the request
1253 def api_meta(options)
1255 def api_meta(options)
1254 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1256 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1255 # compatibility mode for activeresource clients that raise
1257 # compatibility mode for activeresource clients that raise
1256 # an error when unserializing an array with attributes
1258 # an error when unserializing an array with attributes
1257 nil
1259 nil
1258 else
1260 else
1259 options
1261 options
1260 end
1262 end
1261 end
1263 end
1262
1264
1263 private
1265 private
1264
1266
1265 def wiki_helper
1267 def wiki_helper
1266 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1268 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1267 extend helper
1269 extend helper
1268 return self
1270 return self
1269 end
1271 end
1270
1272
1271 def link_to_content_update(text, url_params = {}, html_options = {})
1273 def link_to_content_update(text, url_params = {}, html_options = {})
1272 link_to(text, url_params, html_options)
1274 link_to(text, url_params, html_options)
1273 end
1275 end
1274 end
1276 end
@@ -1,134 +1,139
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 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 QueriesHelper
20 module QueriesHelper
21 def filters_options_for_select(query)
21 def filters_options_for_select(query)
22 options = [[]]
22 options = [[]]
23 options += query.available_filters.sort {|a,b| a[1][:order] <=> b[1][:order]}.map do |field, field_options|
23 options += query.available_filters.sort {|a,b| a[1][:order] <=> b[1][:order]}.map do |field, field_options|
24 [field_options[:name], field]
24 [field_options[:name], field]
25 end
25 end
26 options_for_select(options)
26 options_for_select(options)
27 end
27 end
28
28
29 def column_header(column)
29 def column_header(column)
30 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
30 column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption,
31 :default_order => column.default_order) :
31 :default_order => column.default_order) :
32 content_tag('th', h(column.caption))
32 content_tag('th', h(column.caption))
33 end
33 end
34
34
35 def column_content(column, issue)
35 def column_content(column, issue)
36 value = column.value(issue)
36 value = column.value(issue)
37 if value.is_a?(Array)
37 if value.is_a?(Array)
38 value.collect {|v| column_value(column, issue, v)}.compact.sort.join(', ').html_safe
38 value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe
39 else
39 else
40 column_value(column, issue, value)
40 column_value(column, issue, value)
41 end
41 end
42 end
42 end
43
43
44 def column_value(column, issue, value)
44 def column_value(column, issue, value)
45 case value.class.name
45 case value.class.name
46 when 'String'
46 when 'String'
47 if column.name == :subject
47 if column.name == :subject
48 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
48 link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
49 else
49 else
50 h(value)
50 h(value)
51 end
51 end
52 when 'Time'
52 when 'Time'
53 format_time(value)
53 format_time(value)
54 when 'Date'
54 when 'Date'
55 format_date(value)
55 format_date(value)
56 when 'Fixnum', 'Float'
56 when 'Fixnum', 'Float'
57 if column.name == :done_ratio
57 if column.name == :done_ratio
58 progress_bar(value, :width => '80px')
58 progress_bar(value, :width => '80px')
59 elsif column.name == :spent_hours
59 elsif column.name == :spent_hours
60 sprintf "%.2f", value
60 sprintf "%.2f", value
61 else
61 else
62 h(value.to_s)
62 h(value.to_s)
63 end
63 end
64 when 'User'
64 when 'User'
65 link_to_user value
65 link_to_user value
66 when 'Project'
66 when 'Project'
67 link_to_project value
67 link_to_project value
68 when 'Version'
68 when 'Version'
69 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
69 link_to(h(value), :controller => 'versions', :action => 'show', :id => value)
70 when 'TrueClass'
70 when 'TrueClass'
71 l(:general_text_Yes)
71 l(:general_text_Yes)
72 when 'FalseClass'
72 when 'FalseClass'
73 l(:general_text_No)
73 l(:general_text_No)
74 when 'Issue'
74 when 'Issue'
75 link_to_issue(value, :subject => false)
75 link_to_issue(value, :subject => false)
76 when 'IssueRelation'
77 other = value.other_issue(issue)
78 content_tag('span',
79 (l(value.label_for(issue)) + " " + link_to_issue(other, :subject => false, :tracker => false)).html_safe,
80 :class => value.css_classes_for(issue))
76 else
81 else
77 h(value)
82 h(value)
78 end
83 end
79 end
84 end
80
85
81 # Retrieve query from session or build a new query
86 # Retrieve query from session or build a new query
82 def retrieve_query
87 def retrieve_query
83 if !params[:query_id].blank?
88 if !params[:query_id].blank?
84 cond = "project_id IS NULL"
89 cond = "project_id IS NULL"
85 cond << " OR project_id = #{@project.id}" if @project
90 cond << " OR project_id = #{@project.id}" if @project
86 @query = Query.find(params[:query_id], :conditions => cond)
91 @query = Query.find(params[:query_id], :conditions => cond)
87 raise ::Unauthorized unless @query.visible?
92 raise ::Unauthorized unless @query.visible?
88 @query.project = @project
93 @query.project = @project
89 session[:query] = {:id => @query.id, :project_id => @query.project_id}
94 session[:query] = {:id => @query.id, :project_id => @query.project_id}
90 sort_clear
95 sort_clear
91 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
96 elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil)
92 # Give it a name, required to be valid
97 # Give it a name, required to be valid
93 @query = Query.new(:name => "_")
98 @query = Query.new(:name => "_")
94 @query.project = @project
99 @query.project = @project
95 build_query_from_params
100 build_query_from_params
96 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
101 session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
97 else
102 else
98 # retrieve from session
103 # retrieve from session
99 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
104 @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
100 @query ||= Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
105 @query ||= Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
101 @query.project = @project
106 @query.project = @project
102 end
107 end
103 end
108 end
104
109
105 def retrieve_query_from_session
110 def retrieve_query_from_session
106 if session[:query]
111 if session[:query]
107 if session[:query][:id]
112 if session[:query][:id]
108 @query = Query.find_by_id(session[:query][:id])
113 @query = Query.find_by_id(session[:query][:id])
109 return unless @query
114 return unless @query
110 else
115 else
111 @query = Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
116 @query = Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
112 end
117 end
113 if session[:query].has_key?(:project_id)
118 if session[:query].has_key?(:project_id)
114 @query.project_id = session[:query][:project_id]
119 @query.project_id = session[:query][:project_id]
115 else
120 else
116 @query.project = @project
121 @query.project = @project
117 end
122 end
118 @query
123 @query
119 end
124 end
120 end
125 end
121
126
122 def build_query_from_params
127 def build_query_from_params
123 if params[:fields] || params[:f]
128 if params[:fields] || params[:f]
124 @query.filters = {}
129 @query.filters = {}
125 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
130 @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v])
126 else
131 else
127 @query.available_filters.keys.each do |field|
132 @query.available_filters.keys.each do |field|
128 @query.add_short_filter(field, params[field]) if params[field]
133 @query.add_short_filter(field, params[field]) if params[field]
129 end
134 end
130 end
135 end
131 @query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
136 @query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by])
132 @query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
137 @query.column_names = params[:c] || (params[:query] && params[:query][:column_names])
133 end
138 end
134 end
139 end
@@ -1,1292 +1,1311
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61 validate :validate_issue, :validate_required_fields
61 validate :validate_issue, :validate_required_fields
62
62
63 scope :visible,
63 scope :visible,
64 lambda {|*args| { :include => :project,
64 lambda {|*args| { :include => :project,
65 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
66
66
67 scope :open, lambda {|*args|
67 scope :open, lambda {|*args|
68 is_closed = args.size > 0 ? !args.first : false
68 is_closed = args.size > 0 ? !args.first : false
69 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
70 }
70 }
71
71
72 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
73 scope :on_active_project, :include => [:status, :project, :tracker],
73 scope :on_active_project, :include => [:status, :project, :tracker],
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75
75
76 before_create :default_assign
76 before_create :default_assign
77 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
77 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 # Should be after_create but would be called before previous after_save callbacks
80 # Should be after_create but would be called before previous after_save callbacks
81 after_save :after_create_from_copy
81 after_save :after_create_from_copy
82 after_destroy :update_parent_attributes
82 after_destroy :update_parent_attributes
83
83
84 # Returns a SQL conditions string used to find all issues visible by the specified user
84 # Returns a SQL conditions string used to find all issues visible by the specified user
85 def self.visible_condition(user, options={})
85 def self.visible_condition(user, options={})
86 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
86 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
87 if user.logged?
87 if user.logged?
88 case role.issues_visibility
88 case role.issues_visibility
89 when 'all'
89 when 'all'
90 nil
90 nil
91 when 'default'
91 when 'default'
92 user_ids = [user.id] + user.groups.map(&:id)
92 user_ids = [user.id] + user.groups.map(&:id)
93 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
93 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 when 'own'
94 when 'own'
95 user_ids = [user.id] + user.groups.map(&:id)
95 user_ids = [user.id] + user.groups.map(&:id)
96 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
96 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
97 else
97 else
98 '1=0'
98 '1=0'
99 end
99 end
100 else
100 else
101 "(#{table_name}.is_private = #{connection.quoted_false})"
101 "(#{table_name}.is_private = #{connection.quoted_false})"
102 end
102 end
103 end
103 end
104 end
104 end
105
105
106 # Returns true if usr or current user is allowed to view the issue
106 # Returns true if usr or current user is allowed to view the issue
107 def visible?(usr=nil)
107 def visible?(usr=nil)
108 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
108 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
109 if user.logged?
109 if user.logged?
110 case role.issues_visibility
110 case role.issues_visibility
111 when 'all'
111 when 'all'
112 true
112 true
113 when 'default'
113 when 'default'
114 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
114 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
115 when 'own'
115 when 'own'
116 self.author == user || user.is_or_belongs_to?(assigned_to)
116 self.author == user || user.is_or_belongs_to?(assigned_to)
117 else
117 else
118 false
118 false
119 end
119 end
120 else
120 else
121 !self.is_private?
121 !self.is_private?
122 end
122 end
123 end
123 end
124 end
124 end
125
125
126 def initialize(attributes=nil, *args)
126 def initialize(attributes=nil, *args)
127 super
127 super
128 if new_record?
128 if new_record?
129 # set default values for new records only
129 # set default values for new records only
130 self.status ||= IssueStatus.default
130 self.status ||= IssueStatus.default
131 self.priority ||= IssuePriority.default
131 self.priority ||= IssuePriority.default
132 self.watcher_user_ids = []
132 self.watcher_user_ids = []
133 end
133 end
134 end
134 end
135
135
136 # AR#Persistence#destroy would raise and RecordNotFound exception
136 # AR#Persistence#destroy would raise and RecordNotFound exception
137 # if the issue was already deleted or updated (non matching lock_version).
137 # if the issue was already deleted or updated (non matching lock_version).
138 # This is a problem when bulk deleting issues or deleting a project
138 # This is a problem when bulk deleting issues or deleting a project
139 # (because an issue may already be deleted if its parent was deleted
139 # (because an issue may already be deleted if its parent was deleted
140 # first).
140 # first).
141 # The issue is reloaded by the nested_set before being deleted so
141 # The issue is reloaded by the nested_set before being deleted so
142 # the lock_version condition should not be an issue but we handle it.
142 # the lock_version condition should not be an issue but we handle it.
143 def destroy
143 def destroy
144 super
144 super
145 rescue ActiveRecord::RecordNotFound
145 rescue ActiveRecord::RecordNotFound
146 # Stale or already deleted
146 # Stale or already deleted
147 begin
147 begin
148 reload
148 reload
149 rescue ActiveRecord::RecordNotFound
149 rescue ActiveRecord::RecordNotFound
150 # The issue was actually already deleted
150 # The issue was actually already deleted
151 @destroyed = true
151 @destroyed = true
152 return freeze
152 return freeze
153 end
153 end
154 # The issue was stale, retry to destroy
154 # The issue was stale, retry to destroy
155 super
155 super
156 end
156 end
157
157
158 def reload(*args)
158 def reload(*args)
159 @workflow_rule_by_attribute = nil
159 @workflow_rule_by_attribute = nil
160 @assignable_versions = nil
160 @assignable_versions = nil
161 super
161 super
162 end
162 end
163
163
164 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
164 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
165 def available_custom_fields
165 def available_custom_fields
166 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
166 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
167 end
167 end
168
168
169 # Copies attributes from another issue, arg can be an id or an Issue
169 # Copies attributes from another issue, arg can be an id or an Issue
170 def copy_from(arg, options={})
170 def copy_from(arg, options={})
171 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
171 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
172 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
172 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
173 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
173 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
174 self.status = issue.status
174 self.status = issue.status
175 self.author = User.current
175 self.author = User.current
176 unless options[:attachments] == false
176 unless options[:attachments] == false
177 self.attachments = issue.attachments.map do |attachement|
177 self.attachments = issue.attachments.map do |attachement|
178 attachement.copy(:container => self)
178 attachement.copy(:container => self)
179 end
179 end
180 end
180 end
181 @copied_from = issue
181 @copied_from = issue
182 @copy_options = options
182 @copy_options = options
183 self
183 self
184 end
184 end
185
185
186 # Returns an unsaved copy of the issue
186 # Returns an unsaved copy of the issue
187 def copy(attributes=nil, copy_options={})
187 def copy(attributes=nil, copy_options={})
188 copy = self.class.new.copy_from(self, copy_options)
188 copy = self.class.new.copy_from(self, copy_options)
189 copy.attributes = attributes if attributes
189 copy.attributes = attributes if attributes
190 copy
190 copy
191 end
191 end
192
192
193 # Returns true if the issue is a copy
193 # Returns true if the issue is a copy
194 def copy?
194 def copy?
195 @copied_from.present?
195 @copied_from.present?
196 end
196 end
197
197
198 # Moves/copies an issue to a new project and tracker
198 # Moves/copies an issue to a new project and tracker
199 # Returns the moved/copied issue on success, false on failure
199 # Returns the moved/copied issue on success, false on failure
200 def move_to_project(new_project, new_tracker=nil, options={})
200 def move_to_project(new_project, new_tracker=nil, options={})
201 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
201 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
202
202
203 if options[:copy]
203 if options[:copy]
204 issue = self.copy
204 issue = self.copy
205 else
205 else
206 issue = self
206 issue = self
207 end
207 end
208
208
209 issue.init_journal(User.current, options[:notes])
209 issue.init_journal(User.current, options[:notes])
210
210
211 # Preserve previous behaviour
211 # Preserve previous behaviour
212 # #move_to_project doesn't change tracker automatically
212 # #move_to_project doesn't change tracker automatically
213 issue.send :project=, new_project, true
213 issue.send :project=, new_project, true
214 if new_tracker
214 if new_tracker
215 issue.tracker = new_tracker
215 issue.tracker = new_tracker
216 end
216 end
217 # Allow bulk setting of attributes on the issue
217 # Allow bulk setting of attributes on the issue
218 if options[:attributes]
218 if options[:attributes]
219 issue.attributes = options[:attributes]
219 issue.attributes = options[:attributes]
220 end
220 end
221
221
222 issue.save ? issue : false
222 issue.save ? issue : false
223 end
223 end
224
224
225 def status_id=(sid)
225 def status_id=(sid)
226 self.status = nil
226 self.status = nil
227 result = write_attribute(:status_id, sid)
227 result = write_attribute(:status_id, sid)
228 @workflow_rule_by_attribute = nil
228 @workflow_rule_by_attribute = nil
229 result
229 result
230 end
230 end
231
231
232 def priority_id=(pid)
232 def priority_id=(pid)
233 self.priority = nil
233 self.priority = nil
234 write_attribute(:priority_id, pid)
234 write_attribute(:priority_id, pid)
235 end
235 end
236
236
237 def category_id=(cid)
237 def category_id=(cid)
238 self.category = nil
238 self.category = nil
239 write_attribute(:category_id, cid)
239 write_attribute(:category_id, cid)
240 end
240 end
241
241
242 def fixed_version_id=(vid)
242 def fixed_version_id=(vid)
243 self.fixed_version = nil
243 self.fixed_version = nil
244 write_attribute(:fixed_version_id, vid)
244 write_attribute(:fixed_version_id, vid)
245 end
245 end
246
246
247 def tracker_id=(tid)
247 def tracker_id=(tid)
248 self.tracker = nil
248 self.tracker = nil
249 result = write_attribute(:tracker_id, tid)
249 result = write_attribute(:tracker_id, tid)
250 @custom_field_values = nil
250 @custom_field_values = nil
251 @workflow_rule_by_attribute = nil
251 @workflow_rule_by_attribute = nil
252 result
252 result
253 end
253 end
254
254
255 def project_id=(project_id)
255 def project_id=(project_id)
256 if project_id.to_s != self.project_id.to_s
256 if project_id.to_s != self.project_id.to_s
257 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
257 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
258 end
258 end
259 end
259 end
260
260
261 def project=(project, keep_tracker=false)
261 def project=(project, keep_tracker=false)
262 project_was = self.project
262 project_was = self.project
263 write_attribute(:project_id, project ? project.id : nil)
263 write_attribute(:project_id, project ? project.id : nil)
264 association_instance_set('project', project)
264 association_instance_set('project', project)
265 if project_was && project && project_was != project
265 if project_was && project && project_was != project
266 @assignable_versions = nil
266 @assignable_versions = nil
267
267
268 unless keep_tracker || project.trackers.include?(tracker)
268 unless keep_tracker || project.trackers.include?(tracker)
269 self.tracker = project.trackers.first
269 self.tracker = project.trackers.first
270 end
270 end
271 # Reassign to the category with same name if any
271 # Reassign to the category with same name if any
272 if category
272 if category
273 self.category = project.issue_categories.find_by_name(category.name)
273 self.category = project.issue_categories.find_by_name(category.name)
274 end
274 end
275 # Keep the fixed_version if it's still valid in the new_project
275 # Keep the fixed_version if it's still valid in the new_project
276 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
276 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
277 self.fixed_version = nil
277 self.fixed_version = nil
278 end
278 end
279 if parent && parent.project_id != project_id
279 if parent && parent.project_id != project_id
280 self.parent_issue_id = nil
280 self.parent_issue_id = nil
281 end
281 end
282 @custom_field_values = nil
282 @custom_field_values = nil
283 end
283 end
284 end
284 end
285
285
286 def description=(arg)
286 def description=(arg)
287 if arg.is_a?(String)
287 if arg.is_a?(String)
288 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
288 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
289 end
289 end
290 write_attribute(:description, arg)
290 write_attribute(:description, arg)
291 end
291 end
292
292
293 # Overrides assign_attributes so that project and tracker get assigned first
293 # Overrides assign_attributes so that project and tracker get assigned first
294 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
294 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
295 return if new_attributes.nil?
295 return if new_attributes.nil?
296 attrs = new_attributes.dup
296 attrs = new_attributes.dup
297 attrs.stringify_keys!
297 attrs.stringify_keys!
298
298
299 %w(project project_id tracker tracker_id).each do |attr|
299 %w(project project_id tracker tracker_id).each do |attr|
300 if attrs.has_key?(attr)
300 if attrs.has_key?(attr)
301 send "#{attr}=", attrs.delete(attr)
301 send "#{attr}=", attrs.delete(attr)
302 end
302 end
303 end
303 end
304 send :assign_attributes_without_project_and_tracker_first, attrs, *args
304 send :assign_attributes_without_project_and_tracker_first, attrs, *args
305 end
305 end
306 # Do not redefine alias chain on reload (see #4838)
306 # Do not redefine alias chain on reload (see #4838)
307 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
307 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
308
308
309 def estimated_hours=(h)
309 def estimated_hours=(h)
310 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
310 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
311 end
311 end
312
312
313 safe_attributes 'project_id',
313 safe_attributes 'project_id',
314 :if => lambda {|issue, user|
314 :if => lambda {|issue, user|
315 if issue.new_record?
315 if issue.new_record?
316 issue.copy?
316 issue.copy?
317 elsif user.allowed_to?(:move_issues, issue.project)
317 elsif user.allowed_to?(:move_issues, issue.project)
318 projects = Issue.allowed_target_projects_on_move(user)
318 projects = Issue.allowed_target_projects_on_move(user)
319 projects.include?(issue.project) && projects.size > 1
319 projects.include?(issue.project) && projects.size > 1
320 end
320 end
321 }
321 }
322
322
323 safe_attributes 'tracker_id',
323 safe_attributes 'tracker_id',
324 'status_id',
324 'status_id',
325 'category_id',
325 'category_id',
326 'assigned_to_id',
326 'assigned_to_id',
327 'priority_id',
327 'priority_id',
328 'fixed_version_id',
328 'fixed_version_id',
329 'subject',
329 'subject',
330 'description',
330 'description',
331 'start_date',
331 'start_date',
332 'due_date',
332 'due_date',
333 'done_ratio',
333 'done_ratio',
334 'estimated_hours',
334 'estimated_hours',
335 'custom_field_values',
335 'custom_field_values',
336 'custom_fields',
336 'custom_fields',
337 'lock_version',
337 'lock_version',
338 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
338 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
339
339
340 safe_attributes 'status_id',
340 safe_attributes 'status_id',
341 'assigned_to_id',
341 'assigned_to_id',
342 'fixed_version_id',
342 'fixed_version_id',
343 'done_ratio',
343 'done_ratio',
344 'lock_version',
344 'lock_version',
345 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
345 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
346
346
347 safe_attributes 'watcher_user_ids',
347 safe_attributes 'watcher_user_ids',
348 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
348 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
349
349
350 safe_attributes 'is_private',
350 safe_attributes 'is_private',
351 :if => lambda {|issue, user|
351 :if => lambda {|issue, user|
352 user.allowed_to?(:set_issues_private, issue.project) ||
352 user.allowed_to?(:set_issues_private, issue.project) ||
353 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
353 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
354 }
354 }
355
355
356 safe_attributes 'parent_issue_id',
356 safe_attributes 'parent_issue_id',
357 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
357 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
358 user.allowed_to?(:manage_subtasks, issue.project)}
358 user.allowed_to?(:manage_subtasks, issue.project)}
359
359
360 def safe_attribute_names(user=nil)
360 def safe_attribute_names(user=nil)
361 names = super
361 names = super
362 names -= disabled_core_fields
362 names -= disabled_core_fields
363 names -= read_only_attribute_names(user)
363 names -= read_only_attribute_names(user)
364 names
364 names
365 end
365 end
366
366
367 # Safely sets attributes
367 # Safely sets attributes
368 # Should be called from controllers instead of #attributes=
368 # Should be called from controllers instead of #attributes=
369 # attr_accessible is too rough because we still want things like
369 # attr_accessible is too rough because we still want things like
370 # Issue.new(:project => foo) to work
370 # Issue.new(:project => foo) to work
371 def safe_attributes=(attrs, user=User.current)
371 def safe_attributes=(attrs, user=User.current)
372 return unless attrs.is_a?(Hash)
372 return unless attrs.is_a?(Hash)
373
373
374 attrs = attrs.dup
374 attrs = attrs.dup
375
375
376 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
376 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
377 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
377 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
378 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
378 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
379 self.project_id = p
379 self.project_id = p
380 end
380 end
381 end
381 end
382
382
383 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
383 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
384 self.tracker_id = t
384 self.tracker_id = t
385 end
385 end
386
386
387 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
387 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
388 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
388 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
389 self.status_id = s
389 self.status_id = s
390 end
390 end
391 end
391 end
392
392
393 attrs = delete_unsafe_attributes(attrs, user)
393 attrs = delete_unsafe_attributes(attrs, user)
394 return if attrs.empty?
394 return if attrs.empty?
395
395
396 unless leaf?
396 unless leaf?
397 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
397 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
398 end
398 end
399
399
400 if attrs['parent_issue_id'].present?
400 if attrs['parent_issue_id'].present?
401 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
401 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
402 end
402 end
403
403
404 if attrs['custom_field_values'].present?
404 if attrs['custom_field_values'].present?
405 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
405 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
406 end
406 end
407
407
408 if attrs['custom_fields'].present?
408 if attrs['custom_fields'].present?
409 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
409 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
410 end
410 end
411
411
412 # mass-assignment security bypass
412 # mass-assignment security bypass
413 assign_attributes attrs, :without_protection => true
413 assign_attributes attrs, :without_protection => true
414 end
414 end
415
415
416 def disabled_core_fields
416 def disabled_core_fields
417 tracker ? tracker.disabled_core_fields : []
417 tracker ? tracker.disabled_core_fields : []
418 end
418 end
419
419
420 # Returns the custom_field_values that can be edited by the given user
420 # Returns the custom_field_values that can be edited by the given user
421 def editable_custom_field_values(user=nil)
421 def editable_custom_field_values(user=nil)
422 custom_field_values.reject do |value|
422 custom_field_values.reject do |value|
423 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
423 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
424 end
424 end
425 end
425 end
426
426
427 # Returns the names of attributes that are read-only for user or the current user
427 # Returns the names of attributes that are read-only for user or the current user
428 # For users with multiple roles, the read-only fields are the intersection of
428 # For users with multiple roles, the read-only fields are the intersection of
429 # read-only fields of each role
429 # read-only fields of each role
430 # The result is an array of strings where sustom fields are represented with their ids
430 # The result is an array of strings where sustom fields are represented with their ids
431 #
431 #
432 # Examples:
432 # Examples:
433 # issue.read_only_attribute_names # => ['due_date', '2']
433 # issue.read_only_attribute_names # => ['due_date', '2']
434 # issue.read_only_attribute_names(user) # => []
434 # issue.read_only_attribute_names(user) # => []
435 def read_only_attribute_names(user=nil)
435 def read_only_attribute_names(user=nil)
436 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
436 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
437 end
437 end
438
438
439 # Returns the names of required attributes for user or the current user
439 # Returns the names of required attributes for user or the current user
440 # For users with multiple roles, the required fields are the intersection of
440 # For users with multiple roles, the required fields are the intersection of
441 # required fields of each role
441 # required fields of each role
442 # The result is an array of strings where sustom fields are represented with their ids
442 # The result is an array of strings where sustom fields are represented with their ids
443 #
443 #
444 # Examples:
444 # Examples:
445 # issue.required_attribute_names # => ['due_date', '2']
445 # issue.required_attribute_names # => ['due_date', '2']
446 # issue.required_attribute_names(user) # => []
446 # issue.required_attribute_names(user) # => []
447 def required_attribute_names(user=nil)
447 def required_attribute_names(user=nil)
448 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
448 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
449 end
449 end
450
450
451 # Returns true if the attribute is required for user
451 # Returns true if the attribute is required for user
452 def required_attribute?(name, user=nil)
452 def required_attribute?(name, user=nil)
453 required_attribute_names(user).include?(name.to_s)
453 required_attribute_names(user).include?(name.to_s)
454 end
454 end
455
455
456 # Returns a hash of the workflow rule by attribute for the given user
456 # Returns a hash of the workflow rule by attribute for the given user
457 #
457 #
458 # Examples:
458 # Examples:
459 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
459 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
460 def workflow_rule_by_attribute(user=nil)
460 def workflow_rule_by_attribute(user=nil)
461 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
461 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
462
462
463 user_real = user || User.current
463 user_real = user || User.current
464 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
464 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
465 return {} if roles.empty?
465 return {} if roles.empty?
466
466
467 result = {}
467 result = {}
468 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
468 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
469 if workflow_permissions.any?
469 if workflow_permissions.any?
470 workflow_rules = workflow_permissions.inject({}) do |h, wp|
470 workflow_rules = workflow_permissions.inject({}) do |h, wp|
471 h[wp.field_name] ||= []
471 h[wp.field_name] ||= []
472 h[wp.field_name] << wp.rule
472 h[wp.field_name] << wp.rule
473 h
473 h
474 end
474 end
475 workflow_rules.each do |attr, rules|
475 workflow_rules.each do |attr, rules|
476 next if rules.size < roles.size
476 next if rules.size < roles.size
477 uniq_rules = rules.uniq
477 uniq_rules = rules.uniq
478 if uniq_rules.size == 1
478 if uniq_rules.size == 1
479 result[attr] = uniq_rules.first
479 result[attr] = uniq_rules.first
480 else
480 else
481 result[attr] = 'required'
481 result[attr] = 'required'
482 end
482 end
483 end
483 end
484 end
484 end
485 @workflow_rule_by_attribute = result if user.nil?
485 @workflow_rule_by_attribute = result if user.nil?
486 result
486 result
487 end
487 end
488 private :workflow_rule_by_attribute
488 private :workflow_rule_by_attribute
489
489
490 def done_ratio
490 def done_ratio
491 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
491 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
492 status.default_done_ratio
492 status.default_done_ratio
493 else
493 else
494 read_attribute(:done_ratio)
494 read_attribute(:done_ratio)
495 end
495 end
496 end
496 end
497
497
498 def self.use_status_for_done_ratio?
498 def self.use_status_for_done_ratio?
499 Setting.issue_done_ratio == 'issue_status'
499 Setting.issue_done_ratio == 'issue_status'
500 end
500 end
501
501
502 def self.use_field_for_done_ratio?
502 def self.use_field_for_done_ratio?
503 Setting.issue_done_ratio == 'issue_field'
503 Setting.issue_done_ratio == 'issue_field'
504 end
504 end
505
505
506 def validate_issue
506 def validate_issue
507 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
507 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
508 errors.add :due_date, :not_a_date
508 errors.add :due_date, :not_a_date
509 end
509 end
510
510
511 if self.due_date and self.start_date and self.due_date < self.start_date
511 if self.due_date and self.start_date and self.due_date < self.start_date
512 errors.add :due_date, :greater_than_start_date
512 errors.add :due_date, :greater_than_start_date
513 end
513 end
514
514
515 if start_date && soonest_start && start_date < soonest_start
515 if start_date && soonest_start && start_date < soonest_start
516 errors.add :start_date, :invalid
516 errors.add :start_date, :invalid
517 end
517 end
518
518
519 if fixed_version
519 if fixed_version
520 if !assignable_versions.include?(fixed_version)
520 if !assignable_versions.include?(fixed_version)
521 errors.add :fixed_version_id, :inclusion
521 errors.add :fixed_version_id, :inclusion
522 elsif reopened? && fixed_version.closed?
522 elsif reopened? && fixed_version.closed?
523 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
523 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
524 end
524 end
525 end
525 end
526
526
527 # Checks that the issue can not be added/moved to a disabled tracker
527 # Checks that the issue can not be added/moved to a disabled tracker
528 if project && (tracker_id_changed? || project_id_changed?)
528 if project && (tracker_id_changed? || project_id_changed?)
529 unless project.trackers.include?(tracker)
529 unless project.trackers.include?(tracker)
530 errors.add :tracker_id, :inclusion
530 errors.add :tracker_id, :inclusion
531 end
531 end
532 end
532 end
533
533
534 # Checks parent issue assignment
534 # Checks parent issue assignment
535 if @parent_issue
535 if @parent_issue
536 if @parent_issue.project_id != project_id
536 if @parent_issue.project_id != project_id
537 errors.add :parent_issue_id, :not_same_project
537 errors.add :parent_issue_id, :not_same_project
538 elsif !new_record?
538 elsif !new_record?
539 # moving an existing issue
539 # moving an existing issue
540 if @parent_issue.root_id != root_id
540 if @parent_issue.root_id != root_id
541 # we can always move to another tree
541 # we can always move to another tree
542 elsif move_possible?(@parent_issue)
542 elsif move_possible?(@parent_issue)
543 # move accepted inside tree
543 # move accepted inside tree
544 else
544 else
545 errors.add :parent_issue_id, :not_a_valid_parent
545 errors.add :parent_issue_id, :not_a_valid_parent
546 end
546 end
547 end
547 end
548 end
548 end
549 end
549 end
550
550
551 # Validates the issue against additional workflow requirements
551 # Validates the issue against additional workflow requirements
552 def validate_required_fields
552 def validate_required_fields
553 user = new_record? ? author : current_journal.try(:user)
553 user = new_record? ? author : current_journal.try(:user)
554
554
555 required_attribute_names(user).each do |attribute|
555 required_attribute_names(user).each do |attribute|
556 if attribute =~ /^\d+$/
556 if attribute =~ /^\d+$/
557 attribute = attribute.to_i
557 attribute = attribute.to_i
558 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
558 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
559 if v && v.value.blank?
559 if v && v.value.blank?
560 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
560 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
561 end
561 end
562 else
562 else
563 if respond_to?(attribute) && send(attribute).blank?
563 if respond_to?(attribute) && send(attribute).blank?
564 errors.add attribute, :blank
564 errors.add attribute, :blank
565 end
565 end
566 end
566 end
567 end
567 end
568 end
568 end
569
569
570 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
570 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
571 # even if the user turns off the setting later
571 # even if the user turns off the setting later
572 def update_done_ratio_from_issue_status
572 def update_done_ratio_from_issue_status
573 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
573 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
574 self.done_ratio = status.default_done_ratio
574 self.done_ratio = status.default_done_ratio
575 end
575 end
576 end
576 end
577
577
578 def init_journal(user, notes = "")
578 def init_journal(user, notes = "")
579 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
579 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
580 if new_record?
580 if new_record?
581 @current_journal.notify = false
581 @current_journal.notify = false
582 else
582 else
583 @attributes_before_change = attributes.dup
583 @attributes_before_change = attributes.dup
584 @custom_values_before_change = {}
584 @custom_values_before_change = {}
585 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
585 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
586 end
586 end
587 @current_journal
587 @current_journal
588 end
588 end
589
589
590 # Returns the id of the last journal or nil
590 # Returns the id of the last journal or nil
591 def last_journal_id
591 def last_journal_id
592 if new_record?
592 if new_record?
593 nil
593 nil
594 else
594 else
595 journals.maximum(:id)
595 journals.maximum(:id)
596 end
596 end
597 end
597 end
598
598
599 # Returns a scope for journals that have an id greater than journal_id
599 # Returns a scope for journals that have an id greater than journal_id
600 def journals_after(journal_id)
600 def journals_after(journal_id)
601 scope = journals.reorder("#{Journal.table_name}.id ASC")
601 scope = journals.reorder("#{Journal.table_name}.id ASC")
602 if journal_id.present?
602 if journal_id.present?
603 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
603 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
604 end
604 end
605 scope
605 scope
606 end
606 end
607
607
608 # Return true if the issue is closed, otherwise false
608 # Return true if the issue is closed, otherwise false
609 def closed?
609 def closed?
610 self.status.is_closed?
610 self.status.is_closed?
611 end
611 end
612
612
613 # Return true if the issue is being reopened
613 # Return true if the issue is being reopened
614 def reopened?
614 def reopened?
615 if !new_record? && status_id_changed?
615 if !new_record? && status_id_changed?
616 status_was = IssueStatus.find_by_id(status_id_was)
616 status_was = IssueStatus.find_by_id(status_id_was)
617 status_new = IssueStatus.find_by_id(status_id)
617 status_new = IssueStatus.find_by_id(status_id)
618 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
618 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
619 return true
619 return true
620 end
620 end
621 end
621 end
622 false
622 false
623 end
623 end
624
624
625 # Return true if the issue is being closed
625 # Return true if the issue is being closed
626 def closing?
626 def closing?
627 if !new_record? && status_id_changed?
627 if !new_record? && status_id_changed?
628 status_was = IssueStatus.find_by_id(status_id_was)
628 status_was = IssueStatus.find_by_id(status_id_was)
629 status_new = IssueStatus.find_by_id(status_id)
629 status_new = IssueStatus.find_by_id(status_id)
630 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
630 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
631 return true
631 return true
632 end
632 end
633 end
633 end
634 false
634 false
635 end
635 end
636
636
637 # Returns true if the issue is overdue
637 # Returns true if the issue is overdue
638 def overdue?
638 def overdue?
639 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
639 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
640 end
640 end
641
641
642 # Is the amount of work done less than it should for the due date
642 # Is the amount of work done less than it should for the due date
643 def behind_schedule?
643 def behind_schedule?
644 return false if start_date.nil? || due_date.nil?
644 return false if start_date.nil? || due_date.nil?
645 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
645 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
646 return done_date <= Date.today
646 return done_date <= Date.today
647 end
647 end
648
648
649 # Does this issue have children?
649 # Does this issue have children?
650 def children?
650 def children?
651 !leaf?
651 !leaf?
652 end
652 end
653
653
654 # Users the issue can be assigned to
654 # Users the issue can be assigned to
655 def assignable_users
655 def assignable_users
656 users = project.assignable_users
656 users = project.assignable_users
657 users << author if author
657 users << author if author
658 users << assigned_to if assigned_to
658 users << assigned_to if assigned_to
659 users.uniq.sort
659 users.uniq.sort
660 end
660 end
661
661
662 # Versions that the issue can be assigned to
662 # Versions that the issue can be assigned to
663 def assignable_versions
663 def assignable_versions
664 return @assignable_versions if @assignable_versions
664 return @assignable_versions if @assignable_versions
665
665
666 versions = project.shared_versions.open.all
666 versions = project.shared_versions.open.all
667 if fixed_version
667 if fixed_version
668 if fixed_version_id_changed?
668 if fixed_version_id_changed?
669 # nothing to do
669 # nothing to do
670 elsif project_id_changed?
670 elsif project_id_changed?
671 if project.shared_versions.include?(fixed_version)
671 if project.shared_versions.include?(fixed_version)
672 versions << fixed_version
672 versions << fixed_version
673 end
673 end
674 else
674 else
675 versions << fixed_version
675 versions << fixed_version
676 end
676 end
677 end
677 end
678 @assignable_versions = versions.uniq.sort
678 @assignable_versions = versions.uniq.sort
679 end
679 end
680
680
681 # Returns true if this issue is blocked by another issue that is still open
681 # Returns true if this issue is blocked by another issue that is still open
682 def blocked?
682 def blocked?
683 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
683 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
684 end
684 end
685
685
686 # Returns an array of statuses that user is able to apply
686 # Returns an array of statuses that user is able to apply
687 def new_statuses_allowed_to(user=User.current, include_default=false)
687 def new_statuses_allowed_to(user=User.current, include_default=false)
688 if new_record? && @copied_from
688 if new_record? && @copied_from
689 [IssueStatus.default, @copied_from.status].compact.uniq.sort
689 [IssueStatus.default, @copied_from.status].compact.uniq.sort
690 else
690 else
691 initial_status = nil
691 initial_status = nil
692 if new_record?
692 if new_record?
693 initial_status = IssueStatus.default
693 initial_status = IssueStatus.default
694 elsif status_id_was
694 elsif status_id_was
695 initial_status = IssueStatus.find_by_id(status_id_was)
695 initial_status = IssueStatus.find_by_id(status_id_was)
696 end
696 end
697 initial_status ||= status
697 initial_status ||= status
698
698
699 statuses = initial_status.find_new_statuses_allowed_to(
699 statuses = initial_status.find_new_statuses_allowed_to(
700 user.admin ? Role.all : user.roles_for_project(project),
700 user.admin ? Role.all : user.roles_for_project(project),
701 tracker,
701 tracker,
702 author == user,
702 author == user,
703 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
703 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
704 )
704 )
705 statuses << initial_status unless statuses.empty?
705 statuses << initial_status unless statuses.empty?
706 statuses << IssueStatus.default if include_default
706 statuses << IssueStatus.default if include_default
707 statuses = statuses.compact.uniq.sort
707 statuses = statuses.compact.uniq.sort
708 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
708 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
709 end
709 end
710 end
710 end
711
711
712 def assigned_to_was
712 def assigned_to_was
713 if assigned_to_id_changed? && assigned_to_id_was.present?
713 if assigned_to_id_changed? && assigned_to_id_was.present?
714 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
714 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
715 end
715 end
716 end
716 end
717
717
718 # Returns the mail adresses of users that should be notified
718 # Returns the mail adresses of users that should be notified
719 def recipients
719 def recipients
720 notified = []
720 notified = []
721 # Author and assignee are always notified unless they have been
721 # Author and assignee are always notified unless they have been
722 # locked or don't want to be notified
722 # locked or don't want to be notified
723 notified << author if author
723 notified << author if author
724 if assigned_to
724 if assigned_to
725 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
725 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
726 end
726 end
727 if assigned_to_was
727 if assigned_to_was
728 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
728 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
729 end
729 end
730 notified = notified.select {|u| u.active? && u.notify_about?(self)}
730 notified = notified.select {|u| u.active? && u.notify_about?(self)}
731
731
732 notified += project.notified_users
732 notified += project.notified_users
733 notified.uniq!
733 notified.uniq!
734 # Remove users that can not view the issue
734 # Remove users that can not view the issue
735 notified.reject! {|user| !visible?(user)}
735 notified.reject! {|user| !visible?(user)}
736 notified.collect(&:mail)
736 notified.collect(&:mail)
737 end
737 end
738
738
739 # Returns the number of hours spent on this issue
739 # Returns the number of hours spent on this issue
740 def spent_hours
740 def spent_hours
741 @spent_hours ||= time_entries.sum(:hours) || 0
741 @spent_hours ||= time_entries.sum(:hours) || 0
742 end
742 end
743
743
744 # Returns the total number of hours spent on this issue and its descendants
744 # Returns the total number of hours spent on this issue and its descendants
745 #
745 #
746 # Example:
746 # Example:
747 # spent_hours => 0.0
747 # spent_hours => 0.0
748 # spent_hours => 50.2
748 # spent_hours => 50.2
749 def total_spent_hours
749 def total_spent_hours
750 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
750 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
751 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
751 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
752 end
752 end
753
753
754 def relations
754 def relations
755 @relations ||= (relations_from + relations_to).sort
755 @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort)
756 end
756 end
757
757
758 # Preloads relations for a collection of issues
758 # Preloads relations for a collection of issues
759 def self.load_relations(issues)
759 def self.load_relations(issues)
760 if issues.any?
760 if issues.any?
761 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
761 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
762 issues.each do |issue|
762 issues.each do |issue|
763 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
763 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
764 end
764 end
765 end
765 end
766 end
766 end
767
767
768 # Preloads visible spent time for a collection of issues
768 # Preloads visible spent time for a collection of issues
769 def self.load_visible_spent_hours(issues, user=User.current)
769 def self.load_visible_spent_hours(issues, user=User.current)
770 if issues.any?
770 if issues.any?
771 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
771 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
772 issues.each do |issue|
772 issues.each do |issue|
773 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
773 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
774 end
774 end
775 end
775 end
776 end
776 end
777
777
778 # Preloads visible relations for a collection of issues
779 def self.load_visible_relations(issues, user=User.current)
780 if issues.any?
781 issue_ids = issues.map(&:id)
782 # Relations with issue_from in given issues and visible issue_to
783 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
784 # Relations with issue_to in given issues and visible issue_from
785 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
786
787 issues.each do |issue|
788 relations =
789 relations_from.select {|relation| relation.issue_from_id == issue.id} +
790 relations_to.select {|relation| relation.issue_to_id == issue.id}
791
792 issue.instance_variable_set "@relations", IssueRelations.new(issue, relations.sort)
793 end
794 end
795 end
796
778 # Finds an issue relation given its id.
797 # Finds an issue relation given its id.
779 def find_relation(relation_id)
798 def find_relation(relation_id)
780 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
799 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
781 end
800 end
782
801
783 def all_dependent_issues(except=[])
802 def all_dependent_issues(except=[])
784 except << self
803 except << self
785 dependencies = []
804 dependencies = []
786 relations_from.each do |relation|
805 relations_from.each do |relation|
787 if relation.issue_to && !except.include?(relation.issue_to)
806 if relation.issue_to && !except.include?(relation.issue_to)
788 dependencies << relation.issue_to
807 dependencies << relation.issue_to
789 dependencies += relation.issue_to.all_dependent_issues(except)
808 dependencies += relation.issue_to.all_dependent_issues(except)
790 end
809 end
791 end
810 end
792 dependencies
811 dependencies
793 end
812 end
794
813
795 # Returns an array of issues that duplicate this one
814 # Returns an array of issues that duplicate this one
796 def duplicates
815 def duplicates
797 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
816 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
798 end
817 end
799
818
800 # Returns the due date or the target due date if any
819 # Returns the due date or the target due date if any
801 # Used on gantt chart
820 # Used on gantt chart
802 def due_before
821 def due_before
803 due_date || (fixed_version ? fixed_version.effective_date : nil)
822 due_date || (fixed_version ? fixed_version.effective_date : nil)
804 end
823 end
805
824
806 # Returns the time scheduled for this issue.
825 # Returns the time scheduled for this issue.
807 #
826 #
808 # Example:
827 # Example:
809 # Start Date: 2/26/09, End Date: 3/04/09
828 # Start Date: 2/26/09, End Date: 3/04/09
810 # duration => 6
829 # duration => 6
811 def duration
830 def duration
812 (start_date && due_date) ? due_date - start_date : 0
831 (start_date && due_date) ? due_date - start_date : 0
813 end
832 end
814
833
815 def soonest_start
834 def soonest_start
816 @soonest_start ||= (
835 @soonest_start ||= (
817 relations_to.collect{|relation| relation.successor_soonest_start} +
836 relations_to.collect{|relation| relation.successor_soonest_start} +
818 ancestors.collect(&:soonest_start)
837 ancestors.collect(&:soonest_start)
819 ).compact.max
838 ).compact.max
820 end
839 end
821
840
822 def reschedule_after(date)
841 def reschedule_after(date)
823 return if date.nil?
842 return if date.nil?
824 if leaf?
843 if leaf?
825 if start_date.nil? || start_date < date
844 if start_date.nil? || start_date < date
826 self.start_date, self.due_date = date, date + duration
845 self.start_date, self.due_date = date, date + duration
827 begin
846 begin
828 save
847 save
829 rescue ActiveRecord::StaleObjectError
848 rescue ActiveRecord::StaleObjectError
830 reload
849 reload
831 self.start_date, self.due_date = date, date + duration
850 self.start_date, self.due_date = date, date + duration
832 save
851 save
833 end
852 end
834 end
853 end
835 else
854 else
836 leaves.each do |leaf|
855 leaves.each do |leaf|
837 leaf.reschedule_after(date)
856 leaf.reschedule_after(date)
838 end
857 end
839 end
858 end
840 end
859 end
841
860
842 def <=>(issue)
861 def <=>(issue)
843 if issue.nil?
862 if issue.nil?
844 -1
863 -1
845 elsif root_id != issue.root_id
864 elsif root_id != issue.root_id
846 (root_id || 0) <=> (issue.root_id || 0)
865 (root_id || 0) <=> (issue.root_id || 0)
847 else
866 else
848 (lft || 0) <=> (issue.lft || 0)
867 (lft || 0) <=> (issue.lft || 0)
849 end
868 end
850 end
869 end
851
870
852 def to_s
871 def to_s
853 "#{tracker} ##{id}: #{subject}"
872 "#{tracker} ##{id}: #{subject}"
854 end
873 end
855
874
856 # Returns a string of css classes that apply to the issue
875 # Returns a string of css classes that apply to the issue
857 def css_classes
876 def css_classes
858 s = "issue status-#{status_id} priority-#{priority_id}"
877 s = "issue status-#{status_id} priority-#{priority_id}"
859 s << ' closed' if closed?
878 s << ' closed' if closed?
860 s << ' overdue' if overdue?
879 s << ' overdue' if overdue?
861 s << ' child' if child?
880 s << ' child' if child?
862 s << ' parent' unless leaf?
881 s << ' parent' unless leaf?
863 s << ' private' if is_private?
882 s << ' private' if is_private?
864 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
883 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
865 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
884 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
866 s
885 s
867 end
886 end
868
887
869 # Saves an issue and a time_entry from the parameters
888 # Saves an issue and a time_entry from the parameters
870 def save_issue_with_child_records(params, existing_time_entry=nil)
889 def save_issue_with_child_records(params, existing_time_entry=nil)
871 Issue.transaction do
890 Issue.transaction do
872 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
891 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
873 @time_entry = existing_time_entry || TimeEntry.new
892 @time_entry = existing_time_entry || TimeEntry.new
874 @time_entry.project = project
893 @time_entry.project = project
875 @time_entry.issue = self
894 @time_entry.issue = self
876 @time_entry.user = User.current
895 @time_entry.user = User.current
877 @time_entry.spent_on = User.current.today
896 @time_entry.spent_on = User.current.today
878 @time_entry.attributes = params[:time_entry]
897 @time_entry.attributes = params[:time_entry]
879 self.time_entries << @time_entry
898 self.time_entries << @time_entry
880 end
899 end
881
900
882 # TODO: Rename hook
901 # TODO: Rename hook
883 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
902 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
884 if save
903 if save
885 # TODO: Rename hook
904 # TODO: Rename hook
886 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
905 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
887 else
906 else
888 raise ActiveRecord::Rollback
907 raise ActiveRecord::Rollback
889 end
908 end
890 end
909 end
891 end
910 end
892
911
893 # Unassigns issues from +version+ if it's no longer shared with issue's project
912 # Unassigns issues from +version+ if it's no longer shared with issue's project
894 def self.update_versions_from_sharing_change(version)
913 def self.update_versions_from_sharing_change(version)
895 # Update issues assigned to the version
914 # Update issues assigned to the version
896 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
915 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
897 end
916 end
898
917
899 # Unassigns issues from versions that are no longer shared
918 # Unassigns issues from versions that are no longer shared
900 # after +project+ was moved
919 # after +project+ was moved
901 def self.update_versions_from_hierarchy_change(project)
920 def self.update_versions_from_hierarchy_change(project)
902 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
921 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
903 # Update issues of the moved projects and issues assigned to a version of a moved project
922 # Update issues of the moved projects and issues assigned to a version of a moved project
904 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
923 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
905 end
924 end
906
925
907 def parent_issue_id=(arg)
926 def parent_issue_id=(arg)
908 parent_issue_id = arg.blank? ? nil : arg.to_i
927 parent_issue_id = arg.blank? ? nil : arg.to_i
909 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
928 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
910 @parent_issue.id
929 @parent_issue.id
911 else
930 else
912 @parent_issue = nil
931 @parent_issue = nil
913 nil
932 nil
914 end
933 end
915 end
934 end
916
935
917 def parent_issue_id
936 def parent_issue_id
918 if instance_variable_defined? :@parent_issue
937 if instance_variable_defined? :@parent_issue
919 @parent_issue.nil? ? nil : @parent_issue.id
938 @parent_issue.nil? ? nil : @parent_issue.id
920 else
939 else
921 parent_id
940 parent_id
922 end
941 end
923 end
942 end
924
943
925 # Extracted from the ReportsController.
944 # Extracted from the ReportsController.
926 def self.by_tracker(project)
945 def self.by_tracker(project)
927 count_and_group_by(:project => project,
946 count_and_group_by(:project => project,
928 :field => 'tracker_id',
947 :field => 'tracker_id',
929 :joins => Tracker.table_name)
948 :joins => Tracker.table_name)
930 end
949 end
931
950
932 def self.by_version(project)
951 def self.by_version(project)
933 count_and_group_by(:project => project,
952 count_and_group_by(:project => project,
934 :field => 'fixed_version_id',
953 :field => 'fixed_version_id',
935 :joins => Version.table_name)
954 :joins => Version.table_name)
936 end
955 end
937
956
938 def self.by_priority(project)
957 def self.by_priority(project)
939 count_and_group_by(:project => project,
958 count_and_group_by(:project => project,
940 :field => 'priority_id',
959 :field => 'priority_id',
941 :joins => IssuePriority.table_name)
960 :joins => IssuePriority.table_name)
942 end
961 end
943
962
944 def self.by_category(project)
963 def self.by_category(project)
945 count_and_group_by(:project => project,
964 count_and_group_by(:project => project,
946 :field => 'category_id',
965 :field => 'category_id',
947 :joins => IssueCategory.table_name)
966 :joins => IssueCategory.table_name)
948 end
967 end
949
968
950 def self.by_assigned_to(project)
969 def self.by_assigned_to(project)
951 count_and_group_by(:project => project,
970 count_and_group_by(:project => project,
952 :field => 'assigned_to_id',
971 :field => 'assigned_to_id',
953 :joins => User.table_name)
972 :joins => User.table_name)
954 end
973 end
955
974
956 def self.by_author(project)
975 def self.by_author(project)
957 count_and_group_by(:project => project,
976 count_and_group_by(:project => project,
958 :field => 'author_id',
977 :field => 'author_id',
959 :joins => User.table_name)
978 :joins => User.table_name)
960 end
979 end
961
980
962 def self.by_subproject(project)
981 def self.by_subproject(project)
963 ActiveRecord::Base.connection.select_all("select s.id as status_id,
982 ActiveRecord::Base.connection.select_all("select s.id as status_id,
964 s.is_closed as closed,
983 s.is_closed as closed,
965 #{Issue.table_name}.project_id as project_id,
984 #{Issue.table_name}.project_id as project_id,
966 count(#{Issue.table_name}.id) as total
985 count(#{Issue.table_name}.id) as total
967 from
986 from
968 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
987 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
969 where
988 where
970 #{Issue.table_name}.status_id=s.id
989 #{Issue.table_name}.status_id=s.id
971 and #{Issue.table_name}.project_id = #{Project.table_name}.id
990 and #{Issue.table_name}.project_id = #{Project.table_name}.id
972 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
991 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
973 and #{Issue.table_name}.project_id <> #{project.id}
992 and #{Issue.table_name}.project_id <> #{project.id}
974 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
993 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
975 end
994 end
976 # End ReportsController extraction
995 # End ReportsController extraction
977
996
978 # Returns an array of projects that user can assign the issue to
997 # Returns an array of projects that user can assign the issue to
979 def allowed_target_projects(user=User.current)
998 def allowed_target_projects(user=User.current)
980 if new_record?
999 if new_record?
981 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
1000 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
982 else
1001 else
983 self.class.allowed_target_projects_on_move(user)
1002 self.class.allowed_target_projects_on_move(user)
984 end
1003 end
985 end
1004 end
986
1005
987 # Returns an array of projects that user can move issues to
1006 # Returns an array of projects that user can move issues to
988 def self.allowed_target_projects_on_move(user=User.current)
1007 def self.allowed_target_projects_on_move(user=User.current)
989 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
1008 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
990 end
1009 end
991
1010
992 private
1011 private
993
1012
994 def after_project_change
1013 def after_project_change
995 # Update project_id on related time entries
1014 # Update project_id on related time entries
996 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1015 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
997
1016
998 # Delete issue relations
1017 # Delete issue relations
999 unless Setting.cross_project_issue_relations?
1018 unless Setting.cross_project_issue_relations?
1000 relations_from.clear
1019 relations_from.clear
1001 relations_to.clear
1020 relations_to.clear
1002 end
1021 end
1003
1022
1004 # Move subtasks
1023 # Move subtasks
1005 children.each do |child|
1024 children.each do |child|
1006 # Change project and keep project
1025 # Change project and keep project
1007 child.send :project=, project, true
1026 child.send :project=, project, true
1008 unless child.save
1027 unless child.save
1009 raise ActiveRecord::Rollback
1028 raise ActiveRecord::Rollback
1010 end
1029 end
1011 end
1030 end
1012 end
1031 end
1013
1032
1014 # Callback for after the creation of an issue by copy
1033 # Callback for after the creation of an issue by copy
1015 # * adds a "copied to" relation with the copied issue
1034 # * adds a "copied to" relation with the copied issue
1016 # * copies subtasks from the copied issue
1035 # * copies subtasks from the copied issue
1017 def after_create_from_copy
1036 def after_create_from_copy
1018 return unless copy? && !@after_create_from_copy_handled
1037 return unless copy? && !@after_create_from_copy_handled
1019
1038
1020 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1039 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1021 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1040 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1022 unless relation.save
1041 unless relation.save
1023 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1042 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1024 end
1043 end
1025 end
1044 end
1026
1045
1027 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1046 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1028 @copied_from.children.each do |child|
1047 @copied_from.children.each do |child|
1029 unless child.visible?
1048 unless child.visible?
1030 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1049 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1031 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1050 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1032 next
1051 next
1033 end
1052 end
1034 copy = Issue.new.copy_from(child, @copy_options)
1053 copy = Issue.new.copy_from(child, @copy_options)
1035 copy.author = author
1054 copy.author = author
1036 copy.project = project
1055 copy.project = project
1037 copy.parent_issue_id = id
1056 copy.parent_issue_id = id
1038 # Children subtasks are copied recursively
1057 # Children subtasks are copied recursively
1039 unless copy.save
1058 unless copy.save
1040 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1059 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1041 end
1060 end
1042 end
1061 end
1043 end
1062 end
1044 @after_create_from_copy_handled = true
1063 @after_create_from_copy_handled = true
1045 end
1064 end
1046
1065
1047 def update_nested_set_attributes
1066 def update_nested_set_attributes
1048 if root_id.nil?
1067 if root_id.nil?
1049 # issue was just created
1068 # issue was just created
1050 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1069 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1051 set_default_left_and_right
1070 set_default_left_and_right
1052 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1071 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1053 if @parent_issue
1072 if @parent_issue
1054 move_to_child_of(@parent_issue)
1073 move_to_child_of(@parent_issue)
1055 end
1074 end
1056 reload
1075 reload
1057 elsif parent_issue_id != parent_id
1076 elsif parent_issue_id != parent_id
1058 former_parent_id = parent_id
1077 former_parent_id = parent_id
1059 # moving an existing issue
1078 # moving an existing issue
1060 if @parent_issue && @parent_issue.root_id == root_id
1079 if @parent_issue && @parent_issue.root_id == root_id
1061 # inside the same tree
1080 # inside the same tree
1062 move_to_child_of(@parent_issue)
1081 move_to_child_of(@parent_issue)
1063 else
1082 else
1064 # to another tree
1083 # to another tree
1065 unless root?
1084 unless root?
1066 move_to_right_of(root)
1085 move_to_right_of(root)
1067 reload
1086 reload
1068 end
1087 end
1069 old_root_id = root_id
1088 old_root_id = root_id
1070 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1089 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1071 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1090 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1072 offset = target_maxright + 1 - lft
1091 offset = target_maxright + 1 - lft
1073 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1092 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1074 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1093 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1075 self[left_column_name] = lft + offset
1094 self[left_column_name] = lft + offset
1076 self[right_column_name] = rgt + offset
1095 self[right_column_name] = rgt + offset
1077 if @parent_issue
1096 if @parent_issue
1078 move_to_child_of(@parent_issue)
1097 move_to_child_of(@parent_issue)
1079 end
1098 end
1080 end
1099 end
1081 reload
1100 reload
1082 # delete invalid relations of all descendants
1101 # delete invalid relations of all descendants
1083 self_and_descendants.each do |issue|
1102 self_and_descendants.each do |issue|
1084 issue.relations.each do |relation|
1103 issue.relations.each do |relation|
1085 relation.destroy unless relation.valid?
1104 relation.destroy unless relation.valid?
1086 end
1105 end
1087 end
1106 end
1088 # update former parent
1107 # update former parent
1089 recalculate_attributes_for(former_parent_id) if former_parent_id
1108 recalculate_attributes_for(former_parent_id) if former_parent_id
1090 end
1109 end
1091 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1110 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1092 end
1111 end
1093
1112
1094 def update_parent_attributes
1113 def update_parent_attributes
1095 recalculate_attributes_for(parent_id) if parent_id
1114 recalculate_attributes_for(parent_id) if parent_id
1096 end
1115 end
1097
1116
1098 def recalculate_attributes_for(issue_id)
1117 def recalculate_attributes_for(issue_id)
1099 if issue_id && p = Issue.find_by_id(issue_id)
1118 if issue_id && p = Issue.find_by_id(issue_id)
1100 # priority = highest priority of children
1119 # priority = highest priority of children
1101 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1120 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1102 p.priority = IssuePriority.find_by_position(priority_position)
1121 p.priority = IssuePriority.find_by_position(priority_position)
1103 end
1122 end
1104
1123
1105 # start/due dates = lowest/highest dates of children
1124 # start/due dates = lowest/highest dates of children
1106 p.start_date = p.children.minimum(:start_date)
1125 p.start_date = p.children.minimum(:start_date)
1107 p.due_date = p.children.maximum(:due_date)
1126 p.due_date = p.children.maximum(:due_date)
1108 if p.start_date && p.due_date && p.due_date < p.start_date
1127 if p.start_date && p.due_date && p.due_date < p.start_date
1109 p.start_date, p.due_date = p.due_date, p.start_date
1128 p.start_date, p.due_date = p.due_date, p.start_date
1110 end
1129 end
1111
1130
1112 # done ratio = weighted average ratio of leaves
1131 # done ratio = weighted average ratio of leaves
1113 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1132 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1114 leaves_count = p.leaves.count
1133 leaves_count = p.leaves.count
1115 if leaves_count > 0
1134 if leaves_count > 0
1116 average = p.leaves.average(:estimated_hours).to_f
1135 average = p.leaves.average(:estimated_hours).to_f
1117 if average == 0
1136 if average == 0
1118 average = 1
1137 average = 1
1119 end
1138 end
1120 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1139 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1121 progress = done / (average * leaves_count)
1140 progress = done / (average * leaves_count)
1122 p.done_ratio = progress.round
1141 p.done_ratio = progress.round
1123 end
1142 end
1124 end
1143 end
1125
1144
1126 # estimate = sum of leaves estimates
1145 # estimate = sum of leaves estimates
1127 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1146 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1128 p.estimated_hours = nil if p.estimated_hours == 0.0
1147 p.estimated_hours = nil if p.estimated_hours == 0.0
1129
1148
1130 # ancestors will be recursively updated
1149 # ancestors will be recursively updated
1131 p.save(:validate => false)
1150 p.save(:validate => false)
1132 end
1151 end
1133 end
1152 end
1134
1153
1135 # Update issues so their versions are not pointing to a
1154 # Update issues so their versions are not pointing to a
1136 # fixed_version that is not shared with the issue's project
1155 # fixed_version that is not shared with the issue's project
1137 def self.update_versions(conditions=nil)
1156 def self.update_versions(conditions=nil)
1138 # Only need to update issues with a fixed_version from
1157 # Only need to update issues with a fixed_version from
1139 # a different project and that is not systemwide shared
1158 # a different project and that is not systemwide shared
1140 Issue.scoped(:conditions => conditions).all(
1159 Issue.scoped(:conditions => conditions).all(
1141 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1160 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1142 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1161 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1143 " AND #{Version.table_name}.sharing <> 'system'",
1162 " AND #{Version.table_name}.sharing <> 'system'",
1144 :include => [:project, :fixed_version]
1163 :include => [:project, :fixed_version]
1145 ).each do |issue|
1164 ).each do |issue|
1146 next if issue.project.nil? || issue.fixed_version.nil?
1165 next if issue.project.nil? || issue.fixed_version.nil?
1147 unless issue.project.shared_versions.include?(issue.fixed_version)
1166 unless issue.project.shared_versions.include?(issue.fixed_version)
1148 issue.init_journal(User.current)
1167 issue.init_journal(User.current)
1149 issue.fixed_version = nil
1168 issue.fixed_version = nil
1150 issue.save
1169 issue.save
1151 end
1170 end
1152 end
1171 end
1153 end
1172 end
1154
1173
1155 # Callback on file attachment
1174 # Callback on file attachment
1156 def attachment_added(obj)
1175 def attachment_added(obj)
1157 if @current_journal && !obj.new_record?
1176 if @current_journal && !obj.new_record?
1158 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1177 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1159 end
1178 end
1160 end
1179 end
1161
1180
1162 # Callback on attachment deletion
1181 # Callback on attachment deletion
1163 def attachment_removed(obj)
1182 def attachment_removed(obj)
1164 if @current_journal && !obj.new_record?
1183 if @current_journal && !obj.new_record?
1165 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1184 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1166 @current_journal.save
1185 @current_journal.save
1167 end
1186 end
1168 end
1187 end
1169
1188
1170 # Default assignment based on category
1189 # Default assignment based on category
1171 def default_assign
1190 def default_assign
1172 if assigned_to.nil? && category && category.assigned_to
1191 if assigned_to.nil? && category && category.assigned_to
1173 self.assigned_to = category.assigned_to
1192 self.assigned_to = category.assigned_to
1174 end
1193 end
1175 end
1194 end
1176
1195
1177 # Updates start/due dates of following issues
1196 # Updates start/due dates of following issues
1178 def reschedule_following_issues
1197 def reschedule_following_issues
1179 if start_date_changed? || due_date_changed?
1198 if start_date_changed? || due_date_changed?
1180 relations_from.each do |relation|
1199 relations_from.each do |relation|
1181 relation.set_issue_to_dates
1200 relation.set_issue_to_dates
1182 end
1201 end
1183 end
1202 end
1184 end
1203 end
1185
1204
1186 # Closes duplicates if the issue is being closed
1205 # Closes duplicates if the issue is being closed
1187 def close_duplicates
1206 def close_duplicates
1188 if closing?
1207 if closing?
1189 duplicates.each do |duplicate|
1208 duplicates.each do |duplicate|
1190 # Reload is need in case the duplicate was updated by a previous duplicate
1209 # Reload is need in case the duplicate was updated by a previous duplicate
1191 duplicate.reload
1210 duplicate.reload
1192 # Don't re-close it if it's already closed
1211 # Don't re-close it if it's already closed
1193 next if duplicate.closed?
1212 next if duplicate.closed?
1194 # Same user and notes
1213 # Same user and notes
1195 if @current_journal
1214 if @current_journal
1196 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1215 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1197 end
1216 end
1198 duplicate.update_attribute :status, self.status
1217 duplicate.update_attribute :status, self.status
1199 end
1218 end
1200 end
1219 end
1201 end
1220 end
1202
1221
1203 # Make sure updated_on is updated when adding a note
1222 # Make sure updated_on is updated when adding a note
1204 def force_updated_on_change
1223 def force_updated_on_change
1205 if @current_journal
1224 if @current_journal
1206 self.updated_on = current_time_from_proper_timezone
1225 self.updated_on = current_time_from_proper_timezone
1207 end
1226 end
1208 end
1227 end
1209
1228
1210 # Saves the changes in a Journal
1229 # Saves the changes in a Journal
1211 # Called after_save
1230 # Called after_save
1212 def create_journal
1231 def create_journal
1213 if @current_journal
1232 if @current_journal
1214 # attributes changes
1233 # attributes changes
1215 if @attributes_before_change
1234 if @attributes_before_change
1216 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1235 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1217 before = @attributes_before_change[c]
1236 before = @attributes_before_change[c]
1218 after = send(c)
1237 after = send(c)
1219 next if before == after || (before.blank? && after.blank?)
1238 next if before == after || (before.blank? && after.blank?)
1220 @current_journal.details << JournalDetail.new(:property => 'attr',
1239 @current_journal.details << JournalDetail.new(:property => 'attr',
1221 :prop_key => c,
1240 :prop_key => c,
1222 :old_value => before,
1241 :old_value => before,
1223 :value => after)
1242 :value => after)
1224 }
1243 }
1225 end
1244 end
1226 if @custom_values_before_change
1245 if @custom_values_before_change
1227 # custom fields changes
1246 # custom fields changes
1228 custom_field_values.each {|c|
1247 custom_field_values.each {|c|
1229 before = @custom_values_before_change[c.custom_field_id]
1248 before = @custom_values_before_change[c.custom_field_id]
1230 after = c.value
1249 after = c.value
1231 next if before == after || (before.blank? && after.blank?)
1250 next if before == after || (before.blank? && after.blank?)
1232
1251
1233 if before.is_a?(Array) || after.is_a?(Array)
1252 if before.is_a?(Array) || after.is_a?(Array)
1234 before = [before] unless before.is_a?(Array)
1253 before = [before] unless before.is_a?(Array)
1235 after = [after] unless after.is_a?(Array)
1254 after = [after] unless after.is_a?(Array)
1236
1255
1237 # values removed
1256 # values removed
1238 (before - after).reject(&:blank?).each do |value|
1257 (before - after).reject(&:blank?).each do |value|
1239 @current_journal.details << JournalDetail.new(:property => 'cf',
1258 @current_journal.details << JournalDetail.new(:property => 'cf',
1240 :prop_key => c.custom_field_id,
1259 :prop_key => c.custom_field_id,
1241 :old_value => value,
1260 :old_value => value,
1242 :value => nil)
1261 :value => nil)
1243 end
1262 end
1244 # values added
1263 # values added
1245 (after - before).reject(&:blank?).each do |value|
1264 (after - before).reject(&:blank?).each do |value|
1246 @current_journal.details << JournalDetail.new(:property => 'cf',
1265 @current_journal.details << JournalDetail.new(:property => 'cf',
1247 :prop_key => c.custom_field_id,
1266 :prop_key => c.custom_field_id,
1248 :old_value => nil,
1267 :old_value => nil,
1249 :value => value)
1268 :value => value)
1250 end
1269 end
1251 else
1270 else
1252 @current_journal.details << JournalDetail.new(:property => 'cf',
1271 @current_journal.details << JournalDetail.new(:property => 'cf',
1253 :prop_key => c.custom_field_id,
1272 :prop_key => c.custom_field_id,
1254 :old_value => before,
1273 :old_value => before,
1255 :value => after)
1274 :value => after)
1256 end
1275 end
1257 }
1276 }
1258 end
1277 end
1259 @current_journal.save
1278 @current_journal.save
1260 # reset current journal
1279 # reset current journal
1261 init_journal @current_journal.user, @current_journal.notes
1280 init_journal @current_journal.user, @current_journal.notes
1262 end
1281 end
1263 end
1282 end
1264
1283
1265 # Query generator for selecting groups of issue counts for a project
1284 # Query generator for selecting groups of issue counts for a project
1266 # based on specific criteria
1285 # based on specific criteria
1267 #
1286 #
1268 # Options
1287 # Options
1269 # * project - Project to search in.
1288 # * project - Project to search in.
1270 # * field - String. Issue field to key off of in the grouping.
1289 # * field - String. Issue field to key off of in the grouping.
1271 # * joins - String. The table name to join against.
1290 # * joins - String. The table name to join against.
1272 def self.count_and_group_by(options)
1291 def self.count_and_group_by(options)
1273 project = options.delete(:project)
1292 project = options.delete(:project)
1274 select_field = options.delete(:field)
1293 select_field = options.delete(:field)
1275 joins = options.delete(:joins)
1294 joins = options.delete(:joins)
1276
1295
1277 where = "#{Issue.table_name}.#{select_field}=j.id"
1296 where = "#{Issue.table_name}.#{select_field}=j.id"
1278
1297
1279 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1298 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1280 s.is_closed as closed,
1299 s.is_closed as closed,
1281 j.id as #{select_field},
1300 j.id as #{select_field},
1282 count(#{Issue.table_name}.id) as total
1301 count(#{Issue.table_name}.id) as total
1283 from
1302 from
1284 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1303 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1285 where
1304 where
1286 #{Issue.table_name}.status_id=s.id
1305 #{Issue.table_name}.status_id=s.id
1287 and #{where}
1306 and #{where}
1288 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1307 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1289 and #{visible_condition(User.current, :project => project)}
1308 and #{visible_condition(User.current, :project => project)}
1290 group by s.id, s.is_closed, j.id")
1309 group by s.id, s.is_closed, j.id")
1291 end
1310 end
1292 end
1311 end
@@ -1,147 +1,166
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 used to represent the relations of an issue
19 class IssueRelations < Array
20 include Redmine::I18n
21
22 def initialize(issue, *args)
23 @issue = issue
24 super(*args)
25 end
26
27 def to_s(*args)
28 map {|relation| "#{l(relation.label_for(@issue))} ##{relation.other_issue(@issue).id}"}.join(', ')
29 end
30 end
31
18 class IssueRelation < ActiveRecord::Base
32 class IssueRelation < ActiveRecord::Base
19 belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
33 belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
20 belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
34 belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
21
35
22 TYPE_RELATES = "relates"
36 TYPE_RELATES = "relates"
23 TYPE_DUPLICATES = "duplicates"
37 TYPE_DUPLICATES = "duplicates"
24 TYPE_DUPLICATED = "duplicated"
38 TYPE_DUPLICATED = "duplicated"
25 TYPE_BLOCKS = "blocks"
39 TYPE_BLOCKS = "blocks"
26 TYPE_BLOCKED = "blocked"
40 TYPE_BLOCKED = "blocked"
27 TYPE_PRECEDES = "precedes"
41 TYPE_PRECEDES = "precedes"
28 TYPE_FOLLOWS = "follows"
42 TYPE_FOLLOWS = "follows"
29 TYPE_COPIED_TO = "copied_to"
43 TYPE_COPIED_TO = "copied_to"
30 TYPE_COPIED_FROM = "copied_from"
44 TYPE_COPIED_FROM = "copied_from"
31
45
32 TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES },
46 TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES },
33 TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED },
47 TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED },
34 TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
48 TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES },
35 TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4, :sym => TYPE_BLOCKED },
49 TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4, :sym => TYPE_BLOCKED },
36 TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
50 TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS },
37 TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS },
51 TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS },
38 TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES },
52 TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES },
39 TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from, :order => 8, :sym => TYPE_COPIED_FROM },
53 TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from, :order => 8, :sym => TYPE_COPIED_FROM },
40 TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to, :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO }
54 TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to, :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO }
41 }.freeze
55 }.freeze
42
56
43 validates_presence_of :issue_from, :issue_to, :relation_type
57 validates_presence_of :issue_from, :issue_to, :relation_type
44 validates_inclusion_of :relation_type, :in => TYPES.keys
58 validates_inclusion_of :relation_type, :in => TYPES.keys
45 validates_numericality_of :delay, :allow_nil => true
59 validates_numericality_of :delay, :allow_nil => true
46 validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
60 validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
47
61
48 validate :validate_issue_relation
62 validate :validate_issue_relation
49
63
50 attr_protected :issue_from_id, :issue_to_id
64 attr_protected :issue_from_id, :issue_to_id
51
65
52 before_save :handle_issue_order
66 before_save :handle_issue_order
53
67
54 def visible?(user=User.current)
68 def visible?(user=User.current)
55 (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
69 (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user))
56 end
70 end
57
71
58 def deletable?(user=User.current)
72 def deletable?(user=User.current)
59 visible?(user) &&
73 visible?(user) &&
60 ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
74 ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) ||
61 (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
75 (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project)))
62 end
76 end
63
77
64 def initialize(attributes=nil, *args)
78 def initialize(attributes=nil, *args)
65 super
79 super
66 if new_record?
80 if new_record?
67 if relation_type.blank?
81 if relation_type.blank?
68 self.relation_type = IssueRelation::TYPE_RELATES
82 self.relation_type = IssueRelation::TYPE_RELATES
69 end
83 end
70 end
84 end
71 end
85 end
72
86
73 def validate_issue_relation
87 def validate_issue_relation
74 if issue_from && issue_to
88 if issue_from && issue_to
75 errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
89 errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id
76 errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
90 errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations?
77 #detect circular dependencies depending wether the relation should be reversed
91 #detect circular dependencies depending wether the relation should be reversed
78 if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
92 if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
79 errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
93 errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to
80 else
94 else
81 errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
95 errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from
82 end
96 end
83 errors.add :base, :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
97 errors.add :base, :cant_link_an_issue_with_a_descendant if issue_from.is_descendant_of?(issue_to) || issue_from.is_ancestor_of?(issue_to)
84 end
98 end
85 end
99 end
86
100
87 def other_issue(issue)
101 def other_issue(issue)
88 (self.issue_from_id == issue.id) ? issue_to : issue_from
102 (self.issue_from_id == issue.id) ? issue_to : issue_from
89 end
103 end
90
104
91 # Returns the relation type for +issue+
105 # Returns the relation type for +issue+
92 def relation_type_for(issue)
106 def relation_type_for(issue)
93 if TYPES[relation_type]
107 if TYPES[relation_type]
94 if self.issue_from_id == issue.id
108 if self.issue_from_id == issue.id
95 relation_type
109 relation_type
96 else
110 else
97 TYPES[relation_type][:sym]
111 TYPES[relation_type][:sym]
98 end
112 end
99 end
113 end
100 end
114 end
101
115
102 def label_for(issue)
116 def label_for(issue)
103 TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
117 TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow
104 end
118 end
105
119
120 def css_classes_for(issue)
121 "rel-#{relation_type_for(issue)}"
122 end
123
106 def handle_issue_order
124 def handle_issue_order
107 reverse_if_needed
125 reverse_if_needed
108
126
109 if TYPE_PRECEDES == relation_type
127 if TYPE_PRECEDES == relation_type
110 self.delay ||= 0
128 self.delay ||= 0
111 else
129 else
112 self.delay = nil
130 self.delay = nil
113 end
131 end
114 set_issue_to_dates
132 set_issue_to_dates
115 end
133 end
116
134
117 def set_issue_to_dates
135 def set_issue_to_dates
118 soonest_start = self.successor_soonest_start
136 soonest_start = self.successor_soonest_start
119 if soonest_start && issue_to
137 if soonest_start && issue_to
120 issue_to.reschedule_after(soonest_start)
138 issue_to.reschedule_after(soonest_start)
121 end
139 end
122 end
140 end
123
141
124 def successor_soonest_start
142 def successor_soonest_start
125 if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date)
143 if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date)
126 (issue_from.due_date || issue_from.start_date) + 1 + delay
144 (issue_from.due_date || issue_from.start_date) + 1 + delay
127 end
145 end
128 end
146 end
129
147
130 def <=>(relation)
148 def <=>(relation)
131 TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
149 r = TYPES[self.relation_type][:order] <=> TYPES[relation.relation_type][:order]
150 r == 0 ? id <=> relation.id : r
132 end
151 end
133
152
134 private
153 private
135
154
136 # Reverses the relation if needed so that it gets stored in the proper way
155 # Reverses the relation if needed so that it gets stored in the proper way
137 # Should not be reversed before validation so that it can be displayed back
156 # Should not be reversed before validation so that it can be displayed back
138 # as entered on new relation form
157 # as entered on new relation form
139 def reverse_if_needed
158 def reverse_if_needed
140 if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
159 if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse]
141 issue_tmp = issue_to
160 issue_tmp = issue_to
142 self.issue_to = issue_from
161 self.issue_to = issue_from
143 self.issue_from = issue_tmp
162 self.issue_from = issue_tmp
144 self.relation_type = TYPES[relation_type][:reverse]
163 self.relation_type = TYPES[relation_type][:reverse]
145 end
164 end
146 end
165 end
147 end
166 end
@@ -1,962 +1,1019
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 @caption_key = options[:caption] || "field_#{name}"
30 @caption_key = options[:caption] || "field_#{name}"
31 end
31 end
32
32
33 def caption
33 def caption
34 l(@caption_key)
34 l(@caption_key)
35 end
35 end
36
36
37 # Returns true if the column is sortable, otherwise false
37 # Returns true if the column is sortable, otherwise false
38 def sortable?
38 def sortable?
39 !@sortable.nil?
39 !@sortable.nil?
40 end
40 end
41
41
42 def sortable
42 def sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
43 @sortable.is_a?(Proc) ? @sortable.call : @sortable
44 end
44 end
45
45
46 def value(issue)
46 def value(issue)
47 issue.send name
47 issue.send name
48 end
48 end
49
49
50 def css_classes
50 def css_classes
51 name
51 name
52 end
52 end
53 end
53 end
54
54
55 class QueryCustomFieldColumn < QueryColumn
55 class QueryCustomFieldColumn < QueryColumn
56
56
57 def initialize(custom_field)
57 def initialize(custom_field)
58 self.name = "cf_#{custom_field.id}".to_sym
58 self.name = "cf_#{custom_field.id}".to_sym
59 self.sortable = custom_field.order_statement || false
59 self.sortable = custom_field.order_statement || false
60 self.groupable = custom_field.group_statement || false
60 self.groupable = custom_field.group_statement || false
61 @cf = custom_field
61 @cf = custom_field
62 end
62 end
63
63
64 def caption
64 def caption
65 @cf.name
65 @cf.name
66 end
66 end
67
67
68 def custom_field
68 def custom_field
69 @cf
69 @cf
70 end
70 end
71
71
72 def value(issue)
72 def value(issue)
73 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
73 cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)}
74 cv.size > 1 ? cv : cv.first
74 cv.size > 1 ? cv : cv.first
75 end
75 end
76
76
77 def css_classes
77 def css_classes
78 @css_classes ||= "#{name} #{@cf.field_format}"
78 @css_classes ||= "#{name} #{@cf.field_format}"
79 end
79 end
80 end
80 end
81
81
82 class Query < ActiveRecord::Base
82 class Query < ActiveRecord::Base
83 class StatementInvalid < ::ActiveRecord::StatementInvalid
83 class StatementInvalid < ::ActiveRecord::StatementInvalid
84 end
84 end
85
85
86 belongs_to :project
86 belongs_to :project
87 belongs_to :user
87 belongs_to :user
88 serialize :filters
88 serialize :filters
89 serialize :column_names
89 serialize :column_names
90 serialize :sort_criteria, Array
90 serialize :sort_criteria, Array
91
91
92 attr_protected :project_id, :user_id
92 attr_protected :project_id, :user_id
93
93
94 validates_presence_of :name
94 validates_presence_of :name
95 validates_length_of :name, :maximum => 255
95 validates_length_of :name, :maximum => 255
96 validate :validate_query_filters
96 validate :validate_query_filters
97
97
98 @@operators = { "=" => :label_equals,
98 @@operators = { "=" => :label_equals,
99 "!" => :label_not_equals,
99 "!" => :label_not_equals,
100 "o" => :label_open_issues,
100 "o" => :label_open_issues,
101 "c" => :label_closed_issues,
101 "c" => :label_closed_issues,
102 "!*" => :label_none,
102 "!*" => :label_none,
103 "*" => :label_all,
103 "*" => :label_all,
104 ">=" => :label_greater_or_equal,
104 ">=" => :label_greater_or_equal,
105 "<=" => :label_less_or_equal,
105 "<=" => :label_less_or_equal,
106 "><" => :label_between,
106 "><" => :label_between,
107 "<t+" => :label_in_less_than,
107 "<t+" => :label_in_less_than,
108 ">t+" => :label_in_more_than,
108 ">t+" => :label_in_more_than,
109 "t+" => :label_in,
109 "t+" => :label_in,
110 "t" => :label_today,
110 "t" => :label_today,
111 "w" => :label_this_week,
111 "w" => :label_this_week,
112 ">t-" => :label_less_than_ago,
112 ">t-" => :label_less_than_ago,
113 "<t-" => :label_more_than_ago,
113 "<t-" => :label_more_than_ago,
114 "t-" => :label_ago,
114 "t-" => :label_ago,
115 "~" => :label_contains,
115 "~" => :label_contains,
116 "!~" => :label_not_contains }
116 "!~" => :label_not_contains,
117 "=p" => :label_any_issues_in_project,
118 "=!p" => :label_any_issues_not_in_project}
117
119
118 cattr_reader :operators
120 cattr_reader :operators
119
121
120 @@operators_by_filter_type = { :list => [ "=", "!" ],
122 @@operators_by_filter_type = { :list => [ "=", "!" ],
121 :list_status => [ "o", "=", "!", "c", "*" ],
123 :list_status => [ "o", "=", "!", "c", "*" ],
122 :list_optional => [ "=", "!", "!*", "*" ],
124 :list_optional => [ "=", "!", "!*", "*" ],
123 :list_subprojects => [ "*", "!*", "=" ],
125 :list_subprojects => [ "*", "!*", "=" ],
124 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
126 :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ],
125 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
127 :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ],
126 :string => [ "=", "~", "!", "!~", "!*", "*" ],
128 :string => [ "=", "~", "!", "!~", "!*", "*" ],
127 :text => [ "~", "!~", "!*", "*" ],
129 :text => [ "~", "!~", "!*", "*" ],
128 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
130 :integer => [ "=", ">=", "<=", "><", "!*", "*" ],
129 :float => [ "=", ">=", "<=", "><", "!*", "*" ] }
131 :float => [ "=", ">=", "<=", "><", "!*", "*" ],
132 :relation => ["=", "=p", "=!p", "!*", "*"]}
130
133
131 cattr_reader :operators_by_filter_type
134 cattr_reader :operators_by_filter_type
132
135
133 @@available_columns = [
136 @@available_columns = [
134 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
137 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
135 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
138 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
136 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
139 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
137 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
140 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
138 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
141 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
139 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
142 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
140 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
143 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
141 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
144 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
142 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
145 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
143 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
146 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
144 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
147 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
145 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
148 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
146 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
149 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
147 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
150 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
148 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
151 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
149 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
152 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
153 QueryColumn.new(:relations, :caption => :label_related_issues)
150 ]
154 ]
151 cattr_reader :available_columns
155 cattr_reader :available_columns
152
156
153 scope :visible, lambda {|*args|
157 scope :visible, lambda {|*args|
154 user = args.shift || User.current
158 user = args.shift || User.current
155 base = Project.allowed_to_condition(user, :view_issues, *args)
159 base = Project.allowed_to_condition(user, :view_issues, *args)
156 user_id = user.logged? ? user.id : 0
160 user_id = user.logged? ? user.id : 0
157 {
161 {
158 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
162 :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id],
159 :include => :project
163 :include => :project
160 }
164 }
161 }
165 }
162
166
163 def initialize(attributes=nil, *args)
167 def initialize(attributes=nil, *args)
164 super attributes
168 super attributes
165 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
169 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
166 @is_for_all = project.nil?
170 @is_for_all = project.nil?
167 end
171 end
168
172
169 def validate_query_filters
173 def validate_query_filters
170 filters.each_key do |field|
174 filters.each_key do |field|
171 if values_for(field)
175 if values_for(field)
172 case type_for(field)
176 case type_for(field)
173 when :integer
177 when :integer
174 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
178 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) }
175 when :float
179 when :float
176 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
180 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) }
177 when :date, :date_past
181 when :date, :date_past
178 case operator_for(field)
182 case operator_for(field)
179 when "=", ">=", "<=", "><"
183 when "=", ">=", "<=", "><"
180 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?) }
184 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?) }
181 when ">t-", "<t-", "t-"
185 when ">t-", "<t-", "t-"
182 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
186 add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) }
183 end
187 end
184 end
188 end
185 end
189 end
186
190
187 add_filter_error(field, :blank) unless
191 add_filter_error(field, :blank) unless
188 # filter requires one or more values
192 # filter requires one or more values
189 (values_for(field) and !values_for(field).first.blank?) or
193 (values_for(field) and !values_for(field).first.blank?) or
190 # filter doesn't require any value
194 # filter doesn't require any value
191 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
195 ["o", "c", "!*", "*", "t", "w"].include? operator_for(field)
192 end if filters
196 end if filters
193 end
197 end
194
198
195 def add_filter_error(field, message)
199 def add_filter_error(field, message)
196 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
200 m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages')
197 errors.add(:base, m)
201 errors.add(:base, m)
198 end
202 end
199
203
200 # Returns true if the query is visible to +user+ or the current user.
204 # Returns true if the query is visible to +user+ or the current user.
201 def visible?(user=User.current)
205 def visible?(user=User.current)
202 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
206 (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id)
203 end
207 end
204
208
205 def editable_by?(user)
209 def editable_by?(user)
206 return false unless user
210 return false unless user
207 # Admin can edit them all and regular users can edit their private queries
211 # Admin can edit them all and regular users can edit their private queries
208 return true if user.admin? || (!is_public && self.user_id == user.id)
212 return true if user.admin? || (!is_public && self.user_id == user.id)
209 # Members can not edit public queries that are for all project (only admin is allowed to)
213 # Members can not edit public queries that are for all project (only admin is allowed to)
210 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
214 is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project)
211 end
215 end
212
216
213 def trackers
217 def trackers
214 @trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
218 @trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers
215 end
219 end
216
220
217 # Returns a hash of localized labels for all filter operators
221 # Returns a hash of localized labels for all filter operators
218 def self.operators_labels
222 def self.operators_labels
219 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
223 operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h}
220 end
224 end
221
225
222 def available_filters
226 def available_filters
223 return @available_filters if @available_filters
227 return @available_filters if @available_filters
224
228
225 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
229 @available_filters = { "status_id" => { :type => :list_status, :order => 1, :values => IssueStatus.find(:all, :order => 'position').collect{|s| [s.name, s.id.to_s] } },
226 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
230 "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } },
227 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
231 "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } },
228 "subject" => { :type => :text, :order => 8 },
232 "subject" => { :type => :text, :order => 8 },
229 "created_on" => { :type => :date_past, :order => 9 },
233 "created_on" => { :type => :date_past, :order => 9 },
230 "updated_on" => { :type => :date_past, :order => 10 },
234 "updated_on" => { :type => :date_past, :order => 10 },
231 "start_date" => { :type => :date, :order => 11 },
235 "start_date" => { :type => :date, :order => 11 },
232 "due_date" => { :type => :date, :order => 12 },
236 "due_date" => { :type => :date, :order => 12 },
233 "estimated_hours" => { :type => :float, :order => 13 },
237 "estimated_hours" => { :type => :float, :order => 13 },
234 "done_ratio" => { :type => :integer, :order => 14 }}
238 "done_ratio" => { :type => :integer, :order => 14 }}
235
239
240 IssueRelation::TYPES.each do |relation_type, options|
241 @available_filters[relation_type] = {:type => :relation, :order => @available_filters.size + 100, :label => options[:name]}
242 end
243
236 principals = []
244 principals = []
237 if project
245 if project
238 principals += project.principals.sort
246 principals += project.principals.sort
239 unless project.leaf?
247 unless project.leaf?
240 subprojects = project.descendants.visible.all
248 subprojects = project.descendants.visible.all
241 if subprojects.any?
249 if subprojects.any?
242 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
250 @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } }
243 principals += Principal.member_of(subprojects)
251 principals += Principal.member_of(subprojects)
244 end
252 end
245 end
253 end
246 else
254 else
247 all_projects = Project.visible.all
248 if all_projects.any?
255 if all_projects.any?
249 # members of visible projects
256 # members of visible projects
250 principals += Principal.member_of(all_projects)
257 principals += Principal.member_of(all_projects)
251
258
252 # project filter
259 # project filter
253 project_values = []
260 project_values = []
254 if User.current.logged? && User.current.memberships.any?
261 if User.current.logged? && User.current.memberships.any?
255 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
262 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
256 end
263 end
257 Project.project_tree(all_projects) do |p, level|
264 project_values += all_projects_values
258 prefix = (level > 0 ? ('--' * level + ' ') : '')
259 project_values << ["#{prefix}#{p.name}", p.id.to_s]
260 end
261 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
265 @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty?
262 end
266 end
263 end
267 end
264 principals.uniq!
268 principals.uniq!
265 principals.sort!
269 principals.sort!
266 users = principals.select {|p| p.is_a?(User)}
270 users = principals.select {|p| p.is_a?(User)}
267
271
268 assigned_to_values = []
272 assigned_to_values = []
269 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
273 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
270 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
274 assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] }
271 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
275 @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty?
272
276
273 author_values = []
277 author_values = []
274 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
278 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
275 author_values += users.collect{|s| [s.name, s.id.to_s] }
279 author_values += users.collect{|s| [s.name, s.id.to_s] }
276 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
280 @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty?
277
281
278 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
282 group_values = Group.all.collect {|g| [g.name, g.id.to_s] }
279 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
283 @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty?
280
284
281 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
285 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
282 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
286 @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty?
283
287
284 if User.current.logged?
288 if User.current.logged?
285 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
289 @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] }
286 end
290 end
287
291
288 if project
292 if project
289 # project specific filters
293 # project specific filters
290 categories = project.issue_categories.all
294 categories = project.issue_categories.all
291 unless categories.empty?
295 unless categories.empty?
292 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
296 @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } }
293 end
297 end
294 versions = project.shared_versions.all
298 versions = project.shared_versions.all
295 unless versions.empty?
299 unless versions.empty?
296 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
300 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
297 end
301 end
298 add_custom_fields_filters(project.all_issue_custom_fields)
302 add_custom_fields_filters(project.all_issue_custom_fields)
299 else
303 else
300 # global filters for cross project issue list
304 # global filters for cross project issue list
301 system_shared_versions = Version.visible.find_all_by_sharing('system')
305 system_shared_versions = Version.visible.find_all_by_sharing('system')
302 unless system_shared_versions.empty?
306 unless system_shared_versions.empty?
303 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
307 @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => system_shared_versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] } }
304 end
308 end
305 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
309 add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true}))
306 end
310 end
307
311
308 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
312 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
309
313
310 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
314 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
311 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
315 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
312 @available_filters["is_private"] = { :type => :list, :order => 15, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
316 @available_filters["is_private"] = { :type => :list, :order => 15, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] }
313 end
317 end
314
318
315 Tracker.disabled_core_fields(trackers).each {|field|
319 Tracker.disabled_core_fields(trackers).each {|field|
316 @available_filters.delete field
320 @available_filters.delete field
317 }
321 }
318
322
319 @available_filters.each do |field, options|
323 @available_filters.each do |field, options|
320 options[:name] ||= l("field_#{field}".gsub(/_id$/, ''))
324 options[:name] ||= l(options[:label] || "field_#{field}".gsub(/_id$/, ''))
321 end
325 end
322
326
323 @available_filters
327 @available_filters
324 end
328 end
325
329
326 # Returns a representation of the available filters for JSON serialization
330 # Returns a representation of the available filters for JSON serialization
327 def available_filters_as_json
331 def available_filters_as_json
328 json = {}
332 json = {}
329 available_filters.each do |field, options|
333 available_filters.each do |field, options|
330 json[field] = options.slice(:type, :name, :values).stringify_keys
334 json[field] = options.slice(:type, :name, :values).stringify_keys
331 end
335 end
332 json
336 json
333 end
337 end
334
338
339 def all_projects
340 @all_projects ||= Project.visible.all
341 end
342
343 def all_projects_values
344 return @all_projects_values if @all_projects_values
345
346 values = []
347 Project.project_tree(all_projects) do |p, level|
348 prefix = (level > 0 ? ('--' * level + ' ') : '')
349 values << ["#{prefix}#{p.name}", p.id.to_s]
350 end
351 @all_projects_values = values
352 end
353
335 def add_filter(field, operator, values)
354 def add_filter(field, operator, values)
336 # values must be an array
355 # values must be an array
337 return unless values.nil? || values.is_a?(Array)
356 return unless values.nil? || values.is_a?(Array)
338 # check if field is defined as an available filter
357 # check if field is defined as an available filter
339 if available_filters.has_key? field
358 if available_filters.has_key? field
340 filter_options = available_filters[field]
359 filter_options = available_filters[field]
341 # check if operator is allowed for that filter
360 # check if operator is allowed for that filter
342 #if @@operators_by_filter_type[filter_options[:type]].include? operator
361 #if @@operators_by_filter_type[filter_options[:type]].include? operator
343 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
362 # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]})
344 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
363 # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator
345 #end
364 #end
346 filters[field] = {:operator => operator, :values => (values || [''])}
365 filters[field] = {:operator => operator, :values => (values || [''])}
347 end
366 end
348 end
367 end
349
368
350 def add_short_filter(field, expression)
369 def add_short_filter(field, expression)
351 return unless expression && available_filters.has_key?(field)
370 return unless expression && available_filters.has_key?(field)
352 field_type = available_filters[field][:type]
371 field_type = available_filters[field][:type]
353 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
372 @@operators_by_filter_type[field_type].sort.reverse.detect do |operator|
354 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
373 next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/
355 add_filter field, operator, $1.present? ? $1.split('|') : ['']
374 add_filter field, operator, $1.present? ? $1.split('|') : ['']
356 end || add_filter(field, '=', expression.split('|'))
375 end || add_filter(field, '=', expression.split('|'))
357 end
376 end
358
377
359 # Add multiple filters using +add_filter+
378 # Add multiple filters using +add_filter+
360 def add_filters(fields, operators, values)
379 def add_filters(fields, operators, values)
361 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
380 if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash))
362 fields.each do |field|
381 fields.each do |field|
363 add_filter(field, operators[field], values && values[field])
382 add_filter(field, operators[field], values && values[field])
364 end
383 end
365 end
384 end
366 end
385 end
367
386
368 def has_filter?(field)
387 def has_filter?(field)
369 filters and filters[field]
388 filters and filters[field]
370 end
389 end
371
390
372 def type_for(field)
391 def type_for(field)
373 available_filters[field][:type] if available_filters.has_key?(field)
392 available_filters[field][:type] if available_filters.has_key?(field)
374 end
393 end
375
394
376 def operator_for(field)
395 def operator_for(field)
377 has_filter?(field) ? filters[field][:operator] : nil
396 has_filter?(field) ? filters[field][:operator] : nil
378 end
397 end
379
398
380 def values_for(field)
399 def values_for(field)
381 has_filter?(field) ? filters[field][:values] : nil
400 has_filter?(field) ? filters[field][:values] : nil
382 end
401 end
383
402
384 def value_for(field, index=0)
403 def value_for(field, index=0)
385 (values_for(field) || [])[index]
404 (values_for(field) || [])[index]
386 end
405 end
387
406
388 def label_for(field)
407 def label_for(field)
389 label = available_filters[field][:name] if available_filters.has_key?(field)
408 label = available_filters[field][:name] if available_filters.has_key?(field)
390 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
409 label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field)
391 end
410 end
392
411
393 def available_columns
412 def available_columns
394 return @available_columns if @available_columns
413 return @available_columns if @available_columns
395 @available_columns = ::Query.available_columns.dup
414 @available_columns = ::Query.available_columns.dup
396 @available_columns += (project ?
415 @available_columns += (project ?
397 project.all_issue_custom_fields :
416 project.all_issue_custom_fields :
398 IssueCustomField.find(:all)
417 IssueCustomField.find(:all)
399 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
418 ).collect {|cf| QueryCustomFieldColumn.new(cf) }
400
419
401 if User.current.allowed_to?(:view_time_entries, project, :global => true)
420 if User.current.allowed_to?(:view_time_entries, project, :global => true)
402 index = nil
421 index = nil
403 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
422 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
404 index = (index ? index + 1 : -1)
423 index = (index ? index + 1 : -1)
405 # insert the column after estimated_hours or at the end
424 # insert the column after estimated_hours or at the end
406 @available_columns.insert index, QueryColumn.new(:spent_hours,
425 @available_columns.insert index, QueryColumn.new(:spent_hours,
407 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
426 :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)",
408 :default_order => 'desc',
427 :default_order => 'desc',
409 :caption => :label_spent_time
428 :caption => :label_spent_time
410 )
429 )
411 end
430 end
412
431
413 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
432 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
414 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
433 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
415 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
434 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
416 end
435 end
417
436
418 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
437 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
419 @available_columns.reject! {|column|
438 @available_columns.reject! {|column|
420 disabled_fields.include?(column.name.to_s)
439 disabled_fields.include?(column.name.to_s)
421 }
440 }
422
441
423 @available_columns
442 @available_columns
424 end
443 end
425
444
426 def self.available_columns=(v)
445 def self.available_columns=(v)
427 self.available_columns = (v)
446 self.available_columns = (v)
428 end
447 end
429
448
430 def self.add_available_column(column)
449 def self.add_available_column(column)
431 self.available_columns << (column) if column.is_a?(QueryColumn)
450 self.available_columns << (column) if column.is_a?(QueryColumn)
432 end
451 end
433
452
434 # Returns an array of columns that can be used to group the results
453 # Returns an array of columns that can be used to group the results
435 def groupable_columns
454 def groupable_columns
436 available_columns.select {|c| c.groupable}
455 available_columns.select {|c| c.groupable}
437 end
456 end
438
457
439 # Returns a Hash of columns and the key for sorting
458 # Returns a Hash of columns and the key for sorting
440 def sortable_columns
459 def sortable_columns
441 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
460 {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column|
442 h[column.name.to_s] = column.sortable
461 h[column.name.to_s] = column.sortable
443 h
462 h
444 })
463 })
445 end
464 end
446
465
447 def columns
466 def columns
448 # preserve the column_names order
467 # preserve the column_names order
449 (has_default_columns? ? default_columns_names : column_names).collect do |name|
468 (has_default_columns? ? default_columns_names : column_names).collect do |name|
450 available_columns.find { |col| col.name == name }
469 available_columns.find { |col| col.name == name }
451 end.compact
470 end.compact
452 end
471 end
453
472
454 def default_columns_names
473 def default_columns_names
455 @default_columns_names ||= begin
474 @default_columns_names ||= begin
456 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
475 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
457
476
458 project.present? ? default_columns : [:project] | default_columns
477 project.present? ? default_columns : [:project] | default_columns
459 end
478 end
460 end
479 end
461
480
462 def column_names=(names)
481 def column_names=(names)
463 if names
482 if names
464 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
483 names = names.select {|n| n.is_a?(Symbol) || !n.blank? }
465 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
484 names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym }
466 # Set column_names to nil if default columns
485 # Set column_names to nil if default columns
467 if names == default_columns_names
486 if names == default_columns_names
468 names = nil
487 names = nil
469 end
488 end
470 end
489 end
471 write_attribute(:column_names, names)
490 write_attribute(:column_names, names)
472 end
491 end
473
492
474 def has_column?(column)
493 def has_column?(column)
475 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
494 column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column)
476 end
495 end
477
496
478 def has_default_columns?
497 def has_default_columns?
479 column_names.nil? || column_names.empty?
498 column_names.nil? || column_names.empty?
480 end
499 end
481
500
482 def sort_criteria=(arg)
501 def sort_criteria=(arg)
483 c = []
502 c = []
484 if arg.is_a?(Hash)
503 if arg.is_a?(Hash)
485 arg = arg.keys.sort.collect {|k| arg[k]}
504 arg = arg.keys.sort.collect {|k| arg[k]}
486 end
505 end
487 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
506 c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']}
488 write_attribute(:sort_criteria, c)
507 write_attribute(:sort_criteria, c)
489 end
508 end
490
509
491 def sort_criteria
510 def sort_criteria
492 read_attribute(:sort_criteria) || []
511 read_attribute(:sort_criteria) || []
493 end
512 end
494
513
495 def sort_criteria_key(arg)
514 def sort_criteria_key(arg)
496 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
515 sort_criteria && sort_criteria[arg] && sort_criteria[arg].first
497 end
516 end
498
517
499 def sort_criteria_order(arg)
518 def sort_criteria_order(arg)
500 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
519 sort_criteria && sort_criteria[arg] && sort_criteria[arg].last
501 end
520 end
502
521
503 # Returns the SQL sort order that should be prepended for grouping
522 # Returns the SQL sort order that should be prepended for grouping
504 def group_by_sort_order
523 def group_by_sort_order
505 if grouped? && (column = group_by_column)
524 if grouped? && (column = group_by_column)
506 column.sortable.is_a?(Array) ?
525 column.sortable.is_a?(Array) ?
507 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
526 column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') :
508 "#{column.sortable} #{column.default_order}"
527 "#{column.sortable} #{column.default_order}"
509 end
528 end
510 end
529 end
511
530
512 # Returns true if the query is a grouped query
531 # Returns true if the query is a grouped query
513 def grouped?
532 def grouped?
514 !group_by_column.nil?
533 !group_by_column.nil?
515 end
534 end
516
535
517 def group_by_column
536 def group_by_column
518 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
537 groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by}
519 end
538 end
520
539
521 def group_by_statement
540 def group_by_statement
522 group_by_column.try(:groupable)
541 group_by_column.try(:groupable)
523 end
542 end
524
543
525 def project_statement
544 def project_statement
526 project_clauses = []
545 project_clauses = []
527 if project && !project.descendants.active.empty?
546 if project && !project.descendants.active.empty?
528 ids = [project.id]
547 ids = [project.id]
529 if has_filter?("subproject_id")
548 if has_filter?("subproject_id")
530 case operator_for("subproject_id")
549 case operator_for("subproject_id")
531 when '='
550 when '='
532 # include the selected subprojects
551 # include the selected subprojects
533 ids += values_for("subproject_id").each(&:to_i)
552 ids += values_for("subproject_id").each(&:to_i)
534 when '!*'
553 when '!*'
535 # main project only
554 # main project only
536 else
555 else
537 # all subprojects
556 # all subprojects
538 ids += project.descendants.collect(&:id)
557 ids += project.descendants.collect(&:id)
539 end
558 end
540 elsif Setting.display_subprojects_issues?
559 elsif Setting.display_subprojects_issues?
541 ids += project.descendants.collect(&:id)
560 ids += project.descendants.collect(&:id)
542 end
561 end
543 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
562 project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
544 elsif project
563 elsif project
545 project_clauses << "#{Project.table_name}.id = %d" % project.id
564 project_clauses << "#{Project.table_name}.id = %d" % project.id
546 end
565 end
547 project_clauses.any? ? project_clauses.join(' AND ') : nil
566 project_clauses.any? ? project_clauses.join(' AND ') : nil
548 end
567 end
549
568
550 def statement
569 def statement
551 # filters clauses
570 # filters clauses
552 filters_clauses = []
571 filters_clauses = []
553 filters.each_key do |field|
572 filters.each_key do |field|
554 next if field == "subproject_id"
573 next if field == "subproject_id"
555 v = values_for(field).clone
574 v = values_for(field).clone
556 next unless v and !v.empty?
575 next unless v and !v.empty?
557 operator = operator_for(field)
576 operator = operator_for(field)
558
577
559 # "me" value subsitution
578 # "me" value subsitution
560 if %w(assigned_to_id author_id watcher_id).include?(field)
579 if %w(assigned_to_id author_id watcher_id).include?(field)
561 if v.delete("me")
580 if v.delete("me")
562 if User.current.logged?
581 if User.current.logged?
563 v.push(User.current.id.to_s)
582 v.push(User.current.id.to_s)
564 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
583 v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id'
565 else
584 else
566 v.push("0")
585 v.push("0")
567 end
586 end
568 end
587 end
569 end
588 end
570
589
571 if field == 'project_id'
590 if field == 'project_id'
572 if v.delete('mine')
591 if v.delete('mine')
573 v += User.current.memberships.map(&:project_id).map(&:to_s)
592 v += User.current.memberships.map(&:project_id).map(&:to_s)
574 end
593 end
575 end
594 end
576
595
577 if field =~ /cf_(\d+)$/
596 if field =~ /cf_(\d+)$/
578 # custom field
597 # custom field
579 filters_clauses << sql_for_custom_field(field, operator, v, $1)
598 filters_clauses << sql_for_custom_field(field, operator, v, $1)
580 elsif respond_to?("sql_for_#{field}_field")
599 elsif respond_to?("sql_for_#{field}_field")
581 # specific statement
600 # specific statement
582 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
601 filters_clauses << send("sql_for_#{field}_field", field, operator, v)
583 else
602 else
584 # regular field
603 # regular field
585 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
604 filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')'
586 end
605 end
587 end if filters and valid?
606 end if filters and valid?
588
607
589 filters_clauses << project_statement
608 filters_clauses << project_statement
590 filters_clauses.reject!(&:blank?)
609 filters_clauses.reject!(&:blank?)
591
610
592 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
611 filters_clauses.any? ? filters_clauses.join(' AND ') : nil
593 end
612 end
594
613
595 # Returns the issue count
614 # Returns the issue count
596 def issue_count
615 def issue_count
597 Issue.visible.count(:include => [:status, :project], :conditions => statement)
616 Issue.visible.count(:include => [:status, :project], :conditions => statement)
598 rescue ::ActiveRecord::StatementInvalid => e
617 rescue ::ActiveRecord::StatementInvalid => e
599 raise StatementInvalid.new(e.message)
618 raise StatementInvalid.new(e.message)
600 end
619 end
601
620
602 # Returns the issue count by group or nil if query is not grouped
621 # Returns the issue count by group or nil if query is not grouped
603 def issue_count_by_group
622 def issue_count_by_group
604 r = nil
623 r = nil
605 if grouped?
624 if grouped?
606 begin
625 begin
607 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
626 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
608 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
627 r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement)
609 rescue ActiveRecord::RecordNotFound
628 rescue ActiveRecord::RecordNotFound
610 r = {nil => issue_count}
629 r = {nil => issue_count}
611 end
630 end
612 c = group_by_column
631 c = group_by_column
613 if c.is_a?(QueryCustomFieldColumn)
632 if c.is_a?(QueryCustomFieldColumn)
614 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
633 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
615 end
634 end
616 end
635 end
617 r
636 r
618 rescue ::ActiveRecord::StatementInvalid => e
637 rescue ::ActiveRecord::StatementInvalid => e
619 raise StatementInvalid.new(e.message)
638 raise StatementInvalid.new(e.message)
620 end
639 end
621
640
622 # Returns the issues
641 # Returns the issues
623 # Valid options are :order, :offset, :limit, :include, :conditions
642 # Valid options are :order, :offset, :limit, :include, :conditions
624 def issues(options={})
643 def issues(options={})
625 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
644 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
626 order_option = nil if order_option.blank?
645 order_option = nil if order_option.blank?
627
646
628 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
647 issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq,
629 :conditions => statement,
648 :conditions => statement,
630 :order => order_option,
649 :order => order_option,
631 :joins => joins_for_order_statement(order_option),
650 :joins => joins_for_order_statement(order_option),
632 :limit => options[:limit],
651 :limit => options[:limit],
633 :offset => options[:offset]
652 :offset => options[:offset]
634
653
635 if has_column?(:spent_hours)
654 if has_column?(:spent_hours)
636 Issue.load_visible_spent_hours(issues)
655 Issue.load_visible_spent_hours(issues)
637 end
656 end
657 if has_column?(:relations)
658 Issue.load_visible_relations(issues)
659 end
638 issues
660 issues
639 rescue ::ActiveRecord::StatementInvalid => e
661 rescue ::ActiveRecord::StatementInvalid => e
640 raise StatementInvalid.new(e.message)
662 raise StatementInvalid.new(e.message)
641 end
663 end
642
664
643 # Returns the issues ids
665 # Returns the issues ids
644 def issue_ids(options={})
666 def issue_ids(options={})
645 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
667 order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',')
646 order_option = nil if order_option.blank?
668 order_option = nil if order_option.blank?
647
669
648 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
670 Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq,
649 :conditions => statement,
671 :conditions => statement,
650 :order => order_option,
672 :order => order_option,
651 :joins => joins_for_order_statement(order_option),
673 :joins => joins_for_order_statement(order_option),
652 :limit => options[:limit],
674 :limit => options[:limit],
653 :offset => options[:offset]).find_ids
675 :offset => options[:offset]).find_ids
654 rescue ::ActiveRecord::StatementInvalid => e
676 rescue ::ActiveRecord::StatementInvalid => e
655 raise StatementInvalid.new(e.message)
677 raise StatementInvalid.new(e.message)
656 end
678 end
657
679
658 # Returns the journals
680 # Returns the journals
659 # Valid options are :order, :offset, :limit
681 # Valid options are :order, :offset, :limit
660 def journals(options={})
682 def journals(options={})
661 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
683 Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}],
662 :conditions => statement,
684 :conditions => statement,
663 :order => options[:order],
685 :order => options[:order],
664 :limit => options[:limit],
686 :limit => options[:limit],
665 :offset => options[:offset]
687 :offset => options[:offset]
666 rescue ::ActiveRecord::StatementInvalid => e
688 rescue ::ActiveRecord::StatementInvalid => e
667 raise StatementInvalid.new(e.message)
689 raise StatementInvalid.new(e.message)
668 end
690 end
669
691
670 # Returns the versions
692 # Returns the versions
671 # Valid options are :conditions
693 # Valid options are :conditions
672 def versions(options={})
694 def versions(options={})
673 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
695 Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement
674 rescue ::ActiveRecord::StatementInvalid => e
696 rescue ::ActiveRecord::StatementInvalid => e
675 raise StatementInvalid.new(e.message)
697 raise StatementInvalid.new(e.message)
676 end
698 end
677
699
678 def sql_for_watcher_id_field(field, operator, value)
700 def sql_for_watcher_id_field(field, operator, value)
679 db_table = Watcher.table_name
701 db_table = Watcher.table_name
680 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
702 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
681 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
703 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
682 end
704 end
683
705
684 def sql_for_member_of_group_field(field, operator, value)
706 def sql_for_member_of_group_field(field, operator, value)
685 if operator == '*' # Any group
707 if operator == '*' # Any group
686 groups = Group.all
708 groups = Group.all
687 operator = '=' # Override the operator since we want to find by assigned_to
709 operator = '=' # Override the operator since we want to find by assigned_to
688 elsif operator == "!*"
710 elsif operator == "!*"
689 groups = Group.all
711 groups = Group.all
690 operator = '!' # Override the operator since we want to find by assigned_to
712 operator = '!' # Override the operator since we want to find by assigned_to
691 else
713 else
692 groups = Group.find_all_by_id(value)
714 groups = Group.find_all_by_id(value)
693 end
715 end
694 groups ||= []
716 groups ||= []
695
717
696 members_of_groups = groups.inject([]) {|user_ids, group|
718 members_of_groups = groups.inject([]) {|user_ids, group|
697 if group && group.user_ids.present?
719 if group && group.user_ids.present?
698 user_ids << group.user_ids
720 user_ids << group.user_ids
699 end
721 end
700 user_ids.flatten.uniq.compact
722 user_ids.flatten.uniq.compact
701 }.sort.collect(&:to_s)
723 }.sort.collect(&:to_s)
702
724
703 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
725 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
704 end
726 end
705
727
706 def sql_for_assigned_to_role_field(field, operator, value)
728 def sql_for_assigned_to_role_field(field, operator, value)
707 case operator
729 case operator
708 when "*", "!*" # Member / Not member
730 when "*", "!*" # Member / Not member
709 sw = operator == "!*" ? 'NOT' : ''
731 sw = operator == "!*" ? 'NOT' : ''
710 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
732 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
711 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
733 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
712 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
734 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
713 when "=", "!"
735 when "=", "!"
714 role_cond = value.any? ?
736 role_cond = value.any? ?
715 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
737 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" :
716 "1=0"
738 "1=0"
717
739
718 sw = operator == "!" ? 'NOT' : ''
740 sw = operator == "!" ? 'NOT' : ''
719 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
741 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
720 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
742 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
721 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
743 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
722 end
744 end
723 end
745 end
724
746
725 def sql_for_is_private_field(field, operator, value)
747 def sql_for_is_private_field(field, operator, value)
726 op = (operator == "=" ? 'IN' : 'NOT IN')
748 op = (operator == "=" ? 'IN' : 'NOT IN')
727 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
749 va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',')
728
750
729 "#{Issue.table_name}.is_private #{op} (#{va})"
751 "#{Issue.table_name}.is_private #{op} (#{va})"
730 end
752 end
731
753
754 def sql_for_relations(field, operator, value, options={})
755 relation_options = IssueRelation::TYPES[field]
756 return relation_options unless relation_options
757
758 relation_type = field
759 join_column, target_join_column = "issue_from_id", "issue_to_id"
760 if relation_options[:reverse] || options[:reverse]
761 relation_type = relation_options[:reverse] || relation_type
762 join_column, target_join_column = target_join_column, join_column
763 end
764
765 sql = case operator
766 when "*", "!*"
767 op = (operator == "*" ? 'IN' : 'NOT IN')
768 "#{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)}')"
769 when "=", "!"
770 op = (operator == "=" ? 'IN' : 'NOT IN')
771 "#{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})"
772 when "=p", "=!p"
773 op = (operator == "=p" ? '=' : '<>')
774 "#{Issue.table_name}.id IN (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 #{op} #{value.first.to_i})"
775 end
776
777 if relation_options[:sym] == field && !options[:reverse]
778 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
779 sqls.join(["!", "!*"].include?(operator) ? " AND " : " OR ")
780 else
781 sql
782 end
783 end
784
785 IssueRelation::TYPES.keys.each do |relation_type|
786 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
787 end
788
732 private
789 private
733
790
734 def sql_for_custom_field(field, operator, value, custom_field_id)
791 def sql_for_custom_field(field, operator, value, custom_field_id)
735 db_table = CustomValue.table_name
792 db_table = CustomValue.table_name
736 db_field = 'value'
793 db_field = 'value'
737 filter = @available_filters[field]
794 filter = @available_filters[field]
738 return nil unless filter
795 return nil unless filter
739 if filter[:format] == 'user'
796 if filter[:format] == 'user'
740 if value.delete('me')
797 if value.delete('me')
741 value.push User.current.id.to_s
798 value.push User.current.id.to_s
742 end
799 end
743 end
800 end
744 not_in = nil
801 not_in = nil
745 if operator == '!'
802 if operator == '!'
746 # Makes ! operator work for custom fields with multiple values
803 # Makes ! operator work for custom fields with multiple values
747 operator = '='
804 operator = '='
748 not_in = 'NOT'
805 not_in = 'NOT'
749 end
806 end
750 customized_key = "id"
807 customized_key = "id"
751 customized_class = Issue
808 customized_class = Issue
752 if field =~ /^(.+)\.cf_/
809 if field =~ /^(.+)\.cf_/
753 assoc = $1
810 assoc = $1
754 customized_key = "#{assoc}_id"
811 customized_key = "#{assoc}_id"
755 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
812 customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil
756 raise "Unknown Issue association #{assoc}" unless customized_class
813 raise "Unknown Issue association #{assoc}" unless customized_class
757 end
814 end
758 "#{Issue.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 " +
815 "#{Issue.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 " +
759 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
816 sql_for_field(field, operator, value, db_table, db_field, true) + ')'
760 end
817 end
761
818
762 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
819 # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+
763 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
820 def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false)
764 sql = ''
821 sql = ''
765 case operator
822 case operator
766 when "="
823 when "="
767 if value.any?
824 if value.any?
768 case type_for(field)
825 case type_for(field)
769 when :date, :date_past
826 when :date, :date_past
770 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
827 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil))
771 when :integer
828 when :integer
772 if is_custom_filter
829 if is_custom_filter
773 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
830 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})"
774 else
831 else
775 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
832 sql = "#{db_table}.#{db_field} = #{value.first.to_i}"
776 end
833 end
777 when :float
834 when :float
778 if is_custom_filter
835 if is_custom_filter
779 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
836 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5})"
780 else
837 else
781 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
838 sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}"
782 end
839 end
783 else
840 else
784 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
841 sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")"
785 end
842 end
786 else
843 else
787 # IN an empty set
844 # IN an empty set
788 sql = "1=0"
845 sql = "1=0"
789 end
846 end
790 when "!"
847 when "!"
791 if value.any?
848 if value.any?
792 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
849 sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))"
793 else
850 else
794 # NOT IN an empty set
851 # NOT IN an empty set
795 sql = "1=1"
852 sql = "1=1"
796 end
853 end
797 when "!*"
854 when "!*"
798 sql = "#{db_table}.#{db_field} IS NULL"
855 sql = "#{db_table}.#{db_field} IS NULL"
799 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
856 sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter
800 when "*"
857 when "*"
801 sql = "#{db_table}.#{db_field} IS NOT NULL"
858 sql = "#{db_table}.#{db_field} IS NOT NULL"
802 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
859 sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter
803 when ">="
860 when ">="
804 if [:date, :date_past].include?(type_for(field))
861 if [:date, :date_past].include?(type_for(field))
805 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
862 sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil)
806 else
863 else
807 if is_custom_filter
864 if is_custom_filter
808 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
865 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})"
809 else
866 else
810 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
867 sql = "#{db_table}.#{db_field} >= #{value.first.to_f}"
811 end
868 end
812 end
869 end
813 when "<="
870 when "<="
814 if [:date, :date_past].include?(type_for(field))
871 if [:date, :date_past].include?(type_for(field))
815 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
872 sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil))
816 else
873 else
817 if is_custom_filter
874 if is_custom_filter
818 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
875 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})"
819 else
876 else
820 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
877 sql = "#{db_table}.#{db_field} <= #{value.first.to_f}"
821 end
878 end
822 end
879 end
823 when "><"
880 when "><"
824 if [:date, :date_past].include?(type_for(field))
881 if [:date, :date_past].include?(type_for(field))
825 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
882 sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil))
826 else
883 else
827 if is_custom_filter
884 if is_custom_filter
828 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
885 sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) BETWEEN #{value[0].to_f} AND #{value[1].to_f})"
829 else
886 else
830 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
887 sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}"
831 end
888 end
832 end
889 end
833 when "o"
890 when "o"
834 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
891 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_false})" if field == "status_id"
835 when "c"
892 when "c"
836 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
893 sql = "#{Issue.table_name}.status_id IN (SELECT id FROM #{IssueStatus.table_name} WHERE is_closed=#{connection.quoted_true})" if field == "status_id"
837 when ">t-"
894 when ">t-"
838 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
895 sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0)
839 when "<t-"
896 when "<t-"
840 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
897 sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i)
841 when "t-"
898 when "t-"
842 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
899 sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i)
843 when ">t+"
900 when ">t+"
844 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
901 sql = relative_date_clause(db_table, db_field, value.first.to_i, nil)
845 when "<t+"
902 when "<t+"
846 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
903 sql = relative_date_clause(db_table, db_field, 0, value.first.to_i)
847 when "t+"
904 when "t+"
848 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
905 sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i)
849 when "t"
906 when "t"
850 sql = relative_date_clause(db_table, db_field, 0, 0)
907 sql = relative_date_clause(db_table, db_field, 0, 0)
851 when "w"
908 when "w"
852 first_day_of_week = l(:general_first_day_of_week).to_i
909 first_day_of_week = l(:general_first_day_of_week).to_i
853 day_of_week = Date.today.cwday
910 day_of_week = Date.today.cwday
854 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
911 days_ago = (day_of_week >= first_day_of_week ? day_of_week - first_day_of_week : day_of_week + 7 - first_day_of_week)
855 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
912 sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6)
856 when "~"
913 when "~"
857 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
914 sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
858 when "!~"
915 when "!~"
859 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
916 sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'"
860 else
917 else
861 raise "Unknown query operator #{operator}"
918 raise "Unknown query operator #{operator}"
862 end
919 end
863
920
864 return sql
921 return sql
865 end
922 end
866
923
867 def add_custom_fields_filters(custom_fields, assoc=nil)
924 def add_custom_fields_filters(custom_fields, assoc=nil)
868 return unless custom_fields.present?
925 return unless custom_fields.present?
869 @available_filters ||= {}
926 @available_filters ||= {}
870
927
871 custom_fields.select(&:is_filter?).each do |field|
928 custom_fields.select(&:is_filter?).each do |field|
872 case field.field_format
929 case field.field_format
873 when "text"
930 when "text"
874 options = { :type => :text, :order => 20 }
931 options = { :type => :text, :order => 20 }
875 when "list"
932 when "list"
876 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
933 options = { :type => :list_optional, :values => field.possible_values, :order => 20}
877 when "date"
934 when "date"
878 options = { :type => :date, :order => 20 }
935 options = { :type => :date, :order => 20 }
879 when "bool"
936 when "bool"
880 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
937 options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 }
881 when "int"
938 when "int"
882 options = { :type => :integer, :order => 20 }
939 options = { :type => :integer, :order => 20 }
883 when "float"
940 when "float"
884 options = { :type => :float, :order => 20 }
941 options = { :type => :float, :order => 20 }
885 when "user", "version"
942 when "user", "version"
886 next unless project
943 next unless project
887 values = field.possible_values_options(project)
944 values = field.possible_values_options(project)
888 if User.current.logged? && field.field_format == 'user'
945 if User.current.logged? && field.field_format == 'user'
889 values.unshift ["<< #{l(:label_me)} >>", "me"]
946 values.unshift ["<< #{l(:label_me)} >>", "me"]
890 end
947 end
891 options = { :type => :list_optional, :values => values, :order => 20}
948 options = { :type => :list_optional, :values => values, :order => 20}
892 else
949 else
893 options = { :type => :string, :order => 20 }
950 options = { :type => :string, :order => 20 }
894 end
951 end
895 filter_id = "cf_#{field.id}"
952 filter_id = "cf_#{field.id}"
896 filter_name = field.name
953 filter_name = field.name
897 if assoc.present?
954 if assoc.present?
898 filter_id = "#{assoc}.#{filter_id}"
955 filter_id = "#{assoc}.#{filter_id}"
899 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
956 filter_name = l("label_attribute_of_#{assoc}", :name => filter_name)
900 end
957 end
901 @available_filters[filter_id] = options.merge({ :name => filter_name, :format => field.field_format })
958 @available_filters[filter_id] = options.merge({ :name => filter_name, :format => field.field_format })
902 end
959 end
903 end
960 end
904
961
905 def add_associations_custom_fields_filters(*associations)
962 def add_associations_custom_fields_filters(*associations)
906 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
963 fields_by_class = CustomField.where(:is_filter => true).group_by(&:class)
907 associations.each do |assoc|
964 associations.each do |assoc|
908 association_klass = Issue.reflect_on_association(assoc).klass
965 association_klass = Issue.reflect_on_association(assoc).klass
909 fields_by_class.each do |field_class, fields|
966 fields_by_class.each do |field_class, fields|
910 if field_class.customized_class <= association_klass
967 if field_class.customized_class <= association_klass
911 add_custom_fields_filters(fields, assoc)
968 add_custom_fields_filters(fields, assoc)
912 end
969 end
913 end
970 end
914 end
971 end
915 end
972 end
916
973
917 # Returns a SQL clause for a date or datetime field.
974 # Returns a SQL clause for a date or datetime field.
918 def date_clause(table, field, from, to)
975 def date_clause(table, field, from, to)
919 s = []
976 s = []
920 if from
977 if from
921 from_yesterday = from - 1
978 from_yesterday = from - 1
922 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
979 from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day)
923 if self.class.default_timezone == :utc
980 if self.class.default_timezone == :utc
924 from_yesterday_time = from_yesterday_time.utc
981 from_yesterday_time = from_yesterday_time.utc
925 end
982 end
926 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
983 s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)])
927 end
984 end
928 if to
985 if to
929 to_time = Time.local(to.year, to.month, to.day)
986 to_time = Time.local(to.year, to.month, to.day)
930 if self.class.default_timezone == :utc
987 if self.class.default_timezone == :utc
931 to_time = to_time.utc
988 to_time = to_time.utc
932 end
989 end
933 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
990 s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)])
934 end
991 end
935 s.join(' AND ')
992 s.join(' AND ')
936 end
993 end
937
994
938 # Returns a SQL clause for a date or datetime field using relative dates.
995 # Returns a SQL clause for a date or datetime field using relative dates.
939 def relative_date_clause(table, field, days_from, days_to)
996 def relative_date_clause(table, field, days_from, days_to)
940 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
997 date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil))
941 end
998 end
942
999
943 # Additional joins required for the given sort options
1000 # Additional joins required for the given sort options
944 def joins_for_order_statement(order_options)
1001 def joins_for_order_statement(order_options)
945 joins = []
1002 joins = []
946
1003
947 if order_options
1004 if order_options
948 if order_options.include?('authors')
1005 if order_options.include?('authors')
949 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
1006 joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id"
950 end
1007 end
951 order_options.scan(/cf_\d+/).uniq.each do |name|
1008 order_options.scan(/cf_\d+/).uniq.each do |name|
952 column = available_columns.detect {|c| c.name.to_s == name}
1009 column = available_columns.detect {|c| c.name.to_s == name}
953 join = column && column.custom_field.join_for_order_statement
1010 join = column && column.custom_field.join_for_order_statement
954 if join
1011 if join
955 joins << join
1012 joins << join
956 end
1013 end
957 end
1014 end
958 end
1015 end
959
1016
960 joins.any? ? joins.join(' ') : nil
1017 joins.any? ? joins.join(' ') : nil
961 end
1018 end
962 end
1019 end
@@ -1,27 +1,28
1 <%= javascript_tag do %>
1 <%= javascript_tag do %>
2 var operatorLabels = <%= raw_json Query.operators_labels %>;
2 var operatorLabels = <%= raw_json Query.operators_labels %>;
3 var operatorByType = <%= raw_json Query.operators_by_filter_type %>;
3 var operatorByType = <%= raw_json Query.operators_by_filter_type %>;
4 var availableFilters = <%= raw_json query.available_filters_as_json %>;
4 var availableFilters = <%= raw_json query.available_filters_as_json %>;
5 var labelDayPlural = <%= raw_json l(:label_day_plural) %>;
5 var labelDayPlural = <%= raw_json l(:label_day_plural) %>;
6 var allProjects = <%= raw query.all_projects_values.to_json %>;
6 $(document).ready(function(){
7 $(document).ready(function(){
7 initFilters();
8 initFilters();
8 <% query.filters.each do |field, options| %>
9 <% query.filters.each do |field, options| %>
9 addFilter("<%= field %>", <%= raw_json query.operator_for(field) %>, <%= raw_json query.values_for(field) %>);
10 addFilter("<%= field %>", <%= raw_json query.operator_for(field) %>, <%= raw_json query.values_for(field) %>);
10 <% end %>
11 <% end %>
11 });
12 });
12 <% end %>
13 <% end %>
13
14
14 <table style="width:100%">
15 <table style="width:100%">
15 <tr>
16 <tr>
16 <td>
17 <td>
17 <table id="filters-table">
18 <table id="filters-table">
18 </table>
19 </table>
19 </td>
20 </td>
20 <td class="add-filter">
21 <td class="add-filter">
21 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
22 <%= label_tag('add_filter_select', l(:label_filter_add)) %>
22 <%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %>
23 <%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %>
23 </td>
24 </td>
24 </tr>
25 </tr>
25 </table>
26 </table>
26 <%= hidden_field_tag 'f[]', '' %>
27 <%= hidden_field_tag 'f[]', '' %>
27 <% include_calendar_headers_tags %>
28 <% include_calendar_headers_tags %>
@@ -1,1064 +1,1066
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
132
133 actionview_instancetag_blank_option: Please select
133 actionview_instancetag_blank_option: Please select
134
134
135 general_text_No: 'No'
135 general_text_No: 'No'
136 general_text_Yes: 'Yes'
136 general_text_Yes: 'Yes'
137 general_text_no: 'no'
137 general_text_no: 'no'
138 general_text_yes: 'yes'
138 general_text_yes: 'yes'
139 general_lang_name: 'English'
139 general_lang_name: 'English'
140 general_csv_separator: ','
140 general_csv_separator: ','
141 general_csv_decimal_separator: '.'
141 general_csv_decimal_separator: '.'
142 general_csv_encoding: ISO-8859-1
142 general_csv_encoding: ISO-8859-1
143 general_pdf_encoding: UTF-8
143 general_pdf_encoding: UTF-8
144 general_first_day_of_week: '7'
144 general_first_day_of_week: '7'
145
145
146 notice_account_updated: Account was successfully updated.
146 notice_account_updated: Account was successfully updated.
147 notice_account_invalid_creditentials: Invalid user or password
147 notice_account_invalid_creditentials: Invalid user or password
148 notice_account_password_updated: Password was successfully updated.
148 notice_account_password_updated: Password was successfully updated.
149 notice_account_wrong_password: Wrong password
149 notice_account_wrong_password: Wrong password
150 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
150 notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you.
151 notice_account_unknown_email: Unknown user.
151 notice_account_unknown_email: Unknown user.
152 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
152 notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password.
153 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
153 notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you.
154 notice_account_activated: Your account has been activated. You can now log in.
154 notice_account_activated: Your account has been activated. You can now log in.
155 notice_successful_create: Successful creation.
155 notice_successful_create: Successful creation.
156 notice_successful_update: Successful update.
156 notice_successful_update: Successful update.
157 notice_successful_delete: Successful deletion.
157 notice_successful_delete: Successful deletion.
158 notice_successful_connection: Successful connection.
158 notice_successful_connection: Successful connection.
159 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
159 notice_file_not_found: The page you were trying to access doesn't exist or has been removed.
160 notice_locking_conflict: Data has been updated by another user.
160 notice_locking_conflict: Data has been updated by another user.
161 notice_not_authorized: You are not authorized to access this page.
161 notice_not_authorized: You are not authorized to access this page.
162 notice_not_authorized_archived_project: The project you're trying to access has been archived.
162 notice_not_authorized_archived_project: The project you're trying to access has been archived.
163 notice_email_sent: "An email was sent to %{value}"
163 notice_email_sent: "An email was sent to %{value}"
164 notice_email_error: "An error occurred while sending mail (%{value})"
164 notice_email_error: "An error occurred while sending mail (%{value})"
165 notice_feeds_access_key_reseted: Your RSS access key was reset.
165 notice_feeds_access_key_reseted: Your RSS access key was reset.
166 notice_api_access_key_reseted: Your API access key was reset.
166 notice_api_access_key_reseted: Your API access key was reset.
167 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
167 notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}."
168 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
168 notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}."
169 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
169 notice_failed_to_save_members: "Failed to save member(s): %{errors}."
170 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
170 notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit."
171 notice_account_pending: "Your account was created and is now pending administrator approval."
171 notice_account_pending: "Your account was created and is now pending administrator approval."
172 notice_default_data_loaded: Default configuration successfully loaded.
172 notice_default_data_loaded: Default configuration successfully loaded.
173 notice_unable_delete_version: Unable to delete version.
173 notice_unable_delete_version: Unable to delete version.
174 notice_unable_delete_time_entry: Unable to delete time log entry.
174 notice_unable_delete_time_entry: Unable to delete time log entry.
175 notice_issue_done_ratios_updated: Issue done ratios updated.
175 notice_issue_done_ratios_updated: Issue done ratios updated.
176 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
176 notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})"
177 notice_issue_successful_create: "Issue %{id} created."
177 notice_issue_successful_create: "Issue %{id} created."
178 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
178 notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it."
179 notice_account_deleted: "Your account has been permanently deleted."
179 notice_account_deleted: "Your account has been permanently deleted."
180 notice_user_successful_create: "User %{id} created."
180 notice_user_successful_create: "User %{id} created."
181
181
182 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
182 error_can_t_load_default_data: "Default configuration could not be loaded: %{value}"
183 error_scm_not_found: "The entry or revision was not found in the repository."
183 error_scm_not_found: "The entry or revision was not found in the repository."
184 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
184 error_scm_command_failed: "An error occurred when trying to access the repository: %{value}"
185 error_scm_annotate: "The entry does not exist or cannot be annotated."
185 error_scm_annotate: "The entry does not exist or cannot be annotated."
186 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
186 error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size."
187 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
187 error_issue_not_found_in_project: 'The issue was not found or does not belong to this project'
188 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
188 error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.'
189 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
189 error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").'
190 error_can_not_delete_custom_field: Unable to delete custom field
190 error_can_not_delete_custom_field: Unable to delete custom field
191 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
191 error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted."
192 error_can_not_remove_role: "This role is in use and cannot be deleted."
192 error_can_not_remove_role: "This role is in use and cannot be deleted."
193 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
193 error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened'
194 error_can_not_archive_project: This project cannot be archived
194 error_can_not_archive_project: This project cannot be archived
195 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
195 error_issue_done_ratios_not_updated: "Issue done ratios not updated."
196 error_workflow_copy_source: 'Please select a source tracker or role'
196 error_workflow_copy_source: 'Please select a source tracker or role'
197 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
197 error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
198 error_unable_delete_issue_status: 'Unable to delete issue status'
198 error_unable_delete_issue_status: 'Unable to delete issue status'
199 error_unable_to_connect: "Unable to connect (%{value})"
199 error_unable_to_connect: "Unable to connect (%{value})"
200 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
200 error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})"
201 error_session_expired: "Your session has expired. Please login again."
201 error_session_expired: "Your session has expired. Please login again."
202 warning_attachments_not_saved: "%{count} file(s) could not be saved."
202 warning_attachments_not_saved: "%{count} file(s) could not be saved."
203
203
204 mail_subject_lost_password: "Your %{value} password"
204 mail_subject_lost_password: "Your %{value} password"
205 mail_body_lost_password: 'To change your password, click on the following link:'
205 mail_body_lost_password: 'To change your password, click on the following link:'
206 mail_subject_register: "Your %{value} account activation"
206 mail_subject_register: "Your %{value} account activation"
207 mail_body_register: 'To activate your account, click on the following link:'
207 mail_body_register: 'To activate your account, click on the following link:'
208 mail_body_account_information_external: "You can use your %{value} account to log in."
208 mail_body_account_information_external: "You can use your %{value} account to log in."
209 mail_body_account_information: Your account information
209 mail_body_account_information: Your account information
210 mail_subject_account_activation_request: "%{value} account activation request"
210 mail_subject_account_activation_request: "%{value} account activation request"
211 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
211 mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:"
212 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
212 mail_subject_reminder: "%{count} issue(s) due in the next %{days} days"
213 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
213 mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:"
214 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
214 mail_subject_wiki_content_added: "'%{id}' wiki page has been added"
215 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
215 mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}."
216 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
216 mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated"
217 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
217 mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}."
218
218
219 gui_validation_error: 1 error
219 gui_validation_error: 1 error
220 gui_validation_error_plural: "%{count} errors"
220 gui_validation_error_plural: "%{count} errors"
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_field_format: Format
235 field_field_format: Format
236 field_is_for_all: For all projects
236 field_is_for_all: For all projects
237 field_possible_values: Possible values
237 field_possible_values: Possible values
238 field_regexp: Regular expression
238 field_regexp: Regular expression
239 field_min_length: Minimum length
239 field_min_length: Minimum length
240 field_max_length: Maximum length
240 field_max_length: Maximum length
241 field_value: Value
241 field_value: Value
242 field_category: Category
242 field_category: Category
243 field_title: Title
243 field_title: Title
244 field_project: Project
244 field_project: Project
245 field_issue: Issue
245 field_issue: Issue
246 field_status: Status
246 field_status: Status
247 field_notes: Notes
247 field_notes: Notes
248 field_is_closed: Issue closed
248 field_is_closed: Issue closed
249 field_is_default: Default value
249 field_is_default: Default value
250 field_tracker: Tracker
250 field_tracker: Tracker
251 field_subject: Subject
251 field_subject: Subject
252 field_due_date: Due date
252 field_due_date: Due date
253 field_assigned_to: Assignee
253 field_assigned_to: Assignee
254 field_priority: Priority
254 field_priority: Priority
255 field_fixed_version: Target version
255 field_fixed_version: Target version
256 field_user: User
256 field_user: User
257 field_principal: Principal
257 field_principal: Principal
258 field_role: Role
258 field_role: Role
259 field_homepage: Homepage
259 field_homepage: Homepage
260 field_is_public: Public
260 field_is_public: Public
261 field_parent: Subproject of
261 field_parent: Subproject of
262 field_is_in_roadmap: Issues displayed in roadmap
262 field_is_in_roadmap: Issues displayed in roadmap
263 field_login: Login
263 field_login: Login
264 field_mail_notification: Email notifications
264 field_mail_notification: Email notifications
265 field_admin: Administrator
265 field_admin: Administrator
266 field_last_login_on: Last connection
266 field_last_login_on: Last connection
267 field_language: Language
267 field_language: Language
268 field_effective_date: Date
268 field_effective_date: Date
269 field_password: Password
269 field_password: Password
270 field_new_password: New password
270 field_new_password: New password
271 field_password_confirmation: Confirmation
271 field_password_confirmation: Confirmation
272 field_version: Version
272 field_version: Version
273 field_type: Type
273 field_type: Type
274 field_host: Host
274 field_host: Host
275 field_port: Port
275 field_port: Port
276 field_account: Account
276 field_account: Account
277 field_base_dn: Base DN
277 field_base_dn: Base DN
278 field_attr_login: Login attribute
278 field_attr_login: Login attribute
279 field_attr_firstname: Firstname attribute
279 field_attr_firstname: Firstname attribute
280 field_attr_lastname: Lastname attribute
280 field_attr_lastname: Lastname attribute
281 field_attr_mail: Email attribute
281 field_attr_mail: Email attribute
282 field_onthefly: On-the-fly user creation
282 field_onthefly: On-the-fly user creation
283 field_start_date: Start date
283 field_start_date: Start date
284 field_done_ratio: "% Done"
284 field_done_ratio: "% Done"
285 field_auth_source: Authentication mode
285 field_auth_source: Authentication mode
286 field_hide_mail: Hide my email address
286 field_hide_mail: Hide my email address
287 field_comments: Comment
287 field_comments: Comment
288 field_url: URL
288 field_url: URL
289 field_start_page: Start page
289 field_start_page: Start page
290 field_subproject: Subproject
290 field_subproject: Subproject
291 field_hours: Hours
291 field_hours: Hours
292 field_activity: Activity
292 field_activity: Activity
293 field_spent_on: Date
293 field_spent_on: Date
294 field_identifier: Identifier
294 field_identifier: Identifier
295 field_is_filter: Used as a filter
295 field_is_filter: Used as a filter
296 field_issue_to: Related issue
296 field_issue_to: Related issue
297 field_delay: Delay
297 field_delay: Delay
298 field_assignable: Issues can be assigned to this role
298 field_assignable: Issues can be assigned to this role
299 field_redirect_existing_links: Redirect existing links
299 field_redirect_existing_links: Redirect existing links
300 field_estimated_hours: Estimated time
300 field_estimated_hours: Estimated time
301 field_column_names: Columns
301 field_column_names: Columns
302 field_time_entries: Log time
302 field_time_entries: Log time
303 field_time_zone: Time zone
303 field_time_zone: Time zone
304 field_searchable: Searchable
304 field_searchable: Searchable
305 field_default_value: Default value
305 field_default_value: Default value
306 field_comments_sorting: Display comments
306 field_comments_sorting: Display comments
307 field_parent_title: Parent page
307 field_parent_title: Parent page
308 field_editable: Editable
308 field_editable: Editable
309 field_watcher: Watcher
309 field_watcher: Watcher
310 field_identity_url: OpenID URL
310 field_identity_url: OpenID URL
311 field_content: Content
311 field_content: Content
312 field_group_by: Group results by
312 field_group_by: Group results by
313 field_sharing: Sharing
313 field_sharing: Sharing
314 field_parent_issue: Parent task
314 field_parent_issue: Parent task
315 field_member_of_group: "Assignee's group"
315 field_member_of_group: "Assignee's group"
316 field_assigned_to_role: "Assignee's role"
316 field_assigned_to_role: "Assignee's role"
317 field_text: Text field
317 field_text: Text field
318 field_visible: Visible
318 field_visible: Visible
319 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
319 field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text"
320 field_issues_visibility: Issues visibility
320 field_issues_visibility: Issues visibility
321 field_is_private: Private
321 field_is_private: Private
322 field_commit_logs_encoding: Commit messages encoding
322 field_commit_logs_encoding: Commit messages encoding
323 field_scm_path_encoding: Path encoding
323 field_scm_path_encoding: Path encoding
324 field_path_to_repository: Path to repository
324 field_path_to_repository: Path to repository
325 field_root_directory: Root directory
325 field_root_directory: Root directory
326 field_cvsroot: CVSROOT
326 field_cvsroot: CVSROOT
327 field_cvs_module: Module
327 field_cvs_module: Module
328 field_repository_is_default: Main repository
328 field_repository_is_default: Main repository
329 field_multiple: Multiple values
329 field_multiple: Multiple values
330 field_auth_source_ldap_filter: LDAP filter
330 field_auth_source_ldap_filter: LDAP filter
331 field_core_fields: Standard fields
331 field_core_fields: Standard fields
332 field_timeout: "Timeout (in seconds)"
332 field_timeout: "Timeout (in seconds)"
333 field_board_parent: Parent forum
333 field_board_parent: Parent forum
334
334
335 setting_app_title: Application title
335 setting_app_title: Application title
336 setting_app_subtitle: Application subtitle
336 setting_app_subtitle: Application subtitle
337 setting_welcome_text: Welcome text
337 setting_welcome_text: Welcome text
338 setting_default_language: Default language
338 setting_default_language: Default language
339 setting_login_required: Authentication required
339 setting_login_required: Authentication required
340 setting_self_registration: Self-registration
340 setting_self_registration: Self-registration
341 setting_attachment_max_size: Maximum attachment size
341 setting_attachment_max_size: Maximum attachment size
342 setting_issues_export_limit: Issues export limit
342 setting_issues_export_limit: Issues export limit
343 setting_mail_from: Emission email address
343 setting_mail_from: Emission email address
344 setting_bcc_recipients: Blind carbon copy recipients (bcc)
344 setting_bcc_recipients: Blind carbon copy recipients (bcc)
345 setting_plain_text_mail: Plain text mail (no HTML)
345 setting_plain_text_mail: Plain text mail (no HTML)
346 setting_host_name: Host name and path
346 setting_host_name: Host name and path
347 setting_text_formatting: Text formatting
347 setting_text_formatting: Text formatting
348 setting_wiki_compression: Wiki history compression
348 setting_wiki_compression: Wiki history compression
349 setting_feeds_limit: Maximum number of items in Atom feeds
349 setting_feeds_limit: Maximum number of items in Atom feeds
350 setting_default_projects_public: New projects are public by default
350 setting_default_projects_public: New projects are public by default
351 setting_autofetch_changesets: Fetch commits automatically
351 setting_autofetch_changesets: Fetch commits automatically
352 setting_sys_api_enabled: Enable WS for repository management
352 setting_sys_api_enabled: Enable WS for repository management
353 setting_commit_ref_keywords: Referencing keywords
353 setting_commit_ref_keywords: Referencing keywords
354 setting_commit_fix_keywords: Fixing keywords
354 setting_commit_fix_keywords: Fixing keywords
355 setting_autologin: Autologin
355 setting_autologin: Autologin
356 setting_date_format: Date format
356 setting_date_format: Date format
357 setting_time_format: Time format
357 setting_time_format: Time format
358 setting_cross_project_issue_relations: Allow cross-project issue relations
358 setting_cross_project_issue_relations: Allow cross-project issue relations
359 setting_issue_list_default_columns: Default columns displayed on the issue list
359 setting_issue_list_default_columns: Default columns displayed on the issue list
360 setting_repositories_encodings: Attachments and repositories encodings
360 setting_repositories_encodings: Attachments and repositories encodings
361 setting_emails_header: Emails header
361 setting_emails_header: Emails header
362 setting_emails_footer: Emails footer
362 setting_emails_footer: Emails footer
363 setting_protocol: Protocol
363 setting_protocol: Protocol
364 setting_per_page_options: Objects per page options
364 setting_per_page_options: Objects per page options
365 setting_user_format: Users display format
365 setting_user_format: Users display format
366 setting_activity_days_default: Days displayed on project activity
366 setting_activity_days_default: Days displayed on project activity
367 setting_display_subprojects_issues: Display subprojects issues on main projects by default
367 setting_display_subprojects_issues: Display subprojects issues on main projects by default
368 setting_enabled_scm: Enabled SCM
368 setting_enabled_scm: Enabled SCM
369 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
369 setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
370 setting_mail_handler_api_enabled: Enable WS for incoming emails
370 setting_mail_handler_api_enabled: Enable WS for incoming emails
371 setting_mail_handler_api_key: API key
371 setting_mail_handler_api_key: API key
372 setting_sequential_project_identifiers: Generate sequential project identifiers
372 setting_sequential_project_identifiers: Generate sequential project identifiers
373 setting_gravatar_enabled: Use Gravatar user icons
373 setting_gravatar_enabled: Use Gravatar user icons
374 setting_gravatar_default: Default Gravatar image
374 setting_gravatar_default: Default Gravatar image
375 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
375 setting_diff_max_lines_displayed: Maximum number of diff lines displayed
376 setting_file_max_size_displayed: Maximum size of text files displayed inline
376 setting_file_max_size_displayed: Maximum size of text files displayed inline
377 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
377 setting_repository_log_display_limit: Maximum number of revisions displayed on file log
378 setting_openid: Allow OpenID login and registration
378 setting_openid: Allow OpenID login and registration
379 setting_password_min_length: Minimum password length
379 setting_password_min_length: Minimum password length
380 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
380 setting_new_project_user_role_id: Role given to a non-admin user who creates a project
381 setting_default_projects_modules: Default enabled modules for new projects
381 setting_default_projects_modules: Default enabled modules for new projects
382 setting_issue_done_ratio: Calculate the issue done ratio with
382 setting_issue_done_ratio: Calculate the issue done ratio with
383 setting_issue_done_ratio_issue_field: Use the issue field
383 setting_issue_done_ratio_issue_field: Use the issue field
384 setting_issue_done_ratio_issue_status: Use the issue status
384 setting_issue_done_ratio_issue_status: Use the issue status
385 setting_start_of_week: Start calendars on
385 setting_start_of_week: Start calendars on
386 setting_rest_api_enabled: Enable REST web service
386 setting_rest_api_enabled: Enable REST web service
387 setting_cache_formatted_text: Cache formatted text
387 setting_cache_formatted_text: Cache formatted text
388 setting_default_notification_option: Default notification option
388 setting_default_notification_option: Default notification option
389 setting_commit_logtime_enabled: Enable time logging
389 setting_commit_logtime_enabled: Enable time logging
390 setting_commit_logtime_activity_id: Activity for logged time
390 setting_commit_logtime_activity_id: Activity for logged time
391 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
391 setting_gantt_items_limit: Maximum number of items displayed on the gantt chart
392 setting_issue_group_assignment: Allow issue assignment to groups
392 setting_issue_group_assignment: Allow issue assignment to groups
393 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
393 setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues
394 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
394 setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed
395 setting_unsubscribe: Allow users to delete their own account
395 setting_unsubscribe: Allow users to delete their own account
396 setting_session_lifetime: Session maximum lifetime
396 setting_session_lifetime: Session maximum lifetime
397 setting_session_timeout: Session inactivity timeout
397 setting_session_timeout: Session inactivity timeout
398 setting_thumbnails_enabled: Display attachment thumbnails
398 setting_thumbnails_enabled: Display attachment thumbnails
399 setting_thumbnails_size: Thumbnails size (in pixels)
399 setting_thumbnails_size: Thumbnails size (in pixels)
400
400
401 permission_add_project: Create project
401 permission_add_project: Create project
402 permission_add_subprojects: Create subprojects
402 permission_add_subprojects: Create subprojects
403 permission_edit_project: Edit project
403 permission_edit_project: Edit project
404 permission_close_project: Close / reopen the project
404 permission_close_project: Close / reopen the project
405 permission_select_project_modules: Select project modules
405 permission_select_project_modules: Select project modules
406 permission_manage_members: Manage members
406 permission_manage_members: Manage members
407 permission_manage_project_activities: Manage project activities
407 permission_manage_project_activities: Manage project activities
408 permission_manage_versions: Manage versions
408 permission_manage_versions: Manage versions
409 permission_manage_categories: Manage issue categories
409 permission_manage_categories: Manage issue categories
410 permission_view_issues: View Issues
410 permission_view_issues: View Issues
411 permission_add_issues: Add issues
411 permission_add_issues: Add issues
412 permission_edit_issues: Edit issues
412 permission_edit_issues: Edit issues
413 permission_manage_issue_relations: Manage issue relations
413 permission_manage_issue_relations: Manage issue relations
414 permission_set_issues_private: Set issues public or private
414 permission_set_issues_private: Set issues public or private
415 permission_set_own_issues_private: Set own issues public or private
415 permission_set_own_issues_private: Set own issues public or private
416 permission_add_issue_notes: Add notes
416 permission_add_issue_notes: Add notes
417 permission_edit_issue_notes: Edit notes
417 permission_edit_issue_notes: Edit notes
418 permission_edit_own_issue_notes: Edit own notes
418 permission_edit_own_issue_notes: Edit own notes
419 permission_move_issues: Move issues
419 permission_move_issues: Move issues
420 permission_delete_issues: Delete issues
420 permission_delete_issues: Delete issues
421 permission_manage_public_queries: Manage public queries
421 permission_manage_public_queries: Manage public queries
422 permission_save_queries: Save queries
422 permission_save_queries: Save queries
423 permission_view_gantt: View gantt chart
423 permission_view_gantt: View gantt chart
424 permission_view_calendar: View calendar
424 permission_view_calendar: View calendar
425 permission_view_issue_watchers: View watchers list
425 permission_view_issue_watchers: View watchers list
426 permission_add_issue_watchers: Add watchers
426 permission_add_issue_watchers: Add watchers
427 permission_delete_issue_watchers: Delete watchers
427 permission_delete_issue_watchers: Delete watchers
428 permission_log_time: Log spent time
428 permission_log_time: Log spent time
429 permission_view_time_entries: View spent time
429 permission_view_time_entries: View spent time
430 permission_edit_time_entries: Edit time logs
430 permission_edit_time_entries: Edit time logs
431 permission_edit_own_time_entries: Edit own time logs
431 permission_edit_own_time_entries: Edit own time logs
432 permission_manage_news: Manage news
432 permission_manage_news: Manage news
433 permission_comment_news: Comment news
433 permission_comment_news: Comment news
434 permission_manage_documents: Manage documents
434 permission_manage_documents: Manage documents
435 permission_view_documents: View documents
435 permission_view_documents: View documents
436 permission_manage_files: Manage files
436 permission_manage_files: Manage files
437 permission_view_files: View files
437 permission_view_files: View files
438 permission_manage_wiki: Manage wiki
438 permission_manage_wiki: Manage wiki
439 permission_rename_wiki_pages: Rename wiki pages
439 permission_rename_wiki_pages: Rename wiki pages
440 permission_delete_wiki_pages: Delete wiki pages
440 permission_delete_wiki_pages: Delete wiki pages
441 permission_view_wiki_pages: View wiki
441 permission_view_wiki_pages: View wiki
442 permission_view_wiki_edits: View wiki history
442 permission_view_wiki_edits: View wiki history
443 permission_edit_wiki_pages: Edit wiki pages
443 permission_edit_wiki_pages: Edit wiki pages
444 permission_delete_wiki_pages_attachments: Delete attachments
444 permission_delete_wiki_pages_attachments: Delete attachments
445 permission_protect_wiki_pages: Protect wiki pages
445 permission_protect_wiki_pages: Protect wiki pages
446 permission_manage_repository: Manage repository
446 permission_manage_repository: Manage repository
447 permission_browse_repository: Browse repository
447 permission_browse_repository: Browse repository
448 permission_view_changesets: View changesets
448 permission_view_changesets: View changesets
449 permission_commit_access: Commit access
449 permission_commit_access: Commit access
450 permission_manage_boards: Manage forums
450 permission_manage_boards: Manage forums
451 permission_view_messages: View messages
451 permission_view_messages: View messages
452 permission_add_messages: Post messages
452 permission_add_messages: Post messages
453 permission_edit_messages: Edit messages
453 permission_edit_messages: Edit messages
454 permission_edit_own_messages: Edit own messages
454 permission_edit_own_messages: Edit own messages
455 permission_delete_messages: Delete messages
455 permission_delete_messages: Delete messages
456 permission_delete_own_messages: Delete own messages
456 permission_delete_own_messages: Delete own messages
457 permission_export_wiki_pages: Export wiki pages
457 permission_export_wiki_pages: Export wiki pages
458 permission_manage_subtasks: Manage subtasks
458 permission_manage_subtasks: Manage subtasks
459 permission_manage_related_issues: Manage related issues
459 permission_manage_related_issues: Manage related issues
460
460
461 project_module_issue_tracking: Issue tracking
461 project_module_issue_tracking: Issue tracking
462 project_module_time_tracking: Time tracking
462 project_module_time_tracking: Time tracking
463 project_module_news: News
463 project_module_news: News
464 project_module_documents: Documents
464 project_module_documents: Documents
465 project_module_files: Files
465 project_module_files: Files
466 project_module_wiki: Wiki
466 project_module_wiki: Wiki
467 project_module_repository: Repository
467 project_module_repository: Repository
468 project_module_boards: Forums
468 project_module_boards: Forums
469 project_module_calendar: Calendar
469 project_module_calendar: Calendar
470 project_module_gantt: Gantt
470 project_module_gantt: Gantt
471
471
472 label_user: User
472 label_user: User
473 label_user_plural: Users
473 label_user_plural: Users
474 label_user_new: New user
474 label_user_new: New user
475 label_user_anonymous: Anonymous
475 label_user_anonymous: Anonymous
476 label_project: Project
476 label_project: Project
477 label_project_new: New project
477 label_project_new: New project
478 label_project_plural: Projects
478 label_project_plural: Projects
479 label_x_projects:
479 label_x_projects:
480 zero: no projects
480 zero: no projects
481 one: 1 project
481 one: 1 project
482 other: "%{count} projects"
482 other: "%{count} projects"
483 label_project_all: All Projects
483 label_project_all: All Projects
484 label_project_latest: Latest projects
484 label_project_latest: Latest projects
485 label_issue: Issue
485 label_issue: Issue
486 label_issue_new: New issue
486 label_issue_new: New issue
487 label_issue_plural: Issues
487 label_issue_plural: Issues
488 label_issue_view_all: View all issues
488 label_issue_view_all: View all issues
489 label_issues_by: "Issues by %{value}"
489 label_issues_by: "Issues by %{value}"
490 label_issue_added: Issue added
490 label_issue_added: Issue added
491 label_issue_updated: Issue updated
491 label_issue_updated: Issue updated
492 label_issue_note_added: Note added
492 label_issue_note_added: Note added
493 label_issue_status_updated: Status updated
493 label_issue_status_updated: Status updated
494 label_issue_priority_updated: Priority updated
494 label_issue_priority_updated: Priority updated
495 label_document: Document
495 label_document: Document
496 label_document_new: New document
496 label_document_new: New document
497 label_document_plural: Documents
497 label_document_plural: Documents
498 label_document_added: Document added
498 label_document_added: Document added
499 label_role: Role
499 label_role: Role
500 label_role_plural: Roles
500 label_role_plural: Roles
501 label_role_new: New role
501 label_role_new: New role
502 label_role_and_permissions: Roles and permissions
502 label_role_and_permissions: Roles and permissions
503 label_role_anonymous: Anonymous
503 label_role_anonymous: Anonymous
504 label_role_non_member: Non member
504 label_role_non_member: Non member
505 label_member: Member
505 label_member: Member
506 label_member_new: New member
506 label_member_new: New member
507 label_member_plural: Members
507 label_member_plural: Members
508 label_tracker: Tracker
508 label_tracker: Tracker
509 label_tracker_plural: Trackers
509 label_tracker_plural: Trackers
510 label_tracker_new: New tracker
510 label_tracker_new: New tracker
511 label_workflow: Workflow
511 label_workflow: Workflow
512 label_issue_status: Issue status
512 label_issue_status: Issue status
513 label_issue_status_plural: Issue statuses
513 label_issue_status_plural: Issue statuses
514 label_issue_status_new: New status
514 label_issue_status_new: New status
515 label_issue_category: Issue category
515 label_issue_category: Issue category
516 label_issue_category_plural: Issue categories
516 label_issue_category_plural: Issue categories
517 label_issue_category_new: New category
517 label_issue_category_new: New category
518 label_custom_field: Custom field
518 label_custom_field: Custom field
519 label_custom_field_plural: Custom fields
519 label_custom_field_plural: Custom fields
520 label_custom_field_new: New custom field
520 label_custom_field_new: New custom field
521 label_enumerations: Enumerations
521 label_enumerations: Enumerations
522 label_enumeration_new: New value
522 label_enumeration_new: New value
523 label_information: Information
523 label_information: Information
524 label_information_plural: Information
524 label_information_plural: Information
525 label_please_login: Please log in
525 label_please_login: Please log in
526 label_register: Register
526 label_register: Register
527 label_login_with_open_id_option: or login with OpenID
527 label_login_with_open_id_option: or login with OpenID
528 label_password_lost: Lost password
528 label_password_lost: Lost password
529 label_home: Home
529 label_home: Home
530 label_my_page: My page
530 label_my_page: My page
531 label_my_account: My account
531 label_my_account: My account
532 label_my_projects: My projects
532 label_my_projects: My projects
533 label_my_page_block: My page block
533 label_my_page_block: My page block
534 label_administration: Administration
534 label_administration: Administration
535 label_login: Sign in
535 label_login: Sign in
536 label_logout: Sign out
536 label_logout: Sign out
537 label_help: Help
537 label_help: Help
538 label_reported_issues: Reported issues
538 label_reported_issues: Reported issues
539 label_assigned_to_me_issues: Issues assigned to me
539 label_assigned_to_me_issues: Issues assigned to me
540 label_last_login: Last connection
540 label_last_login: Last connection
541 label_registered_on: Registered on
541 label_registered_on: Registered on
542 label_activity: Activity
542 label_activity: Activity
543 label_overall_activity: Overall activity
543 label_overall_activity: Overall activity
544 label_user_activity: "%{value}'s activity"
544 label_user_activity: "%{value}'s activity"
545 label_new: New
545 label_new: New
546 label_logged_as: Logged in as
546 label_logged_as: Logged in as
547 label_environment: Environment
547 label_environment: Environment
548 label_authentication: Authentication
548 label_authentication: Authentication
549 label_auth_source: Authentication mode
549 label_auth_source: Authentication mode
550 label_auth_source_new: New authentication mode
550 label_auth_source_new: New authentication mode
551 label_auth_source_plural: Authentication modes
551 label_auth_source_plural: Authentication modes
552 label_subproject_plural: Subprojects
552 label_subproject_plural: Subprojects
553 label_subproject_new: New subproject
553 label_subproject_new: New subproject
554 label_and_its_subprojects: "%{value} and its subprojects"
554 label_and_its_subprojects: "%{value} and its subprojects"
555 label_min_max_length: Min - Max length
555 label_min_max_length: Min - Max length
556 label_list: List
556 label_list: List
557 label_date: Date
557 label_date: Date
558 label_integer: Integer
558 label_integer: Integer
559 label_float: Float
559 label_float: Float
560 label_boolean: Boolean
560 label_boolean: Boolean
561 label_string: Text
561 label_string: Text
562 label_text: Long text
562 label_text: Long text
563 label_attribute: Attribute
563 label_attribute: Attribute
564 label_attribute_plural: Attributes
564 label_attribute_plural: Attributes
565 label_download: "%{count} Download"
565 label_download: "%{count} Download"
566 label_download_plural: "%{count} Downloads"
566 label_download_plural: "%{count} Downloads"
567 label_no_data: No data to display
567 label_no_data: No data to display
568 label_change_status: Change status
568 label_change_status: Change status
569 label_history: History
569 label_history: History
570 label_attachment: File
570 label_attachment: File
571 label_attachment_new: New file
571 label_attachment_new: New file
572 label_attachment_delete: Delete file
572 label_attachment_delete: Delete file
573 label_attachment_plural: Files
573 label_attachment_plural: Files
574 label_file_added: File added
574 label_file_added: File added
575 label_report: Report
575 label_report: Report
576 label_report_plural: Reports
576 label_report_plural: Reports
577 label_news: News
577 label_news: News
578 label_news_new: Add news
578 label_news_new: Add news
579 label_news_plural: News
579 label_news_plural: News
580 label_news_latest: Latest news
580 label_news_latest: Latest news
581 label_news_view_all: View all news
581 label_news_view_all: View all news
582 label_news_added: News added
582 label_news_added: News added
583 label_news_comment_added: Comment added to a news
583 label_news_comment_added: Comment added to a news
584 label_settings: Settings
584 label_settings: Settings
585 label_overview: Overview
585 label_overview: Overview
586 label_version: Version
586 label_version: Version
587 label_version_new: New version
587 label_version_new: New version
588 label_version_plural: Versions
588 label_version_plural: Versions
589 label_close_versions: Close completed versions
589 label_close_versions: Close completed versions
590 label_confirmation: Confirmation
590 label_confirmation: Confirmation
591 label_export_to: 'Also available in:'
591 label_export_to: 'Also available in:'
592 label_read: Read...
592 label_read: Read...
593 label_public_projects: Public projects
593 label_public_projects: Public projects
594 label_open_issues: open
594 label_open_issues: open
595 label_open_issues_plural: open
595 label_open_issues_plural: open
596 label_closed_issues: closed
596 label_closed_issues: closed
597 label_closed_issues_plural: closed
597 label_closed_issues_plural: closed
598 label_x_open_issues_abbr_on_total:
598 label_x_open_issues_abbr_on_total:
599 zero: 0 open / %{total}
599 zero: 0 open / %{total}
600 one: 1 open / %{total}
600 one: 1 open / %{total}
601 other: "%{count} open / %{total}"
601 other: "%{count} open / %{total}"
602 label_x_open_issues_abbr:
602 label_x_open_issues_abbr:
603 zero: 0 open
603 zero: 0 open
604 one: 1 open
604 one: 1 open
605 other: "%{count} open"
605 other: "%{count} open"
606 label_x_closed_issues_abbr:
606 label_x_closed_issues_abbr:
607 zero: 0 closed
607 zero: 0 closed
608 one: 1 closed
608 one: 1 closed
609 other: "%{count} closed"
609 other: "%{count} closed"
610 label_x_issues:
610 label_x_issues:
611 zero: 0 issues
611 zero: 0 issues
612 one: 1 issue
612 one: 1 issue
613 other: "%{count} issues"
613 other: "%{count} issues"
614 label_total: Total
614 label_total: Total
615 label_permissions: Permissions
615 label_permissions: Permissions
616 label_current_status: Current status
616 label_current_status: Current status
617 label_new_statuses_allowed: New statuses allowed
617 label_new_statuses_allowed: New statuses allowed
618 label_all: all
618 label_all: all
619 label_none: none
619 label_none: none
620 label_nobody: nobody
620 label_nobody: nobody
621 label_next: Next
621 label_next: Next
622 label_previous: Previous
622 label_previous: Previous
623 label_used_by: Used by
623 label_used_by: Used by
624 label_details: Details
624 label_details: Details
625 label_add_note: Add a note
625 label_add_note: Add a note
626 label_per_page: Per page
626 label_per_page: Per page
627 label_calendar: Calendar
627 label_calendar: Calendar
628 label_months_from: months from
628 label_months_from: months from
629 label_gantt: Gantt
629 label_gantt: Gantt
630 label_internal: Internal
630 label_internal: Internal
631 label_last_changes: "last %{count} changes"
631 label_last_changes: "last %{count} changes"
632 label_change_view_all: View all changes
632 label_change_view_all: View all changes
633 label_personalize_page: Personalize this page
633 label_personalize_page: Personalize this page
634 label_comment: Comment
634 label_comment: Comment
635 label_comment_plural: Comments
635 label_comment_plural: Comments
636 label_x_comments:
636 label_x_comments:
637 zero: no comments
637 zero: no comments
638 one: 1 comment
638 one: 1 comment
639 other: "%{count} comments"
639 other: "%{count} comments"
640 label_comment_add: Add a comment
640 label_comment_add: Add a comment
641 label_comment_added: Comment added
641 label_comment_added: Comment added
642 label_comment_delete: Delete comments
642 label_comment_delete: Delete comments
643 label_query: Custom query
643 label_query: Custom query
644 label_query_plural: Custom queries
644 label_query_plural: Custom queries
645 label_query_new: New query
645 label_query_new: New query
646 label_my_queries: My custom queries
646 label_my_queries: My custom queries
647 label_filter_add: Add filter
647 label_filter_add: Add filter
648 label_filter_plural: Filters
648 label_filter_plural: Filters
649 label_equals: is
649 label_equals: is
650 label_not_equals: is not
650 label_not_equals: is not
651 label_in_less_than: in less than
651 label_in_less_than: in less than
652 label_in_more_than: in more than
652 label_in_more_than: in more than
653 label_greater_or_equal: '>='
653 label_greater_or_equal: '>='
654 label_less_or_equal: '<='
654 label_less_or_equal: '<='
655 label_between: between
655 label_between: between
656 label_in: in
656 label_in: in
657 label_today: today
657 label_today: today
658 label_all_time: all time
658 label_all_time: all time
659 label_yesterday: yesterday
659 label_yesterday: yesterday
660 label_this_week: this week
660 label_this_week: this week
661 label_last_week: last week
661 label_last_week: last week
662 label_last_n_days: "last %{count} days"
662 label_last_n_days: "last %{count} days"
663 label_this_month: this month
663 label_this_month: this month
664 label_last_month: last month
664 label_last_month: last month
665 label_this_year: this year
665 label_this_year: this year
666 label_date_range: Date range
666 label_date_range: Date range
667 label_less_than_ago: less than days ago
667 label_less_than_ago: less than days ago
668 label_more_than_ago: more than days ago
668 label_more_than_ago: more than days ago
669 label_ago: days ago
669 label_ago: days ago
670 label_contains: contains
670 label_contains: contains
671 label_not_contains: doesn't contain
671 label_not_contains: doesn't contain
672 label_any_issues_in_project: any issues in project
673 label_any_issues_not_in_project: any issues not in project
672 label_day_plural: days
674 label_day_plural: days
673 label_repository: Repository
675 label_repository: Repository
674 label_repository_new: New repository
676 label_repository_new: New repository
675 label_repository_plural: Repositories
677 label_repository_plural: Repositories
676 label_browse: Browse
678 label_browse: Browse
677 label_modification: "%{count} change"
679 label_modification: "%{count} change"
678 label_modification_plural: "%{count} changes"
680 label_modification_plural: "%{count} changes"
679 label_branch: Branch
681 label_branch: Branch
680 label_tag: Tag
682 label_tag: Tag
681 label_revision: Revision
683 label_revision: Revision
682 label_revision_plural: Revisions
684 label_revision_plural: Revisions
683 label_revision_id: "Revision %{value}"
685 label_revision_id: "Revision %{value}"
684 label_associated_revisions: Associated revisions
686 label_associated_revisions: Associated revisions
685 label_added: added
687 label_added: added
686 label_modified: modified
688 label_modified: modified
687 label_copied: copied
689 label_copied: copied
688 label_renamed: renamed
690 label_renamed: renamed
689 label_deleted: deleted
691 label_deleted: deleted
690 label_latest_revision: Latest revision
692 label_latest_revision: Latest revision
691 label_latest_revision_plural: Latest revisions
693 label_latest_revision_plural: Latest revisions
692 label_view_revisions: View revisions
694 label_view_revisions: View revisions
693 label_view_all_revisions: View all revisions
695 label_view_all_revisions: View all revisions
694 label_max_size: Maximum size
696 label_max_size: Maximum size
695 label_sort_highest: Move to top
697 label_sort_highest: Move to top
696 label_sort_higher: Move up
698 label_sort_higher: Move up
697 label_sort_lower: Move down
699 label_sort_lower: Move down
698 label_sort_lowest: Move to bottom
700 label_sort_lowest: Move to bottom
699 label_roadmap: Roadmap
701 label_roadmap: Roadmap
700 label_roadmap_due_in: "Due in %{value}"
702 label_roadmap_due_in: "Due in %{value}"
701 label_roadmap_overdue: "%{value} late"
703 label_roadmap_overdue: "%{value} late"
702 label_roadmap_no_issues: No issues for this version
704 label_roadmap_no_issues: No issues for this version
703 label_search: Search
705 label_search: Search
704 label_result_plural: Results
706 label_result_plural: Results
705 label_all_words: All words
707 label_all_words: All words
706 label_wiki: Wiki
708 label_wiki: Wiki
707 label_wiki_edit: Wiki edit
709 label_wiki_edit: Wiki edit
708 label_wiki_edit_plural: Wiki edits
710 label_wiki_edit_plural: Wiki edits
709 label_wiki_page: Wiki page
711 label_wiki_page: Wiki page
710 label_wiki_page_plural: Wiki pages
712 label_wiki_page_plural: Wiki pages
711 label_index_by_title: Index by title
713 label_index_by_title: Index by title
712 label_index_by_date: Index by date
714 label_index_by_date: Index by date
713 label_current_version: Current version
715 label_current_version: Current version
714 label_preview: Preview
716 label_preview: Preview
715 label_feed_plural: Feeds
717 label_feed_plural: Feeds
716 label_changes_details: Details of all changes
718 label_changes_details: Details of all changes
717 label_issue_tracking: Issue tracking
719 label_issue_tracking: Issue tracking
718 label_spent_time: Spent time
720 label_spent_time: Spent time
719 label_overall_spent_time: Overall spent time
721 label_overall_spent_time: Overall spent time
720 label_f_hour: "%{value} hour"
722 label_f_hour: "%{value} hour"
721 label_f_hour_plural: "%{value} hours"
723 label_f_hour_plural: "%{value} hours"
722 label_time_tracking: Time tracking
724 label_time_tracking: Time tracking
723 label_change_plural: Changes
725 label_change_plural: Changes
724 label_statistics: Statistics
726 label_statistics: Statistics
725 label_commits_per_month: Commits per month
727 label_commits_per_month: Commits per month
726 label_commits_per_author: Commits per author
728 label_commits_per_author: Commits per author
727 label_diff: diff
729 label_diff: diff
728 label_view_diff: View differences
730 label_view_diff: View differences
729 label_diff_inline: inline
731 label_diff_inline: inline
730 label_diff_side_by_side: side by side
732 label_diff_side_by_side: side by side
731 label_options: Options
733 label_options: Options
732 label_copy_workflow_from: Copy workflow from
734 label_copy_workflow_from: Copy workflow from
733 label_permissions_report: Permissions report
735 label_permissions_report: Permissions report
734 label_watched_issues: Watched issues
736 label_watched_issues: Watched issues
735 label_related_issues: Related issues
737 label_related_issues: Related issues
736 label_applied_status: Applied status
738 label_applied_status: Applied status
737 label_loading: Loading...
739 label_loading: Loading...
738 label_relation_new: New relation
740 label_relation_new: New relation
739 label_relation_delete: Delete relation
741 label_relation_delete: Delete relation
740 label_relates_to: related to
742 label_relates_to: Related to
741 label_duplicates: duplicates
743 label_duplicates: Duplicates
742 label_duplicated_by: duplicated by
744 label_duplicated_by: Duplicated by
743 label_blocks: blocks
745 label_blocks: Blocks
744 label_blocked_by: blocked by
746 label_blocked_by: Blocked by
745 label_precedes: precedes
747 label_precedes: Precedes
746 label_follows: follows
748 label_follows: Follows
747 label_copied_to: copied to
749 label_copied_to: Copied to
748 label_copied_from: copied from
750 label_copied_from: Copied from
749 label_end_to_start: end to start
751 label_end_to_start: end to start
750 label_end_to_end: end to end
752 label_end_to_end: end to end
751 label_start_to_start: start to start
753 label_start_to_start: start to start
752 label_start_to_end: start to end
754 label_start_to_end: start to end
753 label_stay_logged_in: Stay logged in
755 label_stay_logged_in: Stay logged in
754 label_disabled: disabled
756 label_disabled: disabled
755 label_show_completed_versions: Show completed versions
757 label_show_completed_versions: Show completed versions
756 label_me: me
758 label_me: me
757 label_board: Forum
759 label_board: Forum
758 label_board_new: New forum
760 label_board_new: New forum
759 label_board_plural: Forums
761 label_board_plural: Forums
760 label_board_locked: Locked
762 label_board_locked: Locked
761 label_board_sticky: Sticky
763 label_board_sticky: Sticky
762 label_topic_plural: Topics
764 label_topic_plural: Topics
763 label_message_plural: Messages
765 label_message_plural: Messages
764 label_message_last: Last message
766 label_message_last: Last message
765 label_message_new: New message
767 label_message_new: New message
766 label_message_posted: Message added
768 label_message_posted: Message added
767 label_reply_plural: Replies
769 label_reply_plural: Replies
768 label_send_information: Send account information to the user
770 label_send_information: Send account information to the user
769 label_year: Year
771 label_year: Year
770 label_month: Month
772 label_month: Month
771 label_week: Week
773 label_week: Week
772 label_date_from: From
774 label_date_from: From
773 label_date_to: To
775 label_date_to: To
774 label_language_based: Based on user's language
776 label_language_based: Based on user's language
775 label_sort_by: "Sort by %{value}"
777 label_sort_by: "Sort by %{value}"
776 label_send_test_email: Send a test email
778 label_send_test_email: Send a test email
777 label_feeds_access_key: RSS access key
779 label_feeds_access_key: RSS access key
778 label_missing_feeds_access_key: Missing a RSS access key
780 label_missing_feeds_access_key: Missing a RSS access key
779 label_feeds_access_key_created_on: "RSS access key created %{value} ago"
781 label_feeds_access_key_created_on: "RSS access key created %{value} ago"
780 label_module_plural: Modules
782 label_module_plural: Modules
781 label_added_time_by: "Added by %{author} %{age} ago"
783 label_added_time_by: "Added by %{author} %{age} ago"
782 label_updated_time_by: "Updated by %{author} %{age} ago"
784 label_updated_time_by: "Updated by %{author} %{age} ago"
783 label_updated_time: "Updated %{value} ago"
785 label_updated_time: "Updated %{value} ago"
784 label_jump_to_a_project: Jump to a project...
786 label_jump_to_a_project: Jump to a project...
785 label_file_plural: Files
787 label_file_plural: Files
786 label_changeset_plural: Changesets
788 label_changeset_plural: Changesets
787 label_default_columns: Default columns
789 label_default_columns: Default columns
788 label_no_change_option: (No change)
790 label_no_change_option: (No change)
789 label_bulk_edit_selected_issues: Bulk edit selected issues
791 label_bulk_edit_selected_issues: Bulk edit selected issues
790 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
792 label_bulk_edit_selected_time_entries: Bulk edit selected time entries
791 label_theme: Theme
793 label_theme: Theme
792 label_default: Default
794 label_default: Default
793 label_search_titles_only: Search titles only
795 label_search_titles_only: Search titles only
794 label_user_mail_option_all: "For any event on all my projects"
796 label_user_mail_option_all: "For any event on all my projects"
795 label_user_mail_option_selected: "For any event on the selected projects only..."
797 label_user_mail_option_selected: "For any event on the selected projects only..."
796 label_user_mail_option_none: "No events"
798 label_user_mail_option_none: "No events"
797 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
799 label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in"
798 label_user_mail_option_only_assigned: "Only for things I am assigned to"
800 label_user_mail_option_only_assigned: "Only for things I am assigned to"
799 label_user_mail_option_only_owner: "Only for things I am the owner of"
801 label_user_mail_option_only_owner: "Only for things I am the owner of"
800 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
802 label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself"
801 label_registration_activation_by_email: account activation by email
803 label_registration_activation_by_email: account activation by email
802 label_registration_manual_activation: manual account activation
804 label_registration_manual_activation: manual account activation
803 label_registration_automatic_activation: automatic account activation
805 label_registration_automatic_activation: automatic account activation
804 label_display_per_page: "Per page: %{value}"
806 label_display_per_page: "Per page: %{value}"
805 label_age: Age
807 label_age: Age
806 label_change_properties: Change properties
808 label_change_properties: Change properties
807 label_general: General
809 label_general: General
808 label_more: More
810 label_more: More
809 label_scm: SCM
811 label_scm: SCM
810 label_plugins: Plugins
812 label_plugins: Plugins
811 label_ldap_authentication: LDAP authentication
813 label_ldap_authentication: LDAP authentication
812 label_downloads_abbr: D/L
814 label_downloads_abbr: D/L
813 label_optional_description: Optional description
815 label_optional_description: Optional description
814 label_add_another_file: Add another file
816 label_add_another_file: Add another file
815 label_preferences: Preferences
817 label_preferences: Preferences
816 label_chronological_order: In chronological order
818 label_chronological_order: In chronological order
817 label_reverse_chronological_order: In reverse chronological order
819 label_reverse_chronological_order: In reverse chronological order
818 label_planning: Planning
820 label_planning: Planning
819 label_incoming_emails: Incoming emails
821 label_incoming_emails: Incoming emails
820 label_generate_key: Generate a key
822 label_generate_key: Generate a key
821 label_issue_watchers: Watchers
823 label_issue_watchers: Watchers
822 label_example: Example
824 label_example: Example
823 label_display: Display
825 label_display: Display
824 label_sort: Sort
826 label_sort: Sort
825 label_ascending: Ascending
827 label_ascending: Ascending
826 label_descending: Descending
828 label_descending: Descending
827 label_date_from_to: From %{start} to %{end}
829 label_date_from_to: From %{start} to %{end}
828 label_wiki_content_added: Wiki page added
830 label_wiki_content_added: Wiki page added
829 label_wiki_content_updated: Wiki page updated
831 label_wiki_content_updated: Wiki page updated
830 label_group: Group
832 label_group: Group
831 label_group_plural: Groups
833 label_group_plural: Groups
832 label_group_new: New group
834 label_group_new: New group
833 label_time_entry_plural: Spent time
835 label_time_entry_plural: Spent time
834 label_version_sharing_none: Not shared
836 label_version_sharing_none: Not shared
835 label_version_sharing_descendants: With subprojects
837 label_version_sharing_descendants: With subprojects
836 label_version_sharing_hierarchy: With project hierarchy
838 label_version_sharing_hierarchy: With project hierarchy
837 label_version_sharing_tree: With project tree
839 label_version_sharing_tree: With project tree
838 label_version_sharing_system: With all projects
840 label_version_sharing_system: With all projects
839 label_update_issue_done_ratios: Update issue done ratios
841 label_update_issue_done_ratios: Update issue done ratios
840 label_copy_source: Source
842 label_copy_source: Source
841 label_copy_target: Target
843 label_copy_target: Target
842 label_copy_same_as_target: Same as target
844 label_copy_same_as_target: Same as target
843 label_display_used_statuses_only: Only display statuses that are used by this tracker
845 label_display_used_statuses_only: Only display statuses that are used by this tracker
844 label_api_access_key: API access key
846 label_api_access_key: API access key
845 label_missing_api_access_key: Missing an API access key
847 label_missing_api_access_key: Missing an API access key
846 label_api_access_key_created_on: "API access key created %{value} ago"
848 label_api_access_key_created_on: "API access key created %{value} ago"
847 label_profile: Profile
849 label_profile: Profile
848 label_subtask_plural: Subtasks
850 label_subtask_plural: Subtasks
849 label_project_copy_notifications: Send email notifications during the project copy
851 label_project_copy_notifications: Send email notifications during the project copy
850 label_principal_search: "Search for user or group:"
852 label_principal_search: "Search for user or group:"
851 label_user_search: "Search for user:"
853 label_user_search: "Search for user:"
852 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
854 label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author
853 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
855 label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee
854 label_issues_visibility_all: All issues
856 label_issues_visibility_all: All issues
855 label_issues_visibility_public: All non private issues
857 label_issues_visibility_public: All non private issues
856 label_issues_visibility_own: Issues created by or assigned to the user
858 label_issues_visibility_own: Issues created by or assigned to the user
857 label_git_report_last_commit: Report last commit for files and directories
859 label_git_report_last_commit: Report last commit for files and directories
858 label_parent_revision: Parent
860 label_parent_revision: Parent
859 label_child_revision: Child
861 label_child_revision: Child
860 label_export_options: "%{export_format} export options"
862 label_export_options: "%{export_format} export options"
861 label_copy_attachments: Copy attachments
863 label_copy_attachments: Copy attachments
862 label_copy_subtasks: Copy subtasks
864 label_copy_subtasks: Copy subtasks
863 label_item_position: "%{position} of %{count}"
865 label_item_position: "%{position} of %{count}"
864 label_completed_versions: Completed versions
866 label_completed_versions: Completed versions
865 label_search_for_watchers: Search for watchers to add
867 label_search_for_watchers: Search for watchers to add
866 label_session_expiration: Session expiration
868 label_session_expiration: Session expiration
867 label_show_closed_projects: View closed projects
869 label_show_closed_projects: View closed projects
868 label_status_transitions: Status transitions
870 label_status_transitions: Status transitions
869 label_fields_permissions: Fields permissions
871 label_fields_permissions: Fields permissions
870 label_readonly: Read-only
872 label_readonly: Read-only
871 label_required: Required
873 label_required: Required
872 label_attribute_of_project: "Project's %{name}"
874 label_attribute_of_project: "Project's %{name}"
873 label_attribute_of_author: "Author's %{name}"
875 label_attribute_of_author: "Author's %{name}"
874 label_attribute_of_assigned_to: "Assignee's %{name}"
876 label_attribute_of_assigned_to: "Assignee's %{name}"
875 label_attribute_of_fixed_version: "Target version's %{name}"
877 label_attribute_of_fixed_version: "Target version's %{name}"
876
878
877 button_login: Login
879 button_login: Login
878 button_submit: Submit
880 button_submit: Submit
879 button_save: Save
881 button_save: Save
880 button_check_all: Check all
882 button_check_all: Check all
881 button_uncheck_all: Uncheck all
883 button_uncheck_all: Uncheck all
882 button_collapse_all: Collapse all
884 button_collapse_all: Collapse all
883 button_expand_all: Expand all
885 button_expand_all: Expand all
884 button_delete: Delete
886 button_delete: Delete
885 button_create: Create
887 button_create: Create
886 button_create_and_continue: Create and continue
888 button_create_and_continue: Create and continue
887 button_test: Test
889 button_test: Test
888 button_edit: Edit
890 button_edit: Edit
889 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
891 button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}"
890 button_add: Add
892 button_add: Add
891 button_change: Change
893 button_change: Change
892 button_apply: Apply
894 button_apply: Apply
893 button_clear: Clear
895 button_clear: Clear
894 button_lock: Lock
896 button_lock: Lock
895 button_unlock: Unlock
897 button_unlock: Unlock
896 button_download: Download
898 button_download: Download
897 button_list: List
899 button_list: List
898 button_view: View
900 button_view: View
899 button_move: Move
901 button_move: Move
900 button_move_and_follow: Move and follow
902 button_move_and_follow: Move and follow
901 button_back: Back
903 button_back: Back
902 button_cancel: Cancel
904 button_cancel: Cancel
903 button_activate: Activate
905 button_activate: Activate
904 button_sort: Sort
906 button_sort: Sort
905 button_log_time: Log time
907 button_log_time: Log time
906 button_rollback: Rollback to this version
908 button_rollback: Rollback to this version
907 button_watch: Watch
909 button_watch: Watch
908 button_unwatch: Unwatch
910 button_unwatch: Unwatch
909 button_reply: Reply
911 button_reply: Reply
910 button_archive: Archive
912 button_archive: Archive
911 button_unarchive: Unarchive
913 button_unarchive: Unarchive
912 button_reset: Reset
914 button_reset: Reset
913 button_rename: Rename
915 button_rename: Rename
914 button_change_password: Change password
916 button_change_password: Change password
915 button_copy: Copy
917 button_copy: Copy
916 button_copy_and_follow: Copy and follow
918 button_copy_and_follow: Copy and follow
917 button_annotate: Annotate
919 button_annotate: Annotate
918 button_update: Update
920 button_update: Update
919 button_configure: Configure
921 button_configure: Configure
920 button_quote: Quote
922 button_quote: Quote
921 button_duplicate: Duplicate
923 button_duplicate: Duplicate
922 button_show: Show
924 button_show: Show
923 button_edit_section: Edit this section
925 button_edit_section: Edit this section
924 button_export: Export
926 button_export: Export
925 button_delete_my_account: Delete my account
927 button_delete_my_account: Delete my account
926 button_close: Close
928 button_close: Close
927 button_reopen: Reopen
929 button_reopen: Reopen
928
930
929 status_active: active
931 status_active: active
930 status_registered: registered
932 status_registered: registered
931 status_locked: locked
933 status_locked: locked
932
934
933 project_status_active: active
935 project_status_active: active
934 project_status_closed: closed
936 project_status_closed: closed
935 project_status_archived: archived
937 project_status_archived: archived
936
938
937 version_status_open: open
939 version_status_open: open
938 version_status_locked: locked
940 version_status_locked: locked
939 version_status_closed: closed
941 version_status_closed: closed
940
942
941 field_active: Active
943 field_active: Active
942
944
943 text_select_mail_notifications: Select actions for which email notifications should be sent.
945 text_select_mail_notifications: Select actions for which email notifications should be sent.
944 text_regexp_info: eg. ^[A-Z0-9]+$
946 text_regexp_info: eg. ^[A-Z0-9]+$
945 text_min_max_length_info: 0 means no restriction
947 text_min_max_length_info: 0 means no restriction
946 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
948 text_project_destroy_confirmation: Are you sure you want to delete this project and related data?
947 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
949 text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted."
948 text_workflow_edit: Select a role and a tracker to edit the workflow
950 text_workflow_edit: Select a role and a tracker to edit the workflow
949 text_are_you_sure: Are you sure?
951 text_are_you_sure: Are you sure?
950 text_are_you_sure_with_children: "Delete issue and all child issues?"
952 text_are_you_sure_with_children: "Delete issue and all child issues?"
951 text_journal_changed: "%{label} changed from %{old} to %{new}"
953 text_journal_changed: "%{label} changed from %{old} to %{new}"
952 text_journal_changed_no_detail: "%{label} updated"
954 text_journal_changed_no_detail: "%{label} updated"
953 text_journal_set_to: "%{label} set to %{value}"
955 text_journal_set_to: "%{label} set to %{value}"
954 text_journal_deleted: "%{label} deleted (%{old})"
956 text_journal_deleted: "%{label} deleted (%{old})"
955 text_journal_added: "%{label} %{value} added"
957 text_journal_added: "%{label} %{value} added"
956 text_tip_issue_begin_day: issue beginning this day
958 text_tip_issue_begin_day: issue beginning this day
957 text_tip_issue_end_day: issue ending this day
959 text_tip_issue_end_day: issue ending this day
958 text_tip_issue_begin_end_day: issue beginning and ending this day
960 text_tip_issue_begin_end_day: issue beginning and ending this day
959 text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
961 text_project_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
960 text_caracters_maximum: "%{count} characters maximum."
962 text_caracters_maximum: "%{count} characters maximum."
961 text_caracters_minimum: "Must be at least %{count} characters long."
963 text_caracters_minimum: "Must be at least %{count} characters long."
962 text_length_between: "Length between %{min} and %{max} characters."
964 text_length_between: "Length between %{min} and %{max} characters."
963 text_tracker_no_workflow: No workflow defined for this tracker
965 text_tracker_no_workflow: No workflow defined for this tracker
964 text_unallowed_characters: Unallowed characters
966 text_unallowed_characters: Unallowed characters
965 text_comma_separated: Multiple values allowed (comma separated).
967 text_comma_separated: Multiple values allowed (comma separated).
966 text_line_separated: Multiple values allowed (one line for each value).
968 text_line_separated: Multiple values allowed (one line for each value).
967 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
969 text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages
968 text_issue_added: "Issue %{id} has been reported by %{author}."
970 text_issue_added: "Issue %{id} has been reported by %{author}."
969 text_issue_updated: "Issue %{id} has been updated by %{author}."
971 text_issue_updated: "Issue %{id} has been updated by %{author}."
970 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
972 text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content?
971 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
973 text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?"
972 text_issue_category_destroy_assignments: Remove category assignments
974 text_issue_category_destroy_assignments: Remove category assignments
973 text_issue_category_reassign_to: Reassign issues to this category
975 text_issue_category_reassign_to: Reassign issues to this category
974 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)."
976 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)."
975 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."
977 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."
976 text_load_default_configuration: Load the default configuration
978 text_load_default_configuration: Load the default configuration
977 text_status_changed_by_changeset: "Applied in changeset %{value}."
979 text_status_changed_by_changeset: "Applied in changeset %{value}."
978 text_time_logged_by_changeset: "Applied in changeset %{value}."
980 text_time_logged_by_changeset: "Applied in changeset %{value}."
979 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
981 text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?'
980 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
982 text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)."
981 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
983 text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?'
982 text_select_project_modules: 'Select modules to enable for this project:'
984 text_select_project_modules: 'Select modules to enable for this project:'
983 text_default_administrator_account_changed: Default administrator account changed
985 text_default_administrator_account_changed: Default administrator account changed
984 text_file_repository_writable: Attachments directory writable
986 text_file_repository_writable: Attachments directory writable
985 text_plugin_assets_writable: Plugin assets directory writable
987 text_plugin_assets_writable: Plugin assets directory writable
986 text_rmagick_available: RMagick available (optional)
988 text_rmagick_available: RMagick available (optional)
987 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
989 text_destroy_time_entries_question: "%{hours} hours were reported on the issues you are about to delete. What do you want to do?"
988 text_destroy_time_entries: Delete reported hours
990 text_destroy_time_entries: Delete reported hours
989 text_assign_time_entries_to_project: Assign reported hours to the project
991 text_assign_time_entries_to_project: Assign reported hours to the project
990 text_reassign_time_entries: 'Reassign reported hours to this issue:'
992 text_reassign_time_entries: 'Reassign reported hours to this issue:'
991 text_user_wrote: "%{value} wrote:"
993 text_user_wrote: "%{value} wrote:"
992 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
994 text_enumeration_destroy_question: "%{count} objects are assigned to this value."
993 text_enumeration_category_reassign_to: 'Reassign them to this value:'
995 text_enumeration_category_reassign_to: 'Reassign them to this value:'
994 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."
996 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."
995 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."
997 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."
996 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
998 text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.'
997 text_custom_field_possible_values_info: 'One line for each value'
999 text_custom_field_possible_values_info: 'One line for each value'
998 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
1000 text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?"
999 text_wiki_page_nullify_children: "Keep child pages as root pages"
1001 text_wiki_page_nullify_children: "Keep child pages as root pages"
1000 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1002 text_wiki_page_destroy_children: "Delete child pages and all their descendants"
1001 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1003 text_wiki_page_reassign_children: "Reassign child pages to this parent page"
1002 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?"
1004 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?"
1003 text_zoom_in: Zoom in
1005 text_zoom_in: Zoom in
1004 text_zoom_out: Zoom out
1006 text_zoom_out: Zoom out
1005 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1007 text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page."
1006 text_scm_path_encoding_note: "Default: UTF-8"
1008 text_scm_path_encoding_note: "Default: UTF-8"
1007 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1009 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1008 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1010 text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo)
1009 text_scm_command: Command
1011 text_scm_command: Command
1010 text_scm_command_version: Version
1012 text_scm_command_version: Version
1011 text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it.
1013 text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it.
1012 text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel.
1014 text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel.
1013 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1015 text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)"
1014 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1016 text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes"
1015 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1017 text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}"
1016 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1018 text_account_destroy_confirmation: "Are you sure you want to proceed?\nYour account will be permanently deleted, with no way to reactivate it."
1017 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1019 text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours."
1018 text_project_closed: This project is closed and read-only.
1020 text_project_closed: This project is closed and read-only.
1019
1021
1020 default_role_manager: Manager
1022 default_role_manager: Manager
1021 default_role_developer: Developer
1023 default_role_developer: Developer
1022 default_role_reporter: Reporter
1024 default_role_reporter: Reporter
1023 default_tracker_bug: Bug
1025 default_tracker_bug: Bug
1024 default_tracker_feature: Feature
1026 default_tracker_feature: Feature
1025 default_tracker_support: Support
1027 default_tracker_support: Support
1026 default_issue_status_new: New
1028 default_issue_status_new: New
1027 default_issue_status_in_progress: In Progress
1029 default_issue_status_in_progress: In Progress
1028 default_issue_status_resolved: Resolved
1030 default_issue_status_resolved: Resolved
1029 default_issue_status_feedback: Feedback
1031 default_issue_status_feedback: Feedback
1030 default_issue_status_closed: Closed
1032 default_issue_status_closed: Closed
1031 default_issue_status_rejected: Rejected
1033 default_issue_status_rejected: Rejected
1032 default_doc_category_user: User documentation
1034 default_doc_category_user: User documentation
1033 default_doc_category_tech: Technical documentation
1035 default_doc_category_tech: Technical documentation
1034 default_priority_low: Low
1036 default_priority_low: Low
1035 default_priority_normal: Normal
1037 default_priority_normal: Normal
1036 default_priority_high: High
1038 default_priority_high: High
1037 default_priority_urgent: Urgent
1039 default_priority_urgent: Urgent
1038 default_priority_immediate: Immediate
1040 default_priority_immediate: Immediate
1039 default_activity_design: Design
1041 default_activity_design: Design
1040 default_activity_development: Development
1042 default_activity_development: Development
1041
1043
1042 enumeration_issue_priorities: Issue priorities
1044 enumeration_issue_priorities: Issue priorities
1043 enumeration_doc_categories: Document categories
1045 enumeration_doc_categories: Document categories
1044 enumeration_activities: Activities (time tracking)
1046 enumeration_activities: Activities (time tracking)
1045 enumeration_system_activity: System Activity
1047 enumeration_system_activity: System Activity
1046 description_filter: Filter
1048 description_filter: Filter
1047 description_search: Searchfield
1049 description_search: Searchfield
1048 description_choose_project: Projects
1050 description_choose_project: Projects
1049 description_project_scope: Search scope
1051 description_project_scope: Search scope
1050 description_notes: Notes
1052 description_notes: Notes
1051 description_message_content: Message content
1053 description_message_content: Message content
1052 description_query_sort_criteria_attribute: Sort attribute
1054 description_query_sort_criteria_attribute: Sort attribute
1053 description_query_sort_criteria_direction: Sort direction
1055 description_query_sort_criteria_direction: Sort direction
1054 description_user_mail_notification: Mail notification settings
1056 description_user_mail_notification: Mail notification settings
1055 description_available_columns: Available Columns
1057 description_available_columns: Available Columns
1056 description_selected_columns: Selected Columns
1058 description_selected_columns: Selected Columns
1057 description_all_columns: All Columns
1059 description_all_columns: All Columns
1058 description_issue_category_reassign: Choose issue category
1060 description_issue_category_reassign: Choose issue category
1059 description_wiki_subpages_reassign: Choose new parent page
1061 description_wiki_subpages_reassign: Choose new parent page
1060 description_date_range_list: Choose range from list
1062 description_date_range_list: Choose range from list
1061 description_date_range_interval: Choose range by selecting start and end date
1063 description_date_range_interval: Choose range by selecting start and end date
1062 description_date_from: Enter start date
1064 description_date_from: Enter start date
1063 description_date_to: Enter end date
1065 description_date_to: Enter end date
1064 text_repository_identifier_info: 'Only lower case letters (a-z), numbers, dashes and underscores are allowed.<br />Once saved, the identifier cannot be changed.'
1066 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,1081 +1,1083
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
149
150 actionview_instancetag_blank_option: Choisir
150 actionview_instancetag_blank_option: Choisir
151
151
152 general_text_No: 'Non'
152 general_text_No: 'Non'
153 general_text_Yes: 'Oui'
153 general_text_Yes: 'Oui'
154 general_text_no: 'non'
154 general_text_no: 'non'
155 general_text_yes: 'oui'
155 general_text_yes: 'oui'
156 general_lang_name: 'Français'
156 general_lang_name: 'Français'
157 general_csv_separator: ';'
157 general_csv_separator: ';'
158 general_csv_decimal_separator: ','
158 general_csv_decimal_separator: ','
159 general_csv_encoding: ISO-8859-1
159 general_csv_encoding: ISO-8859-1
160 general_pdf_encoding: UTF-8
160 general_pdf_encoding: UTF-8
161 general_first_day_of_week: '1'
161 general_first_day_of_week: '1'
162
162
163 notice_account_updated: Le compte a été mis à jour avec succès.
163 notice_account_updated: Le compte a été mis à jour avec succès.
164 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
164 notice_account_invalid_creditentials: Identifiant ou mot de passe invalide.
165 notice_account_password_updated: Mot de passe mis à jour avec succès.
165 notice_account_password_updated: Mot de passe mis à jour avec succès.
166 notice_account_wrong_password: Mot de passe incorrect
166 notice_account_wrong_password: Mot de passe incorrect
167 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé.
167 notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé.
168 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
168 notice_account_unknown_email: Aucun compte ne correspond à cette adresse.
169 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
169 notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe.
170 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
170 notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé.
171 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
171 notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter.
172 notice_successful_create: Création effectuée avec succès.
172 notice_successful_create: Création effectuée avec succès.
173 notice_successful_update: Mise à jour effectuée avec succès.
173 notice_successful_update: Mise à jour effectuée avec succès.
174 notice_successful_delete: Suppression effectuée avec succès.
174 notice_successful_delete: Suppression effectuée avec succès.
175 notice_successful_connection: Connexion réussie.
175 notice_successful_connection: Connexion réussie.
176 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
176 notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée."
177 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
177 notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible.
178 notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page."
178 notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page."
179 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé.
179 notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé.
180 notice_email_sent: "Un email a été envoyé à %{value}"
180 notice_email_sent: "Un email a été envoyé à %{value}"
181 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
181 notice_email_error: "Erreur lors de l'envoi de l'email (%{value})"
182 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée."
182 notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée."
183 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sélectionnées n'ont pas pu être mise(s) à jour : %{ids}."
183 notice_failed_to_save_issues: "%{count} demande(s) sur les %{total} sélectionnées n'ont pas pu être mise(s) à jour : %{ids}."
184 notice_failed_to_save_time_entries: "%{count} temps passé(s) sur les %{total} sélectionnés n'ont pas pu être mis à jour: %{ids}."
184 notice_failed_to_save_time_entries: "%{count} temps passé(s) sur les %{total} sélectionnés n'ont pas pu être mis à jour: %{ids}."
185 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
185 notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour."
186 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
186 notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur."
187 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
187 notice_default_data_loaded: Paramétrage par défaut chargé avec succès.
188 notice_unable_delete_version: Impossible de supprimer cette version.
188 notice_unable_delete_version: Impossible de supprimer cette version.
189 notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour.
189 notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour.
190 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
190 notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée.
191 notice_gantt_chart_truncated: "Le diagramme a été tronqué car il excède le nombre maximal d'éléments pouvant être affichés (%{max})"
191 notice_gantt_chart_truncated: "Le diagramme a été tronqué car il excède le nombre maximal d'éléments pouvant être affichés (%{max})"
192 notice_issue_successful_create: "Demande %{id} créée."
192 notice_issue_successful_create: "Demande %{id} créée."
193 notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez."
193 notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez."
194 notice_account_deleted: "Votre compte a été définitivement supprimé."
194 notice_account_deleted: "Votre compte a été définitivement supprimé."
195 notice_user_successful_create: "Utilisateur %{id} créé."
195 notice_user_successful_create: "Utilisateur %{id} créé."
196
196
197 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}"
197 error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}"
198 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
198 error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt."
199 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
199 error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}"
200 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
200 error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée."
201 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
201 error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet"
202 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
202 error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte'
203 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
203 error_can_not_archive_project: "Ce projet ne peut pas être archivé"
204 error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
204 error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source'
205 error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
205 error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles'
206 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour.
206 error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour.
207 error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
207 error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size})
208 error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
208 error_session_expired: "Votre session a expiré. Veuillez vous reconnecter."
209
209
210 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
210 warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés."
211
211
212 mail_subject_lost_password: "Votre mot de passe %{value}"
212 mail_subject_lost_password: "Votre mot de passe %{value}"
213 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
213 mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :'
214 mail_subject_register: "Activation de votre compte %{value}"
214 mail_subject_register: "Activation de votre compte %{value}"
215 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
215 mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :'
216 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
216 mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter."
217 mail_body_account_information: Paramètres de connexion de votre compte
217 mail_body_account_information: Paramètres de connexion de votre compte
218 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
218 mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}"
219 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :"
219 mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :"
220 mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})"
220 mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})"
221 mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :"
221 mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :"
222 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée"
222 mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée"
223 mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}."
223 mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}."
224 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour"
224 mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour"
225 mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}."
225 mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}."
226
226
227 gui_validation_error: 1 erreur
227 gui_validation_error: 1 erreur
228 gui_validation_error_plural: "%{count} erreurs"
228 gui_validation_error_plural: "%{count} erreurs"
229
229
230 field_name: Nom
230 field_name: Nom
231 field_description: Description
231 field_description: Description
232 field_summary: Résumé
232 field_summary: Résumé
233 field_is_required: Obligatoire
233 field_is_required: Obligatoire
234 field_firstname: Prénom
234 field_firstname: Prénom
235 field_lastname: Nom
235 field_lastname: Nom
236 field_mail: "Email "
236 field_mail: "Email "
237 field_filename: Fichier
237 field_filename: Fichier
238 field_filesize: Taille
238 field_filesize: Taille
239 field_downloads: Téléchargements
239 field_downloads: Téléchargements
240 field_author: Auteur
240 field_author: Auteur
241 field_created_on: "Créé "
241 field_created_on: "Créé "
242 field_updated_on: "Mis-à-jour "
242 field_updated_on: "Mis-à-jour "
243 field_field_format: Format
243 field_field_format: Format
244 field_is_for_all: Pour tous les projets
244 field_is_for_all: Pour tous les projets
245 field_possible_values: Valeurs possibles
245 field_possible_values: Valeurs possibles
246 field_regexp: Expression régulière
246 field_regexp: Expression régulière
247 field_min_length: Longueur minimum
247 field_min_length: Longueur minimum
248 field_max_length: Longueur maximum
248 field_max_length: Longueur maximum
249 field_value: Valeur
249 field_value: Valeur
250 field_category: Catégorie
250 field_category: Catégorie
251 field_title: Titre
251 field_title: Titre
252 field_project: Projet
252 field_project: Projet
253 field_issue: Demande
253 field_issue: Demande
254 field_status: Statut
254 field_status: Statut
255 field_notes: Notes
255 field_notes: Notes
256 field_is_closed: Demande fermée
256 field_is_closed: Demande fermée
257 field_is_default: Valeur par défaut
257 field_is_default: Valeur par défaut
258 field_tracker: Tracker
258 field_tracker: Tracker
259 field_subject: Sujet
259 field_subject: Sujet
260 field_due_date: Echéance
260 field_due_date: Echéance
261 field_assigned_to: Assigné à
261 field_assigned_to: Assigné à
262 field_priority: Priorité
262 field_priority: Priorité
263 field_fixed_version: Version cible
263 field_fixed_version: Version cible
264 field_user: Utilisateur
264 field_user: Utilisateur
265 field_role: Rôle
265 field_role: Rôle
266 field_homepage: "Site web "
266 field_homepage: "Site web "
267 field_is_public: Public
267 field_is_public: Public
268 field_parent: Sous-projet de
268 field_parent: Sous-projet de
269 field_is_in_roadmap: Demandes affichées dans la roadmap
269 field_is_in_roadmap: Demandes affichées dans la roadmap
270 field_login: "Identifiant "
270 field_login: "Identifiant "
271 field_mail_notification: Notifications par mail
271 field_mail_notification: Notifications par mail
272 field_admin: Administrateur
272 field_admin: Administrateur
273 field_last_login_on: "Dernière connexion "
273 field_last_login_on: "Dernière connexion "
274 field_language: Langue
274 field_language: Langue
275 field_effective_date: Date
275 field_effective_date: Date
276 field_password: Mot de passe
276 field_password: Mot de passe
277 field_new_password: Nouveau mot de passe
277 field_new_password: Nouveau mot de passe
278 field_password_confirmation: Confirmation
278 field_password_confirmation: Confirmation
279 field_version: Version
279 field_version: Version
280 field_type: Type
280 field_type: Type
281 field_host: Hôte
281 field_host: Hôte
282 field_port: Port
282 field_port: Port
283 field_account: Compte
283 field_account: Compte
284 field_base_dn: Base DN
284 field_base_dn: Base DN
285 field_attr_login: Attribut Identifiant
285 field_attr_login: Attribut Identifiant
286 field_attr_firstname: Attribut Prénom
286 field_attr_firstname: Attribut Prénom
287 field_attr_lastname: Attribut Nom
287 field_attr_lastname: Attribut Nom
288 field_attr_mail: Attribut Email
288 field_attr_mail: Attribut Email
289 field_onthefly: Création des utilisateurs à la volée
289 field_onthefly: Création des utilisateurs à la volée
290 field_start_date: Début
290 field_start_date: Début
291 field_done_ratio: "% réalisé"
291 field_done_ratio: "% réalisé"
292 field_auth_source: Mode d'authentification
292 field_auth_source: Mode d'authentification
293 field_hide_mail: Cacher mon adresse mail
293 field_hide_mail: Cacher mon adresse mail
294 field_comments: Commentaire
294 field_comments: Commentaire
295 field_url: URL
295 field_url: URL
296 field_start_page: Page de démarrage
296 field_start_page: Page de démarrage
297 field_subproject: Sous-projet
297 field_subproject: Sous-projet
298 field_hours: Heures
298 field_hours: Heures
299 field_activity: Activité
299 field_activity: Activité
300 field_spent_on: Date
300 field_spent_on: Date
301 field_identifier: Identifiant
301 field_identifier: Identifiant
302 field_is_filter: Utilisé comme filtre
302 field_is_filter: Utilisé comme filtre
303 field_issue_to: Demande liée
303 field_issue_to: Demande liée
304 field_delay: Retard
304 field_delay: Retard
305 field_assignable: Demandes assignables à ce rôle
305 field_assignable: Demandes assignables à ce rôle
306 field_redirect_existing_links: Rediriger les liens existants
306 field_redirect_existing_links: Rediriger les liens existants
307 field_estimated_hours: Temps estimé
307 field_estimated_hours: Temps estimé
308 field_column_names: Colonnes
308 field_column_names: Colonnes
309 field_time_zone: Fuseau horaire
309 field_time_zone: Fuseau horaire
310 field_searchable: Utilisé pour les recherches
310 field_searchable: Utilisé pour les recherches
311 field_default_value: Valeur par défaut
311 field_default_value: Valeur par défaut
312 field_comments_sorting: Afficher les commentaires
312 field_comments_sorting: Afficher les commentaires
313 field_parent_title: Page parent
313 field_parent_title: Page parent
314 field_editable: Modifiable
314 field_editable: Modifiable
315 field_watcher: Observateur
315 field_watcher: Observateur
316 field_identity_url: URL OpenID
316 field_identity_url: URL OpenID
317 field_content: Contenu
317 field_content: Contenu
318 field_group_by: Grouper par
318 field_group_by: Grouper par
319 field_sharing: Partage
319 field_sharing: Partage
320 field_active: Actif
320 field_active: Actif
321 field_parent_issue: Tâche parente
321 field_parent_issue: Tâche parente
322 field_visible: Visible
322 field_visible: Visible
323 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
323 field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé"
324 field_issues_visibility: Visibilité des demandes
324 field_issues_visibility: Visibilité des demandes
325 field_is_private: Privée
325 field_is_private: Privée
326 field_commit_logs_encoding: Encodage des messages de commit
326 field_commit_logs_encoding: Encodage des messages de commit
327 field_repository_is_default: Dépôt principal
327 field_repository_is_default: Dépôt principal
328 field_multiple: Valeurs multiples
328 field_multiple: Valeurs multiples
329 field_auth_source_ldap_filter: Filtre LDAP
329 field_auth_source_ldap_filter: Filtre LDAP
330 field_core_fields: Champs standards
330 field_core_fields: Champs standards
331 field_timeout: "Timeout (en secondes)"
331 field_timeout: "Timeout (en secondes)"
332 field_board_parent: Forum parent
332 field_board_parent: Forum parent
333
333
334 setting_app_title: Titre de l'application
334 setting_app_title: Titre de l'application
335 setting_app_subtitle: Sous-titre de l'application
335 setting_app_subtitle: Sous-titre de l'application
336 setting_welcome_text: Texte d'accueil
336 setting_welcome_text: Texte d'accueil
337 setting_default_language: Langue par défaut
337 setting_default_language: Langue par défaut
338 setting_login_required: Authentification obligatoire
338 setting_login_required: Authentification obligatoire
339 setting_self_registration: Inscription des nouveaux utilisateurs
339 setting_self_registration: Inscription des nouveaux utilisateurs
340 setting_attachment_max_size: Taille maximale des fichiers
340 setting_attachment_max_size: Taille maximale des fichiers
341 setting_issues_export_limit: Limite d'exportation des demandes
341 setting_issues_export_limit: Limite d'exportation des demandes
342 setting_mail_from: Adresse d'émission
342 setting_mail_from: Adresse d'émission
343 setting_bcc_recipients: Destinataires en copie cachée (cci)
343 setting_bcc_recipients: Destinataires en copie cachée (cci)
344 setting_plain_text_mail: Mail en texte brut (non HTML)
344 setting_plain_text_mail: Mail en texte brut (non HTML)
345 setting_host_name: Nom d'hôte et chemin
345 setting_host_name: Nom d'hôte et chemin
346 setting_text_formatting: Formatage du texte
346 setting_text_formatting: Formatage du texte
347 setting_wiki_compression: Compression de l'historique des pages wiki
347 setting_wiki_compression: Compression de l'historique des pages wiki
348 setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom
348 setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom
349 setting_default_projects_public: Définir les nouveaux projets comme publics par défaut
349 setting_default_projects_public: Définir les nouveaux projets comme publics par défaut
350 setting_autofetch_changesets: Récupération automatique des commits
350 setting_autofetch_changesets: Récupération automatique des commits
351 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
351 setting_sys_api_enabled: Activer les WS pour la gestion des dépôts
352 setting_commit_ref_keywords: Mots-clés de référencement
352 setting_commit_ref_keywords: Mots-clés de référencement
353 setting_commit_fix_keywords: Mots-clés de résolution
353 setting_commit_fix_keywords: Mots-clés de résolution
354 setting_autologin: Durée maximale de connexion automatique
354 setting_autologin: Durée maximale de connexion automatique
355 setting_date_format: Format de date
355 setting_date_format: Format de date
356 setting_time_format: Format d'heure
356 setting_time_format: Format d'heure
357 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
357 setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets
358 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
358 setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes
359 setting_emails_footer: Pied-de-page des emails
359 setting_emails_footer: Pied-de-page des emails
360 setting_protocol: Protocole
360 setting_protocol: Protocole
361 setting_per_page_options: Options d'objets affichés par page
361 setting_per_page_options: Options d'objets affichés par page
362 setting_user_format: Format d'affichage des utilisateurs
362 setting_user_format: Format d'affichage des utilisateurs
363 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
363 setting_activity_days_default: Nombre de jours affichés sur l'activité des projets
364 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
364 setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux
365 setting_enabled_scm: SCM activés
365 setting_enabled_scm: SCM activés
366 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
366 setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes"
367 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
367 setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails"
368 setting_mail_handler_api_key: Clé de protection de l'API
368 setting_mail_handler_api_key: Clé de protection de l'API
369 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
369 setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels
370 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
370 setting_gravatar_enabled: Afficher les Gravatar des utilisateurs
371 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
371 setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées
372 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
372 setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne
373 setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier"
373 setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier"
374 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
374 setting_openid: "Autoriser l'authentification et l'enregistrement OpenID"
375 setting_password_min_length: Longueur minimum des mots de passe
375 setting_password_min_length: Longueur minimum des mots de passe
376 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
376 setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet
377 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
377 setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets
378 setting_issue_done_ratio: Calcul de l'avancement des demandes
378 setting_issue_done_ratio: Calcul de l'avancement des demandes
379 setting_issue_done_ratio_issue_status: Utiliser le statut
379 setting_issue_done_ratio_issue_status: Utiliser le statut
380 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué'
380 setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué'
381 setting_rest_api_enabled: Activer l'API REST
381 setting_rest_api_enabled: Activer l'API REST
382 setting_gravatar_default: Image Gravatar par défaut
382 setting_gravatar_default: Image Gravatar par défaut
383 setting_start_of_week: Jour de début des calendriers
383 setting_start_of_week: Jour de début des calendriers
384 setting_cache_formatted_text: Mettre en cache le texte formaté
384 setting_cache_formatted_text: Mettre en cache le texte formaté
385 setting_commit_logtime_enabled: Permettre la saisie de temps
385 setting_commit_logtime_enabled: Permettre la saisie de temps
386 setting_commit_logtime_activity_id: Activité pour le temps saisi
386 setting_commit_logtime_activity_id: Activité pour le temps saisi
387 setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt
387 setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt
388 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
388 setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes
389 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
389 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
390 setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets
390 setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets
391 setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
391 setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte
392 setting_session_lifetime: Durée de vie maximale des sessions
392 setting_session_lifetime: Durée de vie maximale des sessions
393 setting_session_timeout: Durée maximale d'inactivité
393 setting_session_timeout: Durée maximale d'inactivité
394 setting_thumbnails_enabled: Afficher les vignettes des images
394 setting_thumbnails_enabled: Afficher les vignettes des images
395 setting_thumbnails_size: Taille des vignettes (en pixels)
395 setting_thumbnails_size: Taille des vignettes (en pixels)
396
396
397 permission_add_project: Créer un projet
397 permission_add_project: Créer un projet
398 permission_add_subprojects: Créer des sous-projets
398 permission_add_subprojects: Créer des sous-projets
399 permission_edit_project: Modifier le projet
399 permission_edit_project: Modifier le projet
400 permission_close_project: Fermer / réouvrir le projet
400 permission_close_project: Fermer / réouvrir le projet
401 permission_select_project_modules: Choisir les modules
401 permission_select_project_modules: Choisir les modules
402 permission_manage_members: Gérer les membres
402 permission_manage_members: Gérer les membres
403 permission_manage_versions: Gérer les versions
403 permission_manage_versions: Gérer les versions
404 permission_manage_categories: Gérer les catégories de demandes
404 permission_manage_categories: Gérer les catégories de demandes
405 permission_view_issues: Voir les demandes
405 permission_view_issues: Voir les demandes
406 permission_add_issues: Créer des demandes
406 permission_add_issues: Créer des demandes
407 permission_edit_issues: Modifier les demandes
407 permission_edit_issues: Modifier les demandes
408 permission_manage_issue_relations: Gérer les relations
408 permission_manage_issue_relations: Gérer les relations
409 permission_set_issues_private: Rendre les demandes publiques ou privées
409 permission_set_issues_private: Rendre les demandes publiques ou privées
410 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
410 permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées
411 permission_add_issue_notes: Ajouter des notes
411 permission_add_issue_notes: Ajouter des notes
412 permission_edit_issue_notes: Modifier les notes
412 permission_edit_issue_notes: Modifier les notes
413 permission_edit_own_issue_notes: Modifier ses propres notes
413 permission_edit_own_issue_notes: Modifier ses propres notes
414 permission_move_issues: Déplacer les demandes
414 permission_move_issues: Déplacer les demandes
415 permission_delete_issues: Supprimer les demandes
415 permission_delete_issues: Supprimer les demandes
416 permission_manage_public_queries: Gérer les requêtes publiques
416 permission_manage_public_queries: Gérer les requêtes publiques
417 permission_save_queries: Sauvegarder les requêtes
417 permission_save_queries: Sauvegarder les requêtes
418 permission_view_gantt: Voir le gantt
418 permission_view_gantt: Voir le gantt
419 permission_view_calendar: Voir le calendrier
419 permission_view_calendar: Voir le calendrier
420 permission_view_issue_watchers: Voir la liste des observateurs
420 permission_view_issue_watchers: Voir la liste des observateurs
421 permission_add_issue_watchers: Ajouter des observateurs
421 permission_add_issue_watchers: Ajouter des observateurs
422 permission_delete_issue_watchers: Supprimer des observateurs
422 permission_delete_issue_watchers: Supprimer des observateurs
423 permission_log_time: Saisir le temps passé
423 permission_log_time: Saisir le temps passé
424 permission_view_time_entries: Voir le temps passé
424 permission_view_time_entries: Voir le temps passé
425 permission_edit_time_entries: Modifier les temps passés
425 permission_edit_time_entries: Modifier les temps passés
426 permission_edit_own_time_entries: Modifier son propre temps passé
426 permission_edit_own_time_entries: Modifier son propre temps passé
427 permission_manage_news: Gérer les annonces
427 permission_manage_news: Gérer les annonces
428 permission_comment_news: Commenter les annonces
428 permission_comment_news: Commenter les annonces
429 permission_manage_documents: Gérer les documents
429 permission_manage_documents: Gérer les documents
430 permission_view_documents: Voir les documents
430 permission_view_documents: Voir les documents
431 permission_manage_files: Gérer les fichiers
431 permission_manage_files: Gérer les fichiers
432 permission_view_files: Voir les fichiers
432 permission_view_files: Voir les fichiers
433 permission_manage_wiki: Gérer le wiki
433 permission_manage_wiki: Gérer le wiki
434 permission_rename_wiki_pages: Renommer les pages
434 permission_rename_wiki_pages: Renommer les pages
435 permission_delete_wiki_pages: Supprimer les pages
435 permission_delete_wiki_pages: Supprimer les pages
436 permission_view_wiki_pages: Voir le wiki
436 permission_view_wiki_pages: Voir le wiki
437 permission_view_wiki_edits: "Voir l'historique des modifications"
437 permission_view_wiki_edits: "Voir l'historique des modifications"
438 permission_edit_wiki_pages: Modifier les pages
438 permission_edit_wiki_pages: Modifier les pages
439 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
439 permission_delete_wiki_pages_attachments: Supprimer les fichiers joints
440 permission_protect_wiki_pages: Protéger les pages
440 permission_protect_wiki_pages: Protéger les pages
441 permission_manage_repository: Gérer le dépôt de sources
441 permission_manage_repository: Gérer le dépôt de sources
442 permission_browse_repository: Parcourir les sources
442 permission_browse_repository: Parcourir les sources
443 permission_view_changesets: Voir les révisions
443 permission_view_changesets: Voir les révisions
444 permission_commit_access: Droit de commit
444 permission_commit_access: Droit de commit
445 permission_manage_boards: Gérer les forums
445 permission_manage_boards: Gérer les forums
446 permission_view_messages: Voir les messages
446 permission_view_messages: Voir les messages
447 permission_add_messages: Poster un message
447 permission_add_messages: Poster un message
448 permission_edit_messages: Modifier les messages
448 permission_edit_messages: Modifier les messages
449 permission_edit_own_messages: Modifier ses propres messages
449 permission_edit_own_messages: Modifier ses propres messages
450 permission_delete_messages: Supprimer les messages
450 permission_delete_messages: Supprimer les messages
451 permission_delete_own_messages: Supprimer ses propres messages
451 permission_delete_own_messages: Supprimer ses propres messages
452 permission_export_wiki_pages: Exporter les pages
452 permission_export_wiki_pages: Exporter les pages
453 permission_manage_project_activities: Gérer les activités
453 permission_manage_project_activities: Gérer les activités
454 permission_manage_subtasks: Gérer les sous-tâches
454 permission_manage_subtasks: Gérer les sous-tâches
455 permission_manage_related_issues: Gérer les demandes associées
455 permission_manage_related_issues: Gérer les demandes associées
456
456
457 project_module_issue_tracking: Suivi des demandes
457 project_module_issue_tracking: Suivi des demandes
458 project_module_time_tracking: Suivi du temps passé
458 project_module_time_tracking: Suivi du temps passé
459 project_module_news: Publication d'annonces
459 project_module_news: Publication d'annonces
460 project_module_documents: Publication de documents
460 project_module_documents: Publication de documents
461 project_module_files: Publication de fichiers
461 project_module_files: Publication de fichiers
462 project_module_wiki: Wiki
462 project_module_wiki: Wiki
463 project_module_repository: Dépôt de sources
463 project_module_repository: Dépôt de sources
464 project_module_boards: Forums de discussion
464 project_module_boards: Forums de discussion
465
465
466 label_user: Utilisateur
466 label_user: Utilisateur
467 label_user_plural: Utilisateurs
467 label_user_plural: Utilisateurs
468 label_user_new: Nouvel utilisateur
468 label_user_new: Nouvel utilisateur
469 label_user_anonymous: Anonyme
469 label_user_anonymous: Anonyme
470 label_project: Projet
470 label_project: Projet
471 label_project_new: Nouveau projet
471 label_project_new: Nouveau projet
472 label_project_plural: Projets
472 label_project_plural: Projets
473 label_x_projects:
473 label_x_projects:
474 zero: aucun projet
474 zero: aucun projet
475 one: un projet
475 one: un projet
476 other: "%{count} projets"
476 other: "%{count} projets"
477 label_project_all: Tous les projets
477 label_project_all: Tous les projets
478 label_project_latest: Derniers projets
478 label_project_latest: Derniers projets
479 label_issue: Demande
479 label_issue: Demande
480 label_issue_new: Nouvelle demande
480 label_issue_new: Nouvelle demande
481 label_issue_plural: Demandes
481 label_issue_plural: Demandes
482 label_issue_view_all: Voir toutes les demandes
482 label_issue_view_all: Voir toutes les demandes
483 label_issue_added: Demande ajoutée
483 label_issue_added: Demande ajoutée
484 label_issue_updated: Demande mise à jour
484 label_issue_updated: Demande mise à jour
485 label_issue_note_added: Note ajoutée
485 label_issue_note_added: Note ajoutée
486 label_issue_status_updated: Statut changé
486 label_issue_status_updated: Statut changé
487 label_issue_priority_updated: Priorité changée
487 label_issue_priority_updated: Priorité changée
488 label_issues_by: "Demandes par %{value}"
488 label_issues_by: "Demandes par %{value}"
489 label_document: Document
489 label_document: Document
490 label_document_new: Nouveau document
490 label_document_new: Nouveau document
491 label_document_plural: Documents
491 label_document_plural: Documents
492 label_document_added: Document ajouté
492 label_document_added: Document ajouté
493 label_role: Rôle
493 label_role: Rôle
494 label_role_plural: Rôles
494 label_role_plural: Rôles
495 label_role_new: Nouveau rôle
495 label_role_new: Nouveau rôle
496 label_role_and_permissions: Rôles et permissions
496 label_role_and_permissions: Rôles et permissions
497 label_role_anonymous: Anonyme
497 label_role_anonymous: Anonyme
498 label_role_non_member: Non membre
498 label_role_non_member: Non membre
499 label_member: Membre
499 label_member: Membre
500 label_member_new: Nouveau membre
500 label_member_new: Nouveau membre
501 label_member_plural: Membres
501 label_member_plural: Membres
502 label_tracker: Tracker
502 label_tracker: Tracker
503 label_tracker_plural: Trackers
503 label_tracker_plural: Trackers
504 label_tracker_new: Nouveau tracker
504 label_tracker_new: Nouveau tracker
505 label_workflow: Workflow
505 label_workflow: Workflow
506 label_issue_status: Statut de demandes
506 label_issue_status: Statut de demandes
507 label_issue_status_plural: Statuts de demandes
507 label_issue_status_plural: Statuts de demandes
508 label_issue_status_new: Nouveau statut
508 label_issue_status_new: Nouveau statut
509 label_issue_category: Catégorie de demandes
509 label_issue_category: Catégorie de demandes
510 label_issue_category_plural: Catégories de demandes
510 label_issue_category_plural: Catégories de demandes
511 label_issue_category_new: Nouvelle catégorie
511 label_issue_category_new: Nouvelle catégorie
512 label_custom_field: Champ personnalisé
512 label_custom_field: Champ personnalisé
513 label_custom_field_plural: Champs personnalisés
513 label_custom_field_plural: Champs personnalisés
514 label_custom_field_new: Nouveau champ personnalisé
514 label_custom_field_new: Nouveau champ personnalisé
515 label_enumerations: Listes de valeurs
515 label_enumerations: Listes de valeurs
516 label_enumeration_new: Nouvelle valeur
516 label_enumeration_new: Nouvelle valeur
517 label_information: Information
517 label_information: Information
518 label_information_plural: Informations
518 label_information_plural: Informations
519 label_please_login: Identification
519 label_please_login: Identification
520 label_register: S'enregistrer
520 label_register: S'enregistrer
521 label_login_with_open_id_option: S'authentifier avec OpenID
521 label_login_with_open_id_option: S'authentifier avec OpenID
522 label_password_lost: Mot de passe perdu
522 label_password_lost: Mot de passe perdu
523 label_home: Accueil
523 label_home: Accueil
524 label_my_page: Ma page
524 label_my_page: Ma page
525 label_my_account: Mon compte
525 label_my_account: Mon compte
526 label_my_projects: Mes projets
526 label_my_projects: Mes projets
527 label_my_page_block: Blocs disponibles
527 label_my_page_block: Blocs disponibles
528 label_administration: Administration
528 label_administration: Administration
529 label_login: Connexion
529 label_login: Connexion
530 label_logout: Déconnexion
530 label_logout: Déconnexion
531 label_help: Aide
531 label_help: Aide
532 label_reported_issues: "Demandes soumises "
532 label_reported_issues: "Demandes soumises "
533 label_assigned_to_me_issues: Demandes qui me sont assignées
533 label_assigned_to_me_issues: Demandes qui me sont assignées
534 label_last_login: "Dernière connexion "
534 label_last_login: "Dernière connexion "
535 label_registered_on: "Inscrit le "
535 label_registered_on: "Inscrit le "
536 label_activity: Activité
536 label_activity: Activité
537 label_overall_activity: Activité globale
537 label_overall_activity: Activité globale
538 label_user_activity: "Activité de %{value}"
538 label_user_activity: "Activité de %{value}"
539 label_new: Nouveau
539 label_new: Nouveau
540 label_logged_as: Connecté en tant que
540 label_logged_as: Connecté en tant que
541 label_environment: Environnement
541 label_environment: Environnement
542 label_authentication: Authentification
542 label_authentication: Authentification
543 label_auth_source: Mode d'authentification
543 label_auth_source: Mode d'authentification
544 label_auth_source_new: Nouveau mode d'authentification
544 label_auth_source_new: Nouveau mode d'authentification
545 label_auth_source_plural: Modes d'authentification
545 label_auth_source_plural: Modes d'authentification
546 label_subproject_plural: Sous-projets
546 label_subproject_plural: Sous-projets
547 label_subproject_new: Nouveau sous-projet
547 label_subproject_new: Nouveau sous-projet
548 label_and_its_subprojects: "%{value} et ses sous-projets"
548 label_and_its_subprojects: "%{value} et ses sous-projets"
549 label_min_max_length: Longueurs mini - maxi
549 label_min_max_length: Longueurs mini - maxi
550 label_list: Liste
550 label_list: Liste
551 label_date: Date
551 label_date: Date
552 label_integer: Entier
552 label_integer: Entier
553 label_float: Nombre décimal
553 label_float: Nombre décimal
554 label_boolean: Booléen
554 label_boolean: Booléen
555 label_string: Texte
555 label_string: Texte
556 label_text: Texte long
556 label_text: Texte long
557 label_attribute: Attribut
557 label_attribute: Attribut
558 label_attribute_plural: Attributs
558 label_attribute_plural: Attributs
559 label_download: "%{count} téléchargement"
559 label_download: "%{count} téléchargement"
560 label_download_plural: "%{count} téléchargements"
560 label_download_plural: "%{count} téléchargements"
561 label_no_data: Aucune donnée à afficher
561 label_no_data: Aucune donnée à afficher
562 label_change_status: Changer le statut
562 label_change_status: Changer le statut
563 label_history: Historique
563 label_history: Historique
564 label_attachment: Fichier
564 label_attachment: Fichier
565 label_attachment_new: Nouveau fichier
565 label_attachment_new: Nouveau fichier
566 label_attachment_delete: Supprimer le fichier
566 label_attachment_delete: Supprimer le fichier
567 label_attachment_plural: Fichiers
567 label_attachment_plural: Fichiers
568 label_file_added: Fichier ajouté
568 label_file_added: Fichier ajouté
569 label_report: Rapport
569 label_report: Rapport
570 label_report_plural: Rapports
570 label_report_plural: Rapports
571 label_news: Annonce
571 label_news: Annonce
572 label_news_new: Nouvelle annonce
572 label_news_new: Nouvelle annonce
573 label_news_plural: Annonces
573 label_news_plural: Annonces
574 label_news_latest: Dernières annonces
574 label_news_latest: Dernières annonces
575 label_news_view_all: Voir toutes les annonces
575 label_news_view_all: Voir toutes les annonces
576 label_news_added: Annonce ajoutée
576 label_news_added: Annonce ajoutée
577 label_news_comment_added: Commentaire ajouté à une annonce
577 label_news_comment_added: Commentaire ajouté à une annonce
578 label_settings: Configuration
578 label_settings: Configuration
579 label_overview: Aperçu
579 label_overview: Aperçu
580 label_version: Version
580 label_version: Version
581 label_version_new: Nouvelle version
581 label_version_new: Nouvelle version
582 label_version_plural: Versions
582 label_version_plural: Versions
583 label_confirmation: Confirmation
583 label_confirmation: Confirmation
584 label_export_to: 'Formats disponibles :'
584 label_export_to: 'Formats disponibles :'
585 label_read: Lire...
585 label_read: Lire...
586 label_public_projects: Projets publics
586 label_public_projects: Projets publics
587 label_open_issues: ouvert
587 label_open_issues: ouvert
588 label_open_issues_plural: ouverts
588 label_open_issues_plural: ouverts
589 label_closed_issues: fermé
589 label_closed_issues: fermé
590 label_closed_issues_plural: fermés
590 label_closed_issues_plural: fermés
591 label_x_open_issues_abbr_on_total:
591 label_x_open_issues_abbr_on_total:
592 zero: 0 ouverte sur %{total}
592 zero: 0 ouverte sur %{total}
593 one: 1 ouverte sur %{total}
593 one: 1 ouverte sur %{total}
594 other: "%{count} ouvertes sur %{total}"
594 other: "%{count} ouvertes sur %{total}"
595 label_x_open_issues_abbr:
595 label_x_open_issues_abbr:
596 zero: 0 ouverte
596 zero: 0 ouverte
597 one: 1 ouverte
597 one: 1 ouverte
598 other: "%{count} ouvertes"
598 other: "%{count} ouvertes"
599 label_x_closed_issues_abbr:
599 label_x_closed_issues_abbr:
600 zero: 0 fermée
600 zero: 0 fermée
601 one: 1 fermée
601 one: 1 fermée
602 other: "%{count} fermées"
602 other: "%{count} fermées"
603 label_x_issues:
603 label_x_issues:
604 zero: 0 demande
604 zero: 0 demande
605 one: 1 demande
605 one: 1 demande
606 other: "%{count} demandes"
606 other: "%{count} demandes"
607 label_total: Total
607 label_total: Total
608 label_permissions: Permissions
608 label_permissions: Permissions
609 label_current_status: Statut actuel
609 label_current_status: Statut actuel
610 label_new_statuses_allowed: Nouveaux statuts autorisés
610 label_new_statuses_allowed: Nouveaux statuts autorisés
611 label_all: tous
611 label_all: tous
612 label_none: aucun
612 label_none: aucun
613 label_nobody: personne
613 label_nobody: personne
614 label_next: Suivant
614 label_next: Suivant
615 label_previous: Précédent
615 label_previous: Précédent
616 label_used_by: Utilisé par
616 label_used_by: Utilisé par
617 label_details: Détails
617 label_details: Détails
618 label_add_note: Ajouter une note
618 label_add_note: Ajouter une note
619 label_per_page: Par page
619 label_per_page: Par page
620 label_calendar: Calendrier
620 label_calendar: Calendrier
621 label_months_from: mois depuis
621 label_months_from: mois depuis
622 label_gantt: Gantt
622 label_gantt: Gantt
623 label_internal: Interne
623 label_internal: Interne
624 label_last_changes: "%{count} derniers changements"
624 label_last_changes: "%{count} derniers changements"
625 label_change_view_all: Voir tous les changements
625 label_change_view_all: Voir tous les changements
626 label_personalize_page: Personnaliser cette page
626 label_personalize_page: Personnaliser cette page
627 label_comment: Commentaire
627 label_comment: Commentaire
628 label_comment_plural: Commentaires
628 label_comment_plural: Commentaires
629 label_x_comments:
629 label_x_comments:
630 zero: aucun commentaire
630 zero: aucun commentaire
631 one: un commentaire
631 one: un commentaire
632 other: "%{count} commentaires"
632 other: "%{count} commentaires"
633 label_comment_add: Ajouter un commentaire
633 label_comment_add: Ajouter un commentaire
634 label_comment_added: Commentaire ajouté
634 label_comment_added: Commentaire ajouté
635 label_comment_delete: Supprimer les commentaires
635 label_comment_delete: Supprimer les commentaires
636 label_query: Rapport personnalisé
636 label_query: Rapport personnalisé
637 label_query_plural: Rapports personnalisés
637 label_query_plural: Rapports personnalisés
638 label_query_new: Nouveau rapport
638 label_query_new: Nouveau rapport
639 label_my_queries: Mes rapports personnalisés
639 label_my_queries: Mes rapports personnalisés
640 label_filter_add: "Ajouter le filtre "
640 label_filter_add: "Ajouter le filtre "
641 label_filter_plural: Filtres
641 label_filter_plural: Filtres
642 label_equals: égal
642 label_equals: égal
643 label_not_equals: différent
643 label_not_equals: différent
644 label_in_less_than: dans moins de
644 label_in_less_than: dans moins de
645 label_in_more_than: dans plus de
645 label_in_more_than: dans plus de
646 label_in: dans
646 label_in: dans
647 label_today: aujourd'hui
647 label_today: aujourd'hui
648 label_all_time: toute la période
648 label_all_time: toute la période
649 label_yesterday: hier
649 label_yesterday: hier
650 label_this_week: cette semaine
650 label_this_week: cette semaine
651 label_last_week: la semaine dernière
651 label_last_week: la semaine dernière
652 label_last_n_days: "les %{count} derniers jours"
652 label_last_n_days: "les %{count} derniers jours"
653 label_this_month: ce mois-ci
653 label_this_month: ce mois-ci
654 label_last_month: le mois dernier
654 label_last_month: le mois dernier
655 label_this_year: cette année
655 label_this_year: cette année
656 label_date_range: Période
656 label_date_range: Période
657 label_less_than_ago: il y a moins de
657 label_less_than_ago: il y a moins de
658 label_more_than_ago: il y a plus de
658 label_more_than_ago: il y a plus de
659 label_ago: il y a
659 label_ago: il y a
660 label_contains: contient
660 label_contains: contient
661 label_not_contains: ne contient pas
661 label_not_contains: ne contient pas
662 label_any_issues_in_project: une demande du projet
663 label_any_issues_not_in_project: une demande hors du projet
662 label_day_plural: jours
664 label_day_plural: jours
663 label_repository: Dépôt
665 label_repository: Dépôt
664 label_repository_new: Nouveau dépôt
666 label_repository_new: Nouveau dépôt
665 label_repository_plural: Dépôts
667 label_repository_plural: Dépôts
666 label_browse: Parcourir
668 label_browse: Parcourir
667 label_modification: "%{count} modification"
669 label_modification: "%{count} modification"
668 label_modification_plural: "%{count} modifications"
670 label_modification_plural: "%{count} modifications"
669 label_revision: "Révision "
671 label_revision: "Révision "
670 label_revision_plural: Révisions
672 label_revision_plural: Révisions
671 label_associated_revisions: Révisions associées
673 label_associated_revisions: Révisions associées
672 label_added: ajouté
674 label_added: ajouté
673 label_modified: modifié
675 label_modified: modifié
674 label_copied: copié
676 label_copied: copié
675 label_renamed: renommé
677 label_renamed: renommé
676 label_deleted: supprimé
678 label_deleted: supprimé
677 label_latest_revision: Dernière révision
679 label_latest_revision: Dernière révision
678 label_latest_revision_plural: Dernières révisions
680 label_latest_revision_plural: Dernières révisions
679 label_view_revisions: Voir les révisions
681 label_view_revisions: Voir les révisions
680 label_max_size: Taille maximale
682 label_max_size: Taille maximale
681 label_sort_highest: Remonter en premier
683 label_sort_highest: Remonter en premier
682 label_sort_higher: Remonter
684 label_sort_higher: Remonter
683 label_sort_lower: Descendre
685 label_sort_lower: Descendre
684 label_sort_lowest: Descendre en dernier
686 label_sort_lowest: Descendre en dernier
685 label_roadmap: Roadmap
687 label_roadmap: Roadmap
686 label_roadmap_due_in: "Échéance dans %{value}"
688 label_roadmap_due_in: "Échéance dans %{value}"
687 label_roadmap_overdue: "En retard de %{value}"
689 label_roadmap_overdue: "En retard de %{value}"
688 label_roadmap_no_issues: Aucune demande pour cette version
690 label_roadmap_no_issues: Aucune demande pour cette version
689 label_search: "Recherche "
691 label_search: "Recherche "
690 label_result_plural: Résultats
692 label_result_plural: Résultats
691 label_all_words: Tous les mots
693 label_all_words: Tous les mots
692 label_wiki: Wiki
694 label_wiki: Wiki
693 label_wiki_edit: Révision wiki
695 label_wiki_edit: Révision wiki
694 label_wiki_edit_plural: Révisions wiki
696 label_wiki_edit_plural: Révisions wiki
695 label_wiki_page: Page wiki
697 label_wiki_page: Page wiki
696 label_wiki_page_plural: Pages wiki
698 label_wiki_page_plural: Pages wiki
697 label_index_by_title: Index par titre
699 label_index_by_title: Index par titre
698 label_index_by_date: Index par date
700 label_index_by_date: Index par date
699 label_current_version: Version actuelle
701 label_current_version: Version actuelle
700 label_preview: Prévisualisation
702 label_preview: Prévisualisation
701 label_feed_plural: Flux RSS
703 label_feed_plural: Flux RSS
702 label_changes_details: Détails de tous les changements
704 label_changes_details: Détails de tous les changements
703 label_issue_tracking: Suivi des demandes
705 label_issue_tracking: Suivi des demandes
704 label_spent_time: Temps passé
706 label_spent_time: Temps passé
705 label_f_hour: "%{value} heure"
707 label_f_hour: "%{value} heure"
706 label_f_hour_plural: "%{value} heures"
708 label_f_hour_plural: "%{value} heures"
707 label_time_tracking: Suivi du temps
709 label_time_tracking: Suivi du temps
708 label_change_plural: Changements
710 label_change_plural: Changements
709 label_statistics: Statistiques
711 label_statistics: Statistiques
710 label_commits_per_month: Commits par mois
712 label_commits_per_month: Commits par mois
711 label_commits_per_author: Commits par auteur
713 label_commits_per_author: Commits par auteur
712 label_view_diff: Voir les différences
714 label_view_diff: Voir les différences
713 label_diff_inline: en ligne
715 label_diff_inline: en ligne
714 label_diff_side_by_side: côte à côte
716 label_diff_side_by_side: côte à côte
715 label_options: Options
717 label_options: Options
716 label_copy_workflow_from: Copier le workflow de
718 label_copy_workflow_from: Copier le workflow de
717 label_permissions_report: Synthèse des permissions
719 label_permissions_report: Synthèse des permissions
718 label_watched_issues: Demandes surveillées
720 label_watched_issues: Demandes surveillées
719 label_related_issues: Demandes liées
721 label_related_issues: Demandes liées
720 label_applied_status: Statut appliqué
722 label_applied_status: Statut appliqué
721 label_loading: Chargement...
723 label_loading: Chargement...
722 label_relation_new: Nouvelle relation
724 label_relation_new: Nouvelle relation
723 label_relation_delete: Supprimer la relation
725 label_relation_delete: Supprimer la relation
724 label_relates_to: lié à
726 label_relates_to: Lié à
725 label_duplicates: duplique
727 label_duplicates: Duplique
726 label_duplicated_by: dupliqué par
728 label_duplicated_by: Dupliqué par
727 label_blocks: bloque
729 label_blocks: Bloque
728 label_blocked_by: bloqué par
730 label_blocked_by: Bloqué par
729 label_precedes: précède
731 label_precedes: Précède
730 label_follows: suit
732 label_follows: Suit
731 label_copied_to: copié vers
733 label_copied_to: Copié vers
732 label_copied_from: copié depuis
734 label_copied_from: Copié depuis
733 label_end_to_start: fin à début
735 label_end_to_start: fin à début
734 label_end_to_end: fin à fin
736 label_end_to_end: fin à fin
735 label_start_to_start: début à début
737 label_start_to_start: début à début
736 label_start_to_end: début à fin
738 label_start_to_end: début à fin
737 label_stay_logged_in: Rester connecté
739 label_stay_logged_in: Rester connecté
738 label_disabled: désactivé
740 label_disabled: désactivé
739 label_show_completed_versions: Voir les versions passées
741 label_show_completed_versions: Voir les versions passées
740 label_me: moi
742 label_me: moi
741 label_board: Forum
743 label_board: Forum
742 label_board_new: Nouveau forum
744 label_board_new: Nouveau forum
743 label_board_plural: Forums
745 label_board_plural: Forums
744 label_topic_plural: Discussions
746 label_topic_plural: Discussions
745 label_message_plural: Messages
747 label_message_plural: Messages
746 label_message_last: Dernier message
748 label_message_last: Dernier message
747 label_message_new: Nouveau message
749 label_message_new: Nouveau message
748 label_message_posted: Message ajouté
750 label_message_posted: Message ajouté
749 label_reply_plural: Réponses
751 label_reply_plural: Réponses
750 label_send_information: Envoyer les informations à l'utilisateur
752 label_send_information: Envoyer les informations à l'utilisateur
751 label_year: Année
753 label_year: Année
752 label_month: Mois
754 label_month: Mois
753 label_week: Semaine
755 label_week: Semaine
754 label_date_from: Du
756 label_date_from: Du
755 label_date_to: Au
757 label_date_to: Au
756 label_language_based: Basé sur la langue de l'utilisateur
758 label_language_based: Basé sur la langue de l'utilisateur
757 label_sort_by: "Trier par %{value}"
759 label_sort_by: "Trier par %{value}"
758 label_send_test_email: Envoyer un email de test
760 label_send_test_email: Envoyer un email de test
759 label_feeds_access_key_created_on: "Clé d'accès RSS créée il y a %{value}"
761 label_feeds_access_key_created_on: "Clé d'accès RSS créée il y a %{value}"
760 label_module_plural: Modules
762 label_module_plural: Modules
761 label_added_time_by: "Ajouté par %{author} il y a %{age}"
763 label_added_time_by: "Ajouté par %{author} il y a %{age}"
762 label_updated_time_by: "Mis à jour par %{author} il y a %{age}"
764 label_updated_time_by: "Mis à jour par %{author} il y a %{age}"
763 label_updated_time: "Mis à jour il y a %{value}"
765 label_updated_time: "Mis à jour il y a %{value}"
764 label_jump_to_a_project: Aller à un projet...
766 label_jump_to_a_project: Aller à un projet...
765 label_file_plural: Fichiers
767 label_file_plural: Fichiers
766 label_changeset_plural: Révisions
768 label_changeset_plural: Révisions
767 label_default_columns: Colonnes par défaut
769 label_default_columns: Colonnes par défaut
768 label_no_change_option: (Pas de changement)
770 label_no_change_option: (Pas de changement)
769 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
771 label_bulk_edit_selected_issues: Modifier les demandes sélectionnées
770 label_theme: Thème
772 label_theme: Thème
771 label_default: Défaut
773 label_default: Défaut
772 label_search_titles_only: Uniquement dans les titres
774 label_search_titles_only: Uniquement dans les titres
773 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
775 label_user_mail_option_all: "Pour tous les événements de tous mes projets"
774 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
776 label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..."
775 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
777 label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue"
776 label_registration_activation_by_email: activation du compte par email
778 label_registration_activation_by_email: activation du compte par email
777 label_registration_manual_activation: activation manuelle du compte
779 label_registration_manual_activation: activation manuelle du compte
778 label_registration_automatic_activation: activation automatique du compte
780 label_registration_automatic_activation: activation automatique du compte
779 label_display_per_page: "Par page : %{value}"
781 label_display_per_page: "Par page : %{value}"
780 label_age: Âge
782 label_age: Âge
781 label_change_properties: Changer les propriétés
783 label_change_properties: Changer les propriétés
782 label_general: Général
784 label_general: Général
783 label_more: Plus
785 label_more: Plus
784 label_scm: SCM
786 label_scm: SCM
785 label_plugins: Plugins
787 label_plugins: Plugins
786 label_ldap_authentication: Authentification LDAP
788 label_ldap_authentication: Authentification LDAP
787 label_downloads_abbr: D/L
789 label_downloads_abbr: D/L
788 label_optional_description: Description facultative
790 label_optional_description: Description facultative
789 label_add_another_file: Ajouter un autre fichier
791 label_add_another_file: Ajouter un autre fichier
790 label_preferences: Préférences
792 label_preferences: Préférences
791 label_chronological_order: Dans l'ordre chronologique
793 label_chronological_order: Dans l'ordre chronologique
792 label_reverse_chronological_order: Dans l'ordre chronologique inverse
794 label_reverse_chronological_order: Dans l'ordre chronologique inverse
793 label_planning: Planning
795 label_planning: Planning
794 label_incoming_emails: Emails entrants
796 label_incoming_emails: Emails entrants
795 label_generate_key: Générer une clé
797 label_generate_key: Générer une clé
796 label_issue_watchers: Observateurs
798 label_issue_watchers: Observateurs
797 label_example: Exemple
799 label_example: Exemple
798 label_display: Affichage
800 label_display: Affichage
799 label_sort: Tri
801 label_sort: Tri
800 label_ascending: Croissant
802 label_ascending: Croissant
801 label_descending: Décroissant
803 label_descending: Décroissant
802 label_date_from_to: Du %{start} au %{end}
804 label_date_from_to: Du %{start} au %{end}
803 label_wiki_content_added: Page wiki ajoutée
805 label_wiki_content_added: Page wiki ajoutée
804 label_wiki_content_updated: Page wiki mise à jour
806 label_wiki_content_updated: Page wiki mise à jour
805 label_group_plural: Groupes
807 label_group_plural: Groupes
806 label_group: Groupe
808 label_group: Groupe
807 label_group_new: Nouveau groupe
809 label_group_new: Nouveau groupe
808 label_time_entry_plural: Temps passé
810 label_time_entry_plural: Temps passé
809 label_version_sharing_none: Non partagé
811 label_version_sharing_none: Non partagé
810 label_version_sharing_descendants: Avec les sous-projets
812 label_version_sharing_descendants: Avec les sous-projets
811 label_version_sharing_hierarchy: Avec toute la hiérarchie
813 label_version_sharing_hierarchy: Avec toute la hiérarchie
812 label_version_sharing_tree: Avec tout l'arbre
814 label_version_sharing_tree: Avec tout l'arbre
813 label_version_sharing_system: Avec tous les projets
815 label_version_sharing_system: Avec tous les projets
814 label_copy_source: Source
816 label_copy_source: Source
815 label_copy_target: Cible
817 label_copy_target: Cible
816 label_copy_same_as_target: Comme la cible
818 label_copy_same_as_target: Comme la cible
817 label_update_issue_done_ratios: Mettre à jour l'avancement des demandes
819 label_update_issue_done_ratios: Mettre à jour l'avancement des demandes
818 label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
820 label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker
819 label_api_access_key: Clé d'accès API
821 label_api_access_key: Clé d'accès API
820 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
822 label_api_access_key_created_on: Clé d'accès API créée il y a %{value}
821 label_feeds_access_key: Clé d'accès RSS
823 label_feeds_access_key: Clé d'accès RSS
822 label_missing_api_access_key: Clé d'accès API manquante
824 label_missing_api_access_key: Clé d'accès API manquante
823 label_missing_feeds_access_key: Clé d'accès RSS manquante
825 label_missing_feeds_access_key: Clé d'accès RSS manquante
824 label_close_versions: Fermer les versions terminées
826 label_close_versions: Fermer les versions terminées
825 label_revision_id: Révision %{value}
827 label_revision_id: Révision %{value}
826 label_profile: Profil
828 label_profile: Profil
827 label_subtask_plural: Sous-tâches
829 label_subtask_plural: Sous-tâches
828 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
830 label_project_copy_notifications: Envoyer les notifications durant la copie du projet
829 label_principal_search: "Rechercher un utilisateur ou un groupe :"
831 label_principal_search: "Rechercher un utilisateur ou un groupe :"
830 label_user_search: "Rechercher un utilisateur :"
832 label_user_search: "Rechercher un utilisateur :"
831 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
833 label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande
832 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
834 label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur
833 label_issues_visibility_all: Toutes les demandes
835 label_issues_visibility_all: Toutes les demandes
834 label_issues_visibility_public: Toutes les demandes non privées
836 label_issues_visibility_public: Toutes les demandes non privées
835 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
837 label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur
836 label_export_options: Options d'exportation %{export_format}
838 label_export_options: Options d'exportation %{export_format}
837 label_copy_attachments: Copier les fichiers
839 label_copy_attachments: Copier les fichiers
838 label_copy_subtasks: Copier les sous-tâches
840 label_copy_subtasks: Copier les sous-tâches
839 label_item_position: "%{position} sur %{count}"
841 label_item_position: "%{position} sur %{count}"
840 label_completed_versions: Versions passées
842 label_completed_versions: Versions passées
841 label_session_expiration: Expiration des sessions
843 label_session_expiration: Expiration des sessions
842 label_show_closed_projects: Voir les projets fermés
844 label_show_closed_projects: Voir les projets fermés
843 label_status_transitions: Changements de statut
845 label_status_transitions: Changements de statut
844 label_fields_permissions: Permissions sur les champs
846 label_fields_permissions: Permissions sur les champs
845 label_readonly: Lecture
847 label_readonly: Lecture
846 label_required: Obligatoire
848 label_required: Obligatoire
847 label_attribute_of_project: "%{name} du projet"
849 label_attribute_of_project: "%{name} du projet"
848 label_attribute_of_author: "%{name} de l'auteur"
850 label_attribute_of_author: "%{name} de l'auteur"
849 label_attribute_of_assigned_to: "%{name} de l'assigné"
851 label_attribute_of_assigned_to: "%{name} de l'assigné"
850 label_attribute_of_fixed_version: "%{name} de la version cible"
852 label_attribute_of_fixed_version: "%{name} de la version cible"
851
853
852 button_login: Connexion
854 button_login: Connexion
853 button_submit: Soumettre
855 button_submit: Soumettre
854 button_save: Sauvegarder
856 button_save: Sauvegarder
855 button_check_all: Tout cocher
857 button_check_all: Tout cocher
856 button_uncheck_all: Tout décocher
858 button_uncheck_all: Tout décocher
857 button_collapse_all: Plier tout
859 button_collapse_all: Plier tout
858 button_expand_all: Déplier tout
860 button_expand_all: Déplier tout
859 button_delete: Supprimer
861 button_delete: Supprimer
860 button_create: Créer
862 button_create: Créer
861 button_create_and_continue: Créer et continuer
863 button_create_and_continue: Créer et continuer
862 button_test: Tester
864 button_test: Tester
863 button_edit: Modifier
865 button_edit: Modifier
864 button_add: Ajouter
866 button_add: Ajouter
865 button_change: Changer
867 button_change: Changer
866 button_apply: Appliquer
868 button_apply: Appliquer
867 button_clear: Effacer
869 button_clear: Effacer
868 button_lock: Verrouiller
870 button_lock: Verrouiller
869 button_unlock: Déverrouiller
871 button_unlock: Déverrouiller
870 button_download: Télécharger
872 button_download: Télécharger
871 button_list: Lister
873 button_list: Lister
872 button_view: Voir
874 button_view: Voir
873 button_move: Déplacer
875 button_move: Déplacer
874 button_move_and_follow: Déplacer et suivre
876 button_move_and_follow: Déplacer et suivre
875 button_back: Retour
877 button_back: Retour
876 button_cancel: Annuler
878 button_cancel: Annuler
877 button_activate: Activer
879 button_activate: Activer
878 button_sort: Trier
880 button_sort: Trier
879 button_log_time: Saisir temps
881 button_log_time: Saisir temps
880 button_rollback: Revenir à cette version
882 button_rollback: Revenir à cette version
881 button_watch: Surveiller
883 button_watch: Surveiller
882 button_unwatch: Ne plus surveiller
884 button_unwatch: Ne plus surveiller
883 button_reply: Répondre
885 button_reply: Répondre
884 button_archive: Archiver
886 button_archive: Archiver
885 button_unarchive: Désarchiver
887 button_unarchive: Désarchiver
886 button_reset: Réinitialiser
888 button_reset: Réinitialiser
887 button_rename: Renommer
889 button_rename: Renommer
888 button_change_password: Changer de mot de passe
890 button_change_password: Changer de mot de passe
889 button_copy: Copier
891 button_copy: Copier
890 button_copy_and_follow: Copier et suivre
892 button_copy_and_follow: Copier et suivre
891 button_annotate: Annoter
893 button_annotate: Annoter
892 button_update: Mettre à jour
894 button_update: Mettre à jour
893 button_configure: Configurer
895 button_configure: Configurer
894 button_quote: Citer
896 button_quote: Citer
895 button_duplicate: Dupliquer
897 button_duplicate: Dupliquer
896 button_show: Afficher
898 button_show: Afficher
897 button_edit_section: Modifier cette section
899 button_edit_section: Modifier cette section
898 button_export: Exporter
900 button_export: Exporter
899 button_delete_my_account: Supprimer mon compte
901 button_delete_my_account: Supprimer mon compte
900 button_close: Fermer
902 button_close: Fermer
901 button_reopen: Réouvrir
903 button_reopen: Réouvrir
902
904
903 status_active: actif
905 status_active: actif
904 status_registered: enregistré
906 status_registered: enregistré
905 status_locked: verrouillé
907 status_locked: verrouillé
906
908
907 project_status_active: actif
909 project_status_active: actif
908 project_status_closed: fermé
910 project_status_closed: fermé
909 project_status_archived: archivé
911 project_status_archived: archivé
910
912
911 version_status_open: ouvert
913 version_status_open: ouvert
912 version_status_locked: verrouillé
914 version_status_locked: verrouillé
913 version_status_closed: fermé
915 version_status_closed: fermé
914
916
915 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
917 text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée
916 text_regexp_info: ex. ^[A-Z0-9]+$
918 text_regexp_info: ex. ^[A-Z0-9]+$
917 text_min_max_length_info: 0 pour aucune restriction
919 text_min_max_length_info: 0 pour aucune restriction
918 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
920 text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ?
919 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés."
921 text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés."
920 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
922 text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow
921 text_are_you_sure: Êtes-vous sûr ?
923 text_are_you_sure: Êtes-vous sûr ?
922 text_tip_issue_begin_day: tâche commençant ce jour
924 text_tip_issue_begin_day: tâche commençant ce jour
923 text_tip_issue_end_day: tâche finissant ce jour
925 text_tip_issue_end_day: tâche finissant ce jour
924 text_tip_issue_begin_end_day: tâche commençant et finissant ce jour
926 text_tip_issue_begin_end_day: tâche commençant et finissant ce jour
925 text_project_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é.'
927 text_project_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é.'
926 text_caracters_maximum: "%{count} caractères maximum."
928 text_caracters_maximum: "%{count} caractères maximum."
927 text_caracters_minimum: "%{count} caractères minimum."
929 text_caracters_minimum: "%{count} caractères minimum."
928 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
930 text_length_between: "Longueur comprise entre %{min} et %{max} caractères."
929 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
931 text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker
930 text_unallowed_characters: Caractères non autorisés
932 text_unallowed_characters: Caractères non autorisés
931 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
933 text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules).
932 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
934 text_line_separated: Plusieurs valeurs possibles (une valeur par ligne).
933 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
935 text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits
934 text_issue_added: "La demande %{id} a été soumise par %{author}."
936 text_issue_added: "La demande %{id} a été soumise par %{author}."
935 text_issue_updated: "La demande %{id} a été mise à jour par %{author}."
937 text_issue_updated: "La demande %{id} a été mise à jour par %{author}."
936 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
938 text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ?
937 text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?"
939 text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?"
938 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
940 text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie
939 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
941 text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie
940 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)."
942 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)."
941 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é."
943 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é."
942 text_load_default_configuration: Charger le paramétrage par défaut
944 text_load_default_configuration: Charger le paramétrage par défaut
943 text_status_changed_by_changeset: "Appliqué par commit %{value}."
945 text_status_changed_by_changeset: "Appliqué par commit %{value}."
944 text_time_logged_by_changeset: "Appliqué par commit %{value}"
946 text_time_logged_by_changeset: "Appliqué par commit %{value}"
945 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
947 text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?'
946 text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)."
948 text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)."
947 text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :'
949 text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :'
948 text_default_administrator_account_changed: Compte administrateur par défaut changé
950 text_default_administrator_account_changed: Compte administrateur par défaut changé
949 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
951 text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture
950 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
952 text_plugin_assets_writable: Répertoire public des plugins accessible en écriture
951 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
953 text_rmagick_available: Bibliothèque RMagick présente (optionnelle)
952 text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
954 text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?"
953 text_destroy_time_entries: Supprimer les heures
955 text_destroy_time_entries: Supprimer les heures
954 text_assign_time_entries_to_project: Reporter les heures sur le projet
956 text_assign_time_entries_to_project: Reporter les heures sur le projet
955 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
957 text_reassign_time_entries: 'Reporter les heures sur cette demande:'
956 text_user_wrote: "%{value} a écrit :"
958 text_user_wrote: "%{value} a écrit :"
957 text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets."
959 text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets."
958 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
960 text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:'
959 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."
961 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."
960 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."
962 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."
961 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
963 text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.'
962 text_custom_field_possible_values_info: 'Une ligne par valeur'
964 text_custom_field_possible_values_info: 'Une ligne par valeur'
963 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
965 text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?"
964 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
966 text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines"
965 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
967 text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes"
966 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
968 text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page"
967 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 ?"
969 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 ?"
968 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page."
970 text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page."
969 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)"
971 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)"
970 text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements"
972 text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements"
971 text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
973 text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}"
972 text_account_destroy_confirmation: "Êtes-vous sûr de vouloir continuer ?\nVotre compte sera définitivement supprimé, sans aucune possibilité de le réactiver."
974 text_account_destroy_confirmation: "Êtes-vous sûr de vouloir continuer ?\nVotre compte sera définitivement supprimé, sans aucune possibilité de le réactiver."
973 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."
975 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."
974 text_project_closed: Ce projet est fermé et accessible en lecture seule.
976 text_project_closed: Ce projet est fermé et accessible en lecture seule.
975
977
976 default_role_manager: "Manager "
978 default_role_manager: "Manager "
977 default_role_developer: "Développeur "
979 default_role_developer: "Développeur "
978 default_role_reporter: "Rapporteur "
980 default_role_reporter: "Rapporteur "
979 default_tracker_bug: Anomalie
981 default_tracker_bug: Anomalie
980 default_tracker_feature: Evolution
982 default_tracker_feature: Evolution
981 default_tracker_support: Assistance
983 default_tracker_support: Assistance
982 default_issue_status_new: Nouveau
984 default_issue_status_new: Nouveau
983 default_issue_status_in_progress: En cours
985 default_issue_status_in_progress: En cours
984 default_issue_status_resolved: Résolu
986 default_issue_status_resolved: Résolu
985 default_issue_status_feedback: Commentaire
987 default_issue_status_feedback: Commentaire
986 default_issue_status_closed: Fermé
988 default_issue_status_closed: Fermé
987 default_issue_status_rejected: Rejeté
989 default_issue_status_rejected: Rejeté
988 default_doc_category_user: Documentation utilisateur
990 default_doc_category_user: Documentation utilisateur
989 default_doc_category_tech: Documentation technique
991 default_doc_category_tech: Documentation technique
990 default_priority_low: Bas
992 default_priority_low: Bas
991 default_priority_normal: Normal
993 default_priority_normal: Normal
992 default_priority_high: Haut
994 default_priority_high: Haut
993 default_priority_urgent: Urgent
995 default_priority_urgent: Urgent
994 default_priority_immediate: Immédiat
996 default_priority_immediate: Immédiat
995 default_activity_design: Conception
997 default_activity_design: Conception
996 default_activity_development: Développement
998 default_activity_development: Développement
997
999
998 enumeration_issue_priorities: Priorités des demandes
1000 enumeration_issue_priorities: Priorités des demandes
999 enumeration_doc_categories: Catégories des documents
1001 enumeration_doc_categories: Catégories des documents
1000 enumeration_activities: Activités (suivi du temps)
1002 enumeration_activities: Activités (suivi du temps)
1001 label_greater_or_equal: ">="
1003 label_greater_or_equal: ">="
1002 label_less_or_equal: "<="
1004 label_less_or_equal: "<="
1003 label_between: entre
1005 label_between: entre
1004 label_view_all_revisions: Voir toutes les révisions
1006 label_view_all_revisions: Voir toutes les révisions
1005 label_tag: Tag
1007 label_tag: Tag
1006 label_branch: Branche
1008 label_branch: Branche
1007 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
1009 error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet."
1008 error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
1010 error_no_default_issue_status: "Aucun statut de demande n'est défini par défaut. Vérifier votre configuration (Administration -> Statuts de demandes)."
1009 text_journal_changed: "%{label} changé de %{old} à %{new}"
1011 text_journal_changed: "%{label} changé de %{old} à %{new}"
1010 text_journal_changed_no_detail: "%{label} mis à jour"
1012 text_journal_changed_no_detail: "%{label} mis à jour"
1011 text_journal_set_to: "%{label} mis à %{value}"
1013 text_journal_set_to: "%{label} mis à %{value}"
1012 text_journal_deleted: "%{label} %{old} supprimé"
1014 text_journal_deleted: "%{label} %{old} supprimé"
1013 text_journal_added: "%{label} %{value} ajouté"
1015 text_journal_added: "%{label} %{value} ajouté"
1014 enumeration_system_activity: Activité système
1016 enumeration_system_activity: Activité système
1015 label_board_sticky: Sticky
1017 label_board_sticky: Sticky
1016 label_board_locked: Verrouillé
1018 label_board_locked: Verrouillé
1017 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
1019 error_unable_delete_issue_status: Impossible de supprimer le statut de demande
1018 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé
1020 error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé
1019 error_unable_to_connect: Connexion impossible (%{value})
1021 error_unable_to_connect: Connexion impossible (%{value})
1020 error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé.
1022 error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé.
1021 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé.
1023 error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé.
1022 field_principal: Principal
1024 field_principal: Principal
1023 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
1025 notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}."
1024 text_zoom_out: Zoom arrière
1026 text_zoom_out: Zoom arrière
1025 text_zoom_in: Zoom avant
1027 text_zoom_in: Zoom avant
1026 notice_unable_delete_time_entry: Impossible de supprimer le temps passé.
1028 notice_unable_delete_time_entry: Impossible de supprimer le temps passé.
1027 label_overall_spent_time: Temps passé global
1029 label_overall_spent_time: Temps passé global
1028 field_time_entries: Temps passé
1030 field_time_entries: Temps passé
1029 project_module_gantt: Gantt
1031 project_module_gantt: Gantt
1030 project_module_calendar: Calendrier
1032 project_module_calendar: Calendrier
1031 button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}"
1033 button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}"
1032 text_are_you_sure_with_children: Supprimer la demande et toutes ses sous-demandes ?
1034 text_are_you_sure_with_children: Supprimer la demande et toutes ses sous-demandes ?
1033 field_text: Champ texte
1035 field_text: Champ texte
1034 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
1036 label_user_mail_option_only_owner: Seulement pour ce que j'ai créé
1035 setting_default_notification_option: Option de notification par défaut
1037 setting_default_notification_option: Option de notification par défaut
1036 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
1038 label_user_mail_option_only_my_events: Seulement pour ce que je surveille
1037 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné
1039 label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné
1038 label_user_mail_option_none: Aucune notification
1040 label_user_mail_option_none: Aucune notification
1039 field_member_of_group: Groupe de l'assigné
1041 field_member_of_group: Groupe de l'assigné
1040 field_assigned_to_role: Rôle de l'assigné
1042 field_assigned_to_role: Rôle de l'assigné
1041 setting_emails_header: En-tête des emails
1043 setting_emails_header: En-tête des emails
1042 label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés
1044 label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés
1043 text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?"
1045 text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?"
1044 field_scm_path_encoding: Encodage des chemins
1046 field_scm_path_encoding: Encodage des chemins
1045 text_scm_path_encoding_note: "Défaut : UTF-8"
1047 text_scm_path_encoding_note: "Défaut : UTF-8"
1046 field_path_to_repository: Chemin du dépôt
1048 field_path_to_repository: Chemin du dépôt
1047 field_root_directory: Répertoire racine
1049 field_root_directory: Répertoire racine
1048 field_cvs_module: Module
1050 field_cvs_module: Module
1049 field_cvsroot: CVSROOT
1051 field_cvsroot: CVSROOT
1050 text_mercurial_repository_note: "Dépôt local (exemples : /hgrepo, c:\\hgrepo)"
1052 text_mercurial_repository_note: "Dépôt local (exemples : /hgrepo, c:\\hgrepo)"
1051 text_scm_command: Commande
1053 text_scm_command: Commande
1052 text_scm_command_version: Version
1054 text_scm_command_version: Version
1053 label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires
1055 label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires
1054 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1056 text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification.
1055 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1057 text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration.
1056 label_diff: diff
1058 label_diff: diff
1057 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1059 text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo)
1058 description_query_sort_criteria_direction: Ordre de tri
1060 description_query_sort_criteria_direction: Ordre de tri
1059 description_project_scope: Périmètre de recherche
1061 description_project_scope: Périmètre de recherche
1060 description_filter: Filtre
1062 description_filter: Filtre
1061 description_user_mail_notification: Option de notification
1063 description_user_mail_notification: Option de notification
1062 description_date_from: Date de début
1064 description_date_from: Date de début
1063 description_message_content: Contenu du message
1065 description_message_content: Contenu du message
1064 description_available_columns: Colonnes disponibles
1066 description_available_columns: Colonnes disponibles
1065 description_all_columns: Toutes les colonnes
1067 description_all_columns: Toutes les colonnes
1066 description_date_range_interval: Choisir une période
1068 description_date_range_interval: Choisir une période
1067 description_issue_category_reassign: Choisir une catégorie
1069 description_issue_category_reassign: Choisir une catégorie
1068 description_search: Champ de recherche
1070 description_search: Champ de recherche
1069 description_notes: Notes
1071 description_notes: Notes
1070 description_date_range_list: Choisir une période prédéfinie
1072 description_date_range_list: Choisir une période prédéfinie
1071 description_choose_project: Projets
1073 description_choose_project: Projets
1072 description_date_to: Date de fin
1074 description_date_to: Date de fin
1073 description_query_sort_criteria_attribute: Critère de tri
1075 description_query_sort_criteria_attribute: Critère de tri
1074 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1076 description_wiki_subpages_reassign: Choisir une nouvelle page parent
1075 description_selected_columns: Colonnes sélectionnées
1077 description_selected_columns: Colonnes sélectionnées
1076 label_parent_revision: Parent
1078 label_parent_revision: Parent
1077 label_child_revision: Enfant
1079 label_child_revision: Enfant
1078 error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale.
1080 error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale.
1079 setting_repositories_encodings: Encodages des fichiers et des dépôts
1081 setting_repositories_encodings: Encodages des fichiers et des dépôts
1080 label_search_for_watchers: Rechercher des observateurs
1082 label_search_for_watchers: Rechercher des observateurs
1081 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é.'
1083 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,584 +1,602
1 /* Redmine - project management software
1 /* Redmine - project management software
2 Copyright (C) 2006-2012 Jean-Philippe Lang */
2 Copyright (C) 2006-2012 Jean-Philippe Lang */
3
3
4 function checkAll(id, checked) {
4 function checkAll(id, checked) {
5 if (checked) {
5 if (checked) {
6 $('#'+id).find('input[type=checkbox]').attr('checked', true);
6 $('#'+id).find('input[type=checkbox]').attr('checked', true);
7 } else {
7 } else {
8 $('#'+id).find('input[type=checkbox]').removeAttr('checked');
8 $('#'+id).find('input[type=checkbox]').removeAttr('checked');
9 }
9 }
10 }
10 }
11
11
12 function toggleCheckboxesBySelector(selector) {
12 function toggleCheckboxesBySelector(selector) {
13 var all_checked = true;
13 var all_checked = true;
14 $(selector).each(function(index) {
14 $(selector).each(function(index) {
15 if (!$(this).is(':checked')) { all_checked = false; }
15 if (!$(this).is(':checked')) { all_checked = false; }
16 });
16 });
17 $(selector).attr('checked', !all_checked)
17 $(selector).attr('checked', !all_checked)
18 }
18 }
19
19
20 function showAndScrollTo(id, focus) {
20 function showAndScrollTo(id, focus) {
21 $('#'+id).show();
21 $('#'+id).show();
22 if (focus!=null) {
22 if (focus!=null) {
23 $('#'+focus).focus();
23 $('#'+focus).focus();
24 }
24 }
25 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
25 $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100);
26 }
26 }
27
27
28 function toggleRowGroup(el) {
28 function toggleRowGroup(el) {
29 var tr = $(el).parents('tr').first();
29 var tr = $(el).parents('tr').first();
30 var n = tr.next();
30 var n = tr.next();
31 tr.toggleClass('open');
31 tr.toggleClass('open');
32 while (n.length && !n.hasClass('group')) {
32 while (n.length && !n.hasClass('group')) {
33 n.toggle();
33 n.toggle();
34 n = n.next('tr');
34 n = n.next('tr');
35 }
35 }
36 }
36 }
37
37
38 function collapseAllRowGroups(el) {
38 function collapseAllRowGroups(el) {
39 var tbody = $(el).parents('tbody').first();
39 var tbody = $(el).parents('tbody').first();
40 tbody.children('tr').each(function(index) {
40 tbody.children('tr').each(function(index) {
41 if ($(this).hasClass('group')) {
41 if ($(this).hasClass('group')) {
42 $(this).removeClass('open');
42 $(this).removeClass('open');
43 } else {
43 } else {
44 $(this).hide();
44 $(this).hide();
45 }
45 }
46 });
46 });
47 }
47 }
48
48
49 function expandAllRowGroups(el) {
49 function expandAllRowGroups(el) {
50 var tbody = $(el).parents('tbody').first();
50 var tbody = $(el).parents('tbody').first();
51 tbody.children('tr').each(function(index) {
51 tbody.children('tr').each(function(index) {
52 if ($(this).hasClass('group')) {
52 if ($(this).hasClass('group')) {
53 $(this).addClass('open');
53 $(this).addClass('open');
54 } else {
54 } else {
55 $(this).show();
55 $(this).show();
56 }
56 }
57 });
57 });
58 }
58 }
59
59
60 function toggleAllRowGroups(el) {
60 function toggleAllRowGroups(el) {
61 var tr = $(el).parents('tr').first();
61 var tr = $(el).parents('tr').first();
62 if (tr.hasClass('open')) {
62 if (tr.hasClass('open')) {
63 collapseAllRowGroups(el);
63 collapseAllRowGroups(el);
64 } else {
64 } else {
65 expandAllRowGroups(el);
65 expandAllRowGroups(el);
66 }
66 }
67 }
67 }
68
68
69 function toggleFieldset(el) {
69 function toggleFieldset(el) {
70 var fieldset = $(el).parents('fieldset').first();
70 var fieldset = $(el).parents('fieldset').first();
71 fieldset.toggleClass('collapsed');
71 fieldset.toggleClass('collapsed');
72 fieldset.children('div').toggle();
72 fieldset.children('div').toggle();
73 }
73 }
74
74
75 function hideFieldset(el) {
75 function hideFieldset(el) {
76 var fieldset = $(el).parents('fieldset').first();
76 var fieldset = $(el).parents('fieldset').first();
77 fieldset.toggleClass('collapsed');
77 fieldset.toggleClass('collapsed');
78 fieldset.children('div').hide();
78 fieldset.children('div').hide();
79 }
79 }
80
80
81 function initFilters(){
81 function initFilters(){
82 $('#add_filter_select').change(function(){
82 $('#add_filter_select').change(function(){
83 addFilter($(this).val(), '', []);
83 addFilter($(this).val(), '', []);
84 });
84 });
85 $('#filters-table td.field input[type=checkbox]').each(function(){
85 $('#filters-table td.field input[type=checkbox]').each(function(){
86 toggleFilter($(this).val());
86 toggleFilter($(this).val());
87 });
87 });
88 $('#filters-table td.field input[type=checkbox]').live('click',function(){
88 $('#filters-table td.field input[type=checkbox]').live('click',function(){
89 toggleFilter($(this).val());
89 toggleFilter($(this).val());
90 });
90 });
91 $('#filters-table .toggle-multiselect').live('click',function(){
91 $('#filters-table .toggle-multiselect').live('click',function(){
92 toggleMultiSelect($(this).siblings('select'));
92 toggleMultiSelect($(this).siblings('select'));
93 });
93 });
94 $('#filters-table input[type=text]').live('keypress', function(e){
94 $('#filters-table input[type=text]').live('keypress', function(e){
95 if (e.keyCode == 13) submit_query_form("query_form");
95 if (e.keyCode == 13) submit_query_form("query_form");
96 });
96 });
97 }
97 }
98
98
99 function addFilter(field, operator, values) {
99 function addFilter(field, operator, values) {
100 var fieldId = field.replace('.', '_');
100 var fieldId = field.replace('.', '_');
101 var tr = $('#tr_'+fieldId);
101 var tr = $('#tr_'+fieldId);
102 if (tr.length > 0) {
102 if (tr.length > 0) {
103 tr.show();
103 tr.show();
104 } else {
104 } else {
105 buildFilterRow(field, operator, values);
105 buildFilterRow(field, operator, values);
106 }
106 }
107 $('#cb_'+fieldId).attr('checked', true);
107 $('#cb_'+fieldId).attr('checked', true);
108 toggleFilter(field);
108 toggleFilter(field);
109 $('#add_filter_select').val('').children('option').each(function(){
109 $('#add_filter_select').val('').children('option').each(function(){
110 if ($(this).attr('value') == field) {
110 if ($(this).attr('value') == field) {
111 $(this).attr('disabled', true);
111 $(this).attr('disabled', true);
112 }
112 }
113 });
113 });
114 }
114 }
115
115
116 function buildFilterRow(field, operator, values) {
116 function buildFilterRow(field, operator, values) {
117 var fieldId = field.replace('.', '_');
117 var fieldId = field.replace('.', '_');
118 var filterTable = $("#filters-table");
118 var filterTable = $("#filters-table");
119 var filterOptions = availableFilters[field];
119 var filterOptions = availableFilters[field];
120 var operators = operatorByType[filterOptions['type']];
120 var operators = operatorByType[filterOptions['type']];
121 var filterValues = filterOptions['values'];
121 var filterValues = filterOptions['values'];
122 var i, select;
122 var i, select;
123
123
124 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
124 var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html(
125 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
125 '<td class="field"><input checked="checked" id="cb_'+fieldId+'" name="f[]" value="'+field+'" type="checkbox"><label for="cb_'+fieldId+'"> '+filterOptions['name']+'</label></td>' +
126 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
126 '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' +
127 '<td class="values"></td>'
127 '<td class="values"></td>'
128 );
128 );
129 filterTable.append(tr);
129 filterTable.append(tr);
130
130
131 select = tr.find('td.operator select');
131 select = tr.find('td.operator select');
132 for (i=0;i<operators.length;i++){
132 for (i=0;i<operators.length;i++){
133 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
133 var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]);
134 if (operators[i] == operator) {option.attr('selected', true)};
134 if (operators[i] == operator) {option.attr('selected', true)};
135 select.append(option);
135 select.append(option);
136 }
136 }
137 select.change(function(){toggleOperator(field)});
137 select.change(function(){toggleOperator(field)});
138
138
139 switch (filterOptions['type']){
139 switch (filterOptions['type']){
140 case "list":
140 case "list":
141 case "list_optional":
141 case "list_optional":
142 case "list_status":
142 case "list_status":
143 case "list_subprojects":
143 case "list_subprojects":
144 tr.find('td.values').append(
144 tr.find('td.values').append(
145 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
145 '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' +
146 ' <span class="toggle-multiselect">&nbsp;</span></span>'
146 ' <span class="toggle-multiselect">&nbsp;</span></span>'
147 );
147 );
148 select = tr.find('td.values select');
148 select = tr.find('td.values select');
149 if (values.length > 1) {select.attr('multiple', true)};
149 if (values.length > 1) {select.attr('multiple', true)};
150 for (i=0;i<filterValues.length;i++){
150 for (i=0;i<filterValues.length;i++){
151 var filterValue = filterValues[i];
151 var filterValue = filterValues[i];
152 var option = $('<option>');
152 var option = $('<option>');
153 if ($.isArray(filterValue)) {
153 if ($.isArray(filterValue)) {
154 option.val(filterValue[1]).text(filterValue[0]);
154 option.val(filterValue[1]).text(filterValue[0]);
155 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
155 if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);}
156 } else {
156 } else {
157 option.val(filterValue).text(filterValue);
157 option.val(filterValue).text(filterValue);
158 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
158 if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);}
159 }
159 }
160 select.append(option);
160 select.append(option);
161 }
161 }
162 break;
162 break;
163 case "date":
163 case "date":
164 case "date_past":
164 case "date_past":
165 tr.find('td.values').append(
165 tr.find('td.values').append(
166 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
166 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' +
167 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
167 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' +
168 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
168 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>'
169 );
169 );
170 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
170 $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions);
171 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
171 $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions);
172 $('#values_'+fieldId).val(values[0]);
172 $('#values_'+fieldId).val(values[0]);
173 break;
173 break;
174 case "string":
174 case "string":
175 case "text":
175 case "text":
176 tr.find('td.values').append(
176 tr.find('td.values').append(
177 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
177 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>'
178 );
178 );
179 $('#values_'+fieldId).val(values[0]);
179 $('#values_'+fieldId).val(values[0]);
180 break;
180 break;
181 case "relation":
182 tr.find('td.values').append(
183 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="6" class="value" /></span>' +
184 '<span style="display:none;"><select class="value" name="v['+field+'][]" id="values_'+fieldId+'_1"></select></span>'
185 );
186 $('#values_'+fieldId).val(values[0]);
187 select = tr.find('td.values select');
188 for (i=0;i<allProjects.length;i++){
189 var filterValue = allProjects[i];
190 var option = $('<option>');
191 option.val(filterValue[1]).text(filterValue[0]);
192 if (values[0] == filterValue[1]) {option.attr('selected', true)};
193 select.append(option);
194 }
181 case "integer":
195 case "integer":
182 case "float":
196 case "float":
183 tr.find('td.values').append(
197 tr.find('td.values').append(
184 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
198 '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' +
185 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
199 ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>'
186 );
200 );
187 $('#values_'+fieldId+'_1').val(values[0]);
201 $('#values_'+fieldId+'_1').val(values[0]);
188 $('#values_'+fieldId+'_2').val(values[1]);
202 $('#values_'+fieldId+'_2').val(values[1]);
189 break;
203 break;
190 }
204 }
191 }
205 }
192
206
193 function toggleFilter(field) {
207 function toggleFilter(field) {
194 var fieldId = field.replace('.', '_');
208 var fieldId = field.replace('.', '_');
195 if ($('#cb_' + fieldId).is(':checked')) {
209 if ($('#cb_' + fieldId).is(':checked')) {
196 $("#operators_" + fieldId).show().removeAttr('disabled');
210 $("#operators_" + fieldId).show().removeAttr('disabled');
197 toggleOperator(field);
211 toggleOperator(field);
198 } else {
212 } else {
199 $("#operators_" + fieldId).hide().attr('disabled', true);
213 $("#operators_" + fieldId).hide().attr('disabled', true);
200 enableValues(field, []);
214 enableValues(field, []);
201 }
215 }
202 }
216 }
203
217
204 function enableValues(field, indexes) {
218 function enableValues(field, indexes) {
205 var fieldId = field.replace('.', '_');
219 var fieldId = field.replace('.', '_');
206 $('#tr_'+fieldId+' td.values .value').each(function(index) {
220 $('#tr_'+fieldId+' td.values .value').each(function(index) {
207 if ($.inArray(index, indexes) >= 0) {
221 if ($.inArray(index, indexes) >= 0) {
208 $(this).removeAttr('disabled');
222 $(this).removeAttr('disabled');
209 $(this).parents('span').first().show();
223 $(this).parents('span').first().show();
210 } else {
224 } else {
211 $(this).val('');
225 $(this).val('');
212 $(this).attr('disabled', true);
226 $(this).attr('disabled', true);
213 $(this).parents('span').first().hide();
227 $(this).parents('span').first().hide();
214 }
228 }
215
229
216 if ($(this).hasClass('group')) {
230 if ($(this).hasClass('group')) {
217 $(this).addClass('open');
231 $(this).addClass('open');
218 } else {
232 } else {
219 $(this).show();
233 $(this).show();
220 }
234 }
221 });
235 });
222 }
236 }
223
237
224 function toggleOperator(field) {
238 function toggleOperator(field) {
225 var fieldId = field.replace('.', '_');
239 var fieldId = field.replace('.', '_');
226 var operator = $("#operators_" + fieldId);
240 var operator = $("#operators_" + fieldId);
227 switch (operator.val()) {
241 switch (operator.val()) {
228 case "!*":
242 case "!*":
229 case "*":
243 case "*":
230 case "t":
244 case "t":
231 case "w":
245 case "w":
232 case "o":
246 case "o":
233 case "c":
247 case "c":
234 enableValues(field, []);
248 enableValues(field, []);
235 break;
249 break;
236 case "><":
250 case "><":
237 enableValues(field, [0,1]);
251 enableValues(field, [0,1]);
238 break;
252 break;
239 case "<t+":
253 case "<t+":
240 case ">t+":
254 case ">t+":
241 case "t+":
255 case "t+":
242 case ">t-":
256 case ">t-":
243 case "<t-":
257 case "<t-":
244 case "t-":
258 case "t-":
245 enableValues(field, [2]);
259 enableValues(field, [2]);
246 break;
260 break;
261 case "=p":
262 case "=!p":
263 enableValues(field, [1]);
264 break;
247 default:
265 default:
248 enableValues(field, [0]);
266 enableValues(field, [0]);
249 break;
267 break;
250 }
268 }
251 }
269 }
252
270
253 function toggleMultiSelect(el) {
271 function toggleMultiSelect(el) {
254 if (el.attr('multiple')) {
272 if (el.attr('multiple')) {
255 el.removeAttr('multiple');
273 el.removeAttr('multiple');
256 } else {
274 } else {
257 el.attr('multiple', true);
275 el.attr('multiple', true);
258 }
276 }
259 }
277 }
260
278
261 function submit_query_form(id) {
279 function submit_query_form(id) {
262 selectAllOptions("selected_columns");
280 selectAllOptions("selected_columns");
263 $('#'+id).submit();
281 $('#'+id).submit();
264 }
282 }
265
283
266 var fileFieldCount = 1;
284 var fileFieldCount = 1;
267 function addFileField() {
285 function addFileField() {
268 var fields = $('#attachments_fields');
286 var fields = $('#attachments_fields');
269 if (fields.children().length >= 10) return false;
287 if (fields.children().length >= 10) return false;
270 fileFieldCount++;
288 fileFieldCount++;
271 var s = fields.children('span').first().clone();
289 var s = fields.children('span').first().clone();
272 s.children('input.file').attr('name', "attachments[" + fileFieldCount + "][file]").val('');
290 s.children('input.file').attr('name', "attachments[" + fileFieldCount + "][file]").val('');
273 s.children('input.description').attr('name', "attachments[" + fileFieldCount + "][description]").val('');
291 s.children('input.description').attr('name', "attachments[" + fileFieldCount + "][description]").val('');
274 fields.append(s);
292 fields.append(s);
275 }
293 }
276
294
277 function removeFileField(el) {
295 function removeFileField(el) {
278 var fields = $('#attachments_fields');
296 var fields = $('#attachments_fields');
279 var s = $(el).parents('span').first();
297 var s = $(el).parents('span').first();
280 if (fields.children().length > 1) {
298 if (fields.children().length > 1) {
281 s.remove();
299 s.remove();
282 } else {
300 } else {
283 s.children('input.file').val('');
301 s.children('input.file').val('');
284 s.children('input.description').val('');
302 s.children('input.description').val('');
285 }
303 }
286 }
304 }
287
305
288 function checkFileSize(el, maxSize, message) {
306 function checkFileSize(el, maxSize, message) {
289 var files = el.files;
307 var files = el.files;
290 if (files) {
308 if (files) {
291 for (var i=0; i<files.length; i++) {
309 for (var i=0; i<files.length; i++) {
292 if (files[i].size > maxSize) {
310 if (files[i].size > maxSize) {
293 alert(message);
311 alert(message);
294 el.value = "";
312 el.value = "";
295 }
313 }
296 }
314 }
297 }
315 }
298 }
316 }
299
317
300 function showTab(name) {
318 function showTab(name) {
301 $('div#content .tab-content').hide();
319 $('div#content .tab-content').hide();
302 $('div.tabs a').removeClass('selected');
320 $('div.tabs a').removeClass('selected');
303 $('#tab-content-' + name).show();
321 $('#tab-content-' + name).show();
304 $('#tab-' + name).addClass('selected');
322 $('#tab-' + name).addClass('selected');
305 return false;
323 return false;
306 }
324 }
307
325
308 function moveTabRight(el) {
326 function moveTabRight(el) {
309 var lis = $(el).parents('div.tabs').first().find('ul').children();
327 var lis = $(el).parents('div.tabs').first().find('ul').children();
310 var tabsWidth = 0;
328 var tabsWidth = 0;
311 var i = 0;
329 var i = 0;
312 lis.each(function(){
330 lis.each(function(){
313 if ($(this).is(':visible')) {
331 if ($(this).is(':visible')) {
314 tabsWidth += $(this).width() + 6;
332 tabsWidth += $(this).width() + 6;
315 }
333 }
316 });
334 });
317 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
335 if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; }
318 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
336 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
319 lis.eq(i).hide();
337 lis.eq(i).hide();
320 }
338 }
321
339
322 function moveTabLeft(el) {
340 function moveTabLeft(el) {
323 var lis = $(el).parents('div.tabs').first().find('ul').children();
341 var lis = $(el).parents('div.tabs').first().find('ul').children();
324 var i = 0;
342 var i = 0;
325 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
343 while (i<lis.length && !lis.eq(i).is(':visible')) { i++; }
326 if (i>0) {
344 if (i>0) {
327 lis.eq(i-1).show();
345 lis.eq(i-1).show();
328 }
346 }
329 }
347 }
330
348
331 function displayTabsButtons() {
349 function displayTabsButtons() {
332 var lis;
350 var lis;
333 var tabsWidth = 0;
351 var tabsWidth = 0;
334 var el;
352 var el;
335 $('div.tabs').each(function() {
353 $('div.tabs').each(function() {
336 el = $(this);
354 el = $(this);
337 lis = el.find('ul').children();
355 lis = el.find('ul').children();
338 lis.each(function(){
356 lis.each(function(){
339 if ($(this).is(':visible')) {
357 if ($(this).is(':visible')) {
340 tabsWidth += $(this).width() + 6;
358 tabsWidth += $(this).width() + 6;
341 }
359 }
342 });
360 });
343 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
361 if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) {
344 el.find('div.tabs-buttons').hide();
362 el.find('div.tabs-buttons').hide();
345 } else {
363 } else {
346 el.find('div.tabs-buttons').show();
364 el.find('div.tabs-buttons').show();
347 }
365 }
348 });
366 });
349 }
367 }
350
368
351 function setPredecessorFieldsVisibility() {
369 function setPredecessorFieldsVisibility() {
352 var relationType = $('#relation_relation_type');
370 var relationType = $('#relation_relation_type');
353 if (relationType.val() == "precedes" || relationType.val() == "follows") {
371 if (relationType.val() == "precedes" || relationType.val() == "follows") {
354 $('#predecessor_fields').show();
372 $('#predecessor_fields').show();
355 } else {
373 } else {
356 $('#predecessor_fields').hide();
374 $('#predecessor_fields').hide();
357 }
375 }
358 }
376 }
359
377
360 function showModal(id, width) {
378 function showModal(id, width) {
361 var el = $('#'+id).first();
379 var el = $('#'+id).first();
362 if (el.length == 0 || el.is(':visible')) {return;}
380 if (el.length == 0 || el.is(':visible')) {return;}
363 var title = el.find('h3.title').text();
381 var title = el.find('h3.title').text();
364 el.dialog({
382 el.dialog({
365 width: width,
383 width: width,
366 modal: true,
384 modal: true,
367 resizable: false,
385 resizable: false,
368 dialogClass: 'modal',
386 dialogClass: 'modal',
369 title: title
387 title: title
370 });
388 });
371 el.find("input[type=text], input[type=submit]").first().focus();
389 el.find("input[type=text], input[type=submit]").first().focus();
372 }
390 }
373
391
374 function hideModal(el) {
392 function hideModal(el) {
375 var modal;
393 var modal;
376 if (el) {
394 if (el) {
377 modal = $(el).parents('.ui-dialog-content');
395 modal = $(el).parents('.ui-dialog-content');
378 } else {
396 } else {
379 modal = $('#ajax-modal');
397 modal = $('#ajax-modal');
380 }
398 }
381 modal.dialog("close");
399 modal.dialog("close");
382 }
400 }
383
401
384 function submitPreview(url, form, target) {
402 function submitPreview(url, form, target) {
385 $.ajax({
403 $.ajax({
386 url: url,
404 url: url,
387 type: 'post',
405 type: 'post',
388 data: $('#'+form).serialize(),
406 data: $('#'+form).serialize(),
389 success: function(data){
407 success: function(data){
390 $('#'+target).html(data);
408 $('#'+target).html(data);
391 }
409 }
392 });
410 });
393 }
411 }
394
412
395 function collapseScmEntry(id) {
413 function collapseScmEntry(id) {
396 $('.'+id).each(function() {
414 $('.'+id).each(function() {
397 if ($(this).hasClass('open')) {
415 if ($(this).hasClass('open')) {
398 collapseScmEntry($(this).attr('id'));
416 collapseScmEntry($(this).attr('id'));
399 }
417 }
400 $(this).hide();
418 $(this).hide();
401 });
419 });
402 $('#'+id).removeClass('open');
420 $('#'+id).removeClass('open');
403 }
421 }
404
422
405 function expandScmEntry(id) {
423 function expandScmEntry(id) {
406 $('.'+id).each(function() {
424 $('.'+id).each(function() {
407 $(this).show();
425 $(this).show();
408 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
426 if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) {
409 expandScmEntry($(this).attr('id'));
427 expandScmEntry($(this).attr('id'));
410 }
428 }
411 });
429 });
412 $('#'+id).addClass('open');
430 $('#'+id).addClass('open');
413 }
431 }
414
432
415 function scmEntryClick(id, url) {
433 function scmEntryClick(id, url) {
416 el = $('#'+id);
434 el = $('#'+id);
417 if (el.hasClass('open')) {
435 if (el.hasClass('open')) {
418 collapseScmEntry(id);
436 collapseScmEntry(id);
419 el.addClass('collapsed');
437 el.addClass('collapsed');
420 return false;
438 return false;
421 } else if (el.hasClass('loaded')) {
439 } else if (el.hasClass('loaded')) {
422 expandScmEntry(id);
440 expandScmEntry(id);
423 el.removeClass('collapsed');
441 el.removeClass('collapsed');
424 return false;
442 return false;
425 }
443 }
426 if (el.hasClass('loading')) {
444 if (el.hasClass('loading')) {
427 return false;
445 return false;
428 }
446 }
429 el.addClass('loading');
447 el.addClass('loading');
430 $.ajax({
448 $.ajax({
431 url: url,
449 url: url,
432 success: function(data){
450 success: function(data){
433 el.after(data);
451 el.after(data);
434 el.addClass('open').addClass('loaded').removeClass('loading');
452 el.addClass('open').addClass('loaded').removeClass('loading');
435 }
453 }
436 });
454 });
437 return true;
455 return true;
438 }
456 }
439
457
440 function randomKey(size) {
458 function randomKey(size) {
441 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
459 var chars = new Array('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z');
442 var key = '';
460 var key = '';
443 for (i = 0; i < size; i++) {
461 for (i = 0; i < size; i++) {
444 key += chars[Math.floor(Math.random() * chars.length)];
462 key += chars[Math.floor(Math.random() * chars.length)];
445 }
463 }
446 return key;
464 return key;
447 }
465 }
448
466
449 // Can't use Rails' remote select because we need the form data
467 // Can't use Rails' remote select because we need the form data
450 function updateIssueFrom(url) {
468 function updateIssueFrom(url) {
451 $.ajax({
469 $.ajax({
452 url: url,
470 url: url,
453 type: 'post',
471 type: 'post',
454 data: $('#issue-form').serialize()
472 data: $('#issue-form').serialize()
455 });
473 });
456 }
474 }
457
475
458 function updateBulkEditFrom(url) {
476 function updateBulkEditFrom(url) {
459 $.ajax({
477 $.ajax({
460 url: url,
478 url: url,
461 type: 'post',
479 type: 'post',
462 data: $('#bulk_edit_form').serialize()
480 data: $('#bulk_edit_form').serialize()
463 });
481 });
464 }
482 }
465
483
466 function observeAutocompleteField(fieldId, url) {
484 function observeAutocompleteField(fieldId, url) {
467 $('#'+fieldId).autocomplete({
485 $('#'+fieldId).autocomplete({
468 source: url,
486 source: url,
469 minLength: 2,
487 minLength: 2,
470 });
488 });
471 }
489 }
472
490
473 function observeSearchfield(fieldId, targetId, url) {
491 function observeSearchfield(fieldId, targetId, url) {
474 $('#'+fieldId).each(function() {
492 $('#'+fieldId).each(function() {
475 var $this = $(this);
493 var $this = $(this);
476 $this.attr('data-value-was', $this.val());
494 $this.attr('data-value-was', $this.val());
477 var check = function() {
495 var check = function() {
478 var val = $this.val();
496 var val = $this.val();
479 if ($this.attr('data-value-was') != val){
497 if ($this.attr('data-value-was') != val){
480 $this.attr('data-value-was', val);
498 $this.attr('data-value-was', val);
481 if (val != '') {
499 if (val != '') {
482 $.ajax({
500 $.ajax({
483 url: url,
501 url: url,
484 type: 'get',
502 type: 'get',
485 data: {q: $this.val()},
503 data: {q: $this.val()},
486 success: function(data){ $('#'+targetId).html(data); },
504 success: function(data){ $('#'+targetId).html(data); },
487 beforeSend: function(){ $this.addClass('ajax-loading'); },
505 beforeSend: function(){ $this.addClass('ajax-loading'); },
488 complete: function(){ $this.removeClass('ajax-loading'); }
506 complete: function(){ $this.removeClass('ajax-loading'); }
489 });
507 });
490 }
508 }
491 }
509 }
492 };
510 };
493 var reset = function() {
511 var reset = function() {
494 if (timer) {
512 if (timer) {
495 clearInterval(timer);
513 clearInterval(timer);
496 timer = setInterval(check, 300);
514 timer = setInterval(check, 300);
497 }
515 }
498 };
516 };
499 var timer = setInterval(check, 300);
517 var timer = setInterval(check, 300);
500 $this.bind('keyup click mousemove', reset);
518 $this.bind('keyup click mousemove', reset);
501 });
519 });
502 }
520 }
503
521
504 function observeProjectModules() {
522 function observeProjectModules() {
505 var f = function() {
523 var f = function() {
506 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
524 /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */
507 if ($('#project_enabled_module_names_issue_tracking').attr('checked')) {
525 if ($('#project_enabled_module_names_issue_tracking').attr('checked')) {
508 $('#project_trackers').show();
526 $('#project_trackers').show();
509 }else{
527 }else{
510 $('#project_trackers').hide();
528 $('#project_trackers').hide();
511 }
529 }
512 };
530 };
513
531
514 $(window).load(f);
532 $(window).load(f);
515 $('#project_enabled_module_names_issue_tracking').change(f);
533 $('#project_enabled_module_names_issue_tracking').change(f);
516 }
534 }
517
535
518 function initMyPageSortable(list, url) {
536 function initMyPageSortable(list, url) {
519 $('#list-'+list).sortable({
537 $('#list-'+list).sortable({
520 connectWith: '.block-receiver',
538 connectWith: '.block-receiver',
521 tolerance: 'pointer',
539 tolerance: 'pointer',
522 update: function(){
540 update: function(){
523 $.ajax({
541 $.ajax({
524 url: url,
542 url: url,
525 type: 'post',
543 type: 'post',
526 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
544 data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})}
527 });
545 });
528 }
546 }
529 });
547 });
530 $("#list-top, #list-left, #list-right").disableSelection();
548 $("#list-top, #list-left, #list-right").disableSelection();
531 }
549 }
532
550
533 var warnLeavingUnsavedMessage;
551 var warnLeavingUnsavedMessage;
534 function warnLeavingUnsaved(message) {
552 function warnLeavingUnsaved(message) {
535 warnLeavingUnsavedMessage = message;
553 warnLeavingUnsavedMessage = message;
536
554
537 $('form').submit(function(){
555 $('form').submit(function(){
538 $('textarea').removeData('changed');
556 $('textarea').removeData('changed');
539 });
557 });
540 $('textarea').change(function(){
558 $('textarea').change(function(){
541 $(this).data('changed', 'changed');
559 $(this).data('changed', 'changed');
542 });
560 });
543 window.onbeforeunload = function(){
561 window.onbeforeunload = function(){
544 var warn = false;
562 var warn = false;
545 $('textarea').blur().each(function(){
563 $('textarea').blur().each(function(){
546 if ($(this).data('changed')) {
564 if ($(this).data('changed')) {
547 warn = true;
565 warn = true;
548 }
566 }
549 });
567 });
550 if (warn) {return warnLeavingUnsavedMessage;}
568 if (warn) {return warnLeavingUnsavedMessage;}
551 };
569 };
552 };
570 };
553
571
554 $(document).ready(function(){
572 $(document).ready(function(){
555 $('#ajax-indicator').bind('ajaxSend', function(){
573 $('#ajax-indicator').bind('ajaxSend', function(){
556 if ($('.ajax-loading').length == 0) {
574 if ($('.ajax-loading').length == 0) {
557 $('#ajax-indicator').show();
575 $('#ajax-indicator').show();
558 }
576 }
559 });
577 });
560 $('#ajax-indicator').bind('ajaxStop', function(){
578 $('#ajax-indicator').bind('ajaxStop', function(){
561 $('#ajax-indicator').hide();
579 $('#ajax-indicator').hide();
562 });
580 });
563 });
581 });
564
582
565 function hideOnLoad() {
583 function hideOnLoad() {
566 $('.hol').hide();
584 $('.hol').hide();
567 }
585 }
568
586
569 function addFormObserversForDoubleSubmit() {
587 function addFormObserversForDoubleSubmit() {
570 $('form[method=post]').each(function() {
588 $('form[method=post]').each(function() {
571 if (!$(this).hasClass('multiple-submit')) {
589 if (!$(this).hasClass('multiple-submit')) {
572 $(this).submit(function(form_submission) {
590 $(this).submit(function(form_submission) {
573 if ($(form_submission.target).attr('data-submitted')) {
591 if ($(form_submission.target).attr('data-submitted')) {
574 form_submission.preventDefault();
592 form_submission.preventDefault();
575 } else {
593 } else {
576 $(form_submission.target).attr('data-submitted', true);
594 $(form_submission.target).attr('data-submitted', true);
577 }
595 }
578 });
596 });
579 }
597 }
580 });
598 });
581 }
599 }
582
600
583 $(document).ready(hideOnLoad);
601 $(document).ready(hideOnLoad);
584 $(document).ready(addFormObserversForDoubleSubmit);
602 $(document).ready(addFormObserversForDoubleSubmit);
@@ -1,1127 +1,1130
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 {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;}
28 #header {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 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
79
80 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
80 #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; }
81 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
81 * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;}
82 html>body #content { min-height: 600px; }
82 html>body #content { min-height: 600px; }
83 * html body #content { height: 600px; } /* IE */
83 * html body #content { height: 600px; } /* IE */
84
84
85 #main.nosidebar #sidebar{ display: none; }
85 #main.nosidebar #sidebar{ display: none; }
86 #main.nosidebar #content{ width: auto; border-right: 0; }
86 #main.nosidebar #content{ width: auto; border-right: 0; }
87
87
88 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
88 #footer {clear: both; border-top: 1px solid #bbb; font-size: 0.9em; color: #aaa; padding: 5px; text-align:center; background:#fff;}
89
89
90 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
90 #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; }
91 #login-form table td {padding: 6px;}
91 #login-form table td {padding: 6px;}
92 #login-form label {font-weight: bold;}
92 #login-form label {font-weight: bold;}
93 #login-form input#username, #login-form input#password { width: 300px; }
93 #login-form input#username, #login-form input#password { width: 300px; }
94
94
95 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
95 div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;}
96 div.modal h3.title {display:none;}
96 div.modal h3.title {display:none;}
97 div.modal p.buttons {text-align:right; margin-bottom:0;}
97 div.modal p.buttons {text-align:right; margin-bottom:0;}
98
98
99 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
99 input#openid_url { background: url(../images/openid-bg.gif) no-repeat; background-color: #fff; background-position: 0 50%; padding-left: 18px; }
100
100
101 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
101 .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; }
102
102
103 /***** Links *****/
103 /***** Links *****/
104 a, a:link, a:visited{ color: #169; text-decoration: none; }
104 a, a:link, a:visited{ color: #169; text-decoration: none; }
105 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
105 a:hover, a:active{ color: #c61a1a; text-decoration: underline;}
106 a img{ border: 0; }
106 a img{ border: 0; }
107
107
108 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
108 a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; }
109 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
109 a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; }
110
110
111 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
111 #sidebar a.selected {line-height:1.7em; padding:1px 3px 2px 2px; margin-left:-2px; background-color:#9DB9D5; color:#fff; border-radius:2px;}
112 #sidebar a.selected:hover {text-decoration:none;}
112 #sidebar a.selected:hover {text-decoration:none;}
113 #admin-menu a {line-height:1.7em;}
113 #admin-menu a {line-height:1.7em;}
114 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
114 #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;}
115
115
116 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
116 a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;}
117 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
117 a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;}
118
118
119 a#toggle-completed-versions {color:#999;}
119 a#toggle-completed-versions {color:#999;}
120 /***** Tables *****/
120 /***** Tables *****/
121 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
121 table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; }
122 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
122 table.list th { background-color:#EEEEEE; padding: 4px; white-space:nowrap; }
123 table.list td { vertical-align: top; }
123 table.list td { vertical-align: top; padding-right:10px; }
124 table.list td.id { width: 2%; text-align: center;}
124 table.list td.id { width: 2%; text-align: center;}
125 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
125 table.list td.checkbox { width: 15px; padding: 2px 0 0 0; }
126 table.list td.checkbox input {padding:0px;}
126 table.list td.checkbox input {padding:0px;}
127 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
127 table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; }
128 table.list td.buttons a { padding-right: 0.6em; }
128 table.list td.buttons a { padding-right: 0.6em; }
129 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
129 table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; }
130
130
131 tr.project td.name a { white-space:nowrap; }
131 tr.project td.name a { white-space:nowrap; }
132 tr.project.closed, tr.project.archived { color: #aaa; }
132 tr.project.closed, tr.project.archived { color: #aaa; }
133 tr.project.closed a, tr.project.archived a { color: #aaa; }
133 tr.project.closed a, tr.project.archived a { color: #aaa; }
134
134
135 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
135 tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
136 tr.project.idnt-1 td.name {padding-left: 0.5em;}
136 tr.project.idnt-1 td.name {padding-left: 0.5em;}
137 tr.project.idnt-2 td.name {padding-left: 2em;}
137 tr.project.idnt-2 td.name {padding-left: 2em;}
138 tr.project.idnt-3 td.name {padding-left: 3.5em;}
138 tr.project.idnt-3 td.name {padding-left: 3.5em;}
139 tr.project.idnt-4 td.name {padding-left: 5em;}
139 tr.project.idnt-4 td.name {padding-left: 5em;}
140 tr.project.idnt-5 td.name {padding-left: 6.5em;}
140 tr.project.idnt-5 td.name {padding-left: 6.5em;}
141 tr.project.idnt-6 td.name {padding-left: 8em;}
141 tr.project.idnt-6 td.name {padding-left: 8em;}
142 tr.project.idnt-7 td.name {padding-left: 9.5em;}
142 tr.project.idnt-7 td.name {padding-left: 9.5em;}
143 tr.project.idnt-8 td.name {padding-left: 11em;}
143 tr.project.idnt-8 td.name {padding-left: 11em;}
144 tr.project.idnt-9 td.name {padding-left: 12.5em;}
144 tr.project.idnt-9 td.name {padding-left: 12.5em;}
145
145
146 tr.issue { text-align: center; white-space: nowrap; }
146 tr.issue { text-align: center; white-space: nowrap; }
147 tr.issue td.subject, tr.issue td.category, td.assigned_to, tr.issue td.string, tr.issue td.text { white-space: normal; }
147 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; }
148 tr.issue td.subject { text-align: left; }
148 tr.issue td.subject, tr.issue td.relations { text-align: left; }
149 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
149 tr.issue td.done_ratio table.progress { margin-left:auto; margin-right: auto;}
150 tr.issue td.relations span {white-space: nowrap;}
150
151
151 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
152 tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;}
152 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
153 tr.issue.idnt-1 td.subject {padding-left: 0.5em;}
153 tr.issue.idnt-2 td.subject {padding-left: 2em;}
154 tr.issue.idnt-2 td.subject {padding-left: 2em;}
154 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
155 tr.issue.idnt-3 td.subject {padding-left: 3.5em;}
155 tr.issue.idnt-4 td.subject {padding-left: 5em;}
156 tr.issue.idnt-4 td.subject {padding-left: 5em;}
156 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
157 tr.issue.idnt-5 td.subject {padding-left: 6.5em;}
157 tr.issue.idnt-6 td.subject {padding-left: 8em;}
158 tr.issue.idnt-6 td.subject {padding-left: 8em;}
158 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
159 tr.issue.idnt-7 td.subject {padding-left: 9.5em;}
159 tr.issue.idnt-8 td.subject {padding-left: 11em;}
160 tr.issue.idnt-8 td.subject {padding-left: 11em;}
160 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
161 tr.issue.idnt-9 td.subject {padding-left: 12.5em;}
161
162
162 tr.entry { border: 1px solid #f8f8f8; }
163 tr.entry { border: 1px solid #f8f8f8; }
163 tr.entry td { white-space: nowrap; }
164 tr.entry td { white-space: nowrap; }
164 tr.entry td.filename { width: 30%; }
165 tr.entry td.filename { width: 30%; }
165 tr.entry td.filename_no_report { width: 70%; }
166 tr.entry td.filename_no_report { width: 70%; }
166 tr.entry td.size { text-align: right; font-size: 90%; }
167 tr.entry td.size { text-align: right; font-size: 90%; }
167 tr.entry td.revision, tr.entry td.author { text-align: center; }
168 tr.entry td.revision, tr.entry td.author { text-align: center; }
168 tr.entry td.age { text-align: right; }
169 tr.entry td.age { text-align: right; }
169 tr.entry.file td.filename a { margin-left: 16px; }
170 tr.entry.file td.filename a { margin-left: 16px; }
170 tr.entry.file td.filename_no_report a { margin-left: 16px; }
171 tr.entry.file td.filename_no_report a { margin-left: 16px; }
171
172
172 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
173 tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;}
173 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
174 tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);}
174
175
175 tr.changeset { height: 20px }
176 tr.changeset { height: 20px }
176 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
177 tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; }
177 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
178 tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; }
178 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
179 tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;}
179 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
180 tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;}
180
181
181 table.files tr.file td { text-align: center; }
182 table.files tr.file td { text-align: center; }
182 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
183 table.files tr.file td.filename { text-align: left; padding-left: 24px; }
183 table.files tr.file td.digest { font-size: 80%; }
184 table.files tr.file td.digest { font-size: 80%; }
184
185
185 table.members td.roles, table.memberships td.roles { width: 45%; }
186 table.members td.roles, table.memberships td.roles { width: 45%; }
186
187
187 tr.message { height: 2.6em; }
188 tr.message { height: 2.6em; }
188 tr.message td.subject { padding-left: 20px; }
189 tr.message td.subject { padding-left: 20px; }
189 tr.message td.created_on { white-space: nowrap; }
190 tr.message td.created_on { white-space: nowrap; }
190 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
191 tr.message td.last_message { font-size: 80%; white-space: nowrap; }
191 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
192 tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; }
192 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
193 tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; }
193
194
194 tr.version.closed, tr.version.closed a { color: #999; }
195 tr.version.closed, tr.version.closed a { color: #999; }
195 tr.version td.name { padding-left: 20px; }
196 tr.version td.name { padding-left: 20px; }
196 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
197 tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; }
197 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
198 tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; }
198
199
199 tr.user td { width:13%; }
200 tr.user td { width:13%; }
200 tr.user td.email { width:18%; }
201 tr.user td.email { width:18%; }
201 tr.user td { white-space: nowrap; }
202 tr.user td { white-space: nowrap; }
202 tr.user.locked, tr.user.registered { color: #aaa; }
203 tr.user.locked, tr.user.registered { color: #aaa; }
203 tr.user.locked a, tr.user.registered a { color: #aaa; }
204 tr.user.locked a, tr.user.registered a { color: #aaa; }
204
205
205 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
206 table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;}
206
207
207 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
208 tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;}
208
209
209 tr.time-entry { text-align: center; white-space: nowrap; }
210 tr.time-entry { text-align: center; white-space: nowrap; }
210 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
211 tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; }
211 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
212 td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; }
212 td.hours .hours-dec { font-size: 0.9em; }
213 td.hours .hours-dec { font-size: 0.9em; }
213
214
214 table.plugins td { vertical-align: middle; }
215 table.plugins td { vertical-align: middle; }
215 table.plugins td.configure { text-align: right; padding-right: 1em; }
216 table.plugins td.configure { text-align: right; padding-right: 1em; }
216 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
217 table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; }
217 table.plugins span.description { display: block; font-size: 0.9em; }
218 table.plugins span.description { display: block; font-size: 0.9em; }
218 table.plugins span.url { display: block; font-size: 0.9em; }
219 table.plugins span.url { display: block; font-size: 0.9em; }
219
220
220 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
221 table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; }
221 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
222 table.list tbody tr.group span.count { color: #aaa; font-size: 80%; }
222 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
223 tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;}
223 tr.group:hover a.toggle-all { display:inline;}
224 tr.group:hover a.toggle-all { display:inline;}
224 a.toggle-all:hover {text-decoration:none;}
225 a.toggle-all:hover {text-decoration:none;}
225
226
226 table.list tbody tr:hover { background-color:#ffffdd; }
227 table.list tbody tr:hover { background-color:#ffffdd; }
227 table.list tbody tr.group:hover { background-color:inherit; }
228 table.list tbody tr.group:hover { background-color:inherit; }
228 table td {padding:2px;}
229 table td {padding:2px;}
229 table p {margin:0;}
230 table p {margin:0;}
230 .odd {background-color:#f6f7f8;}
231 .odd {background-color:#f6f7f8;}
231 .even {background-color: #fff;}
232 .even {background-color: #fff;}
232
233
233 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
234 a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; }
234 a.sort.asc { background-image: url(../images/sort_asc.png); }
235 a.sort.asc { background-image: url(../images/sort_asc.png); }
235 a.sort.desc { background-image: url(../images/sort_desc.png); }
236 a.sort.desc { background-image: url(../images/sort_desc.png); }
236
237
237 table.attributes { width: 100% }
238 table.attributes { width: 100% }
238 table.attributes th { vertical-align: top; text-align: left; }
239 table.attributes th { vertical-align: top; text-align: left; }
239 table.attributes td { vertical-align: top; }
240 table.attributes td { vertical-align: top; }
240
241
241 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
242 table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; }
242 table.boards td.topic-count, table.boards td.message-count {text-align:center;}
243 table.boards td.topic-count, table.boards td.message-count {text-align:center;}
243 table.boards td.last-message {font-size:80%;}
244 table.boards td.last-message {font-size:80%;}
244
245
245 table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;}
246 table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;}
246
247
247 table.query-columns {
248 table.query-columns {
248 border-collapse: collapse;
249 border-collapse: collapse;
249 border: 0;
250 border: 0;
250 }
251 }
251
252
252 table.query-columns td.buttons {
253 table.query-columns td.buttons {
253 vertical-align: middle;
254 vertical-align: middle;
254 text-align: center;
255 text-align: center;
255 }
256 }
256
257
257 td.center {text-align:center;}
258 td.center {text-align:center;}
258
259
259 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
260 h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; }
260
261
261 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
262 div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; }
262 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
263 div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; }
263 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
264 div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; }
264 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
265 div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; }
265
266
266 #watchers ul {margin: 0; padding: 0;}
267 #watchers ul {margin: 0; padding: 0;}
267 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
268 #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;}
268 #watchers select {width: 95%; display: block;}
269 #watchers select {width: 95%; display: block;}
269 #watchers a.delete {opacity: 0.4;}
270 #watchers a.delete {opacity: 0.4;}
270 #watchers a.delete:hover {opacity: 1;}
271 #watchers a.delete:hover {opacity: 1;}
271 #watchers img.gravatar {margin: 0 4px 2px 0;}
272 #watchers img.gravatar {margin: 0 4px 2px 0;}
272
273
273 span#watchers_inputs {overflow:auto; display:block;}
274 span#watchers_inputs {overflow:auto; display:block;}
274 span.search_for_watchers {display:block;}
275 span.search_for_watchers {display:block;}
275 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
276 span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;}
276 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
277 span.search_for_watchers a, span.add_attachment a {padding-left:16px; background: url(../images/bullet_add.png) no-repeat 0 50%; }
277
278
278
279
279 .highlight { background-color: #FCFD8D;}
280 .highlight { background-color: #FCFD8D;}
280 .highlight.token-1 { background-color: #faa;}
281 .highlight.token-1 { background-color: #faa;}
281 .highlight.token-2 { background-color: #afa;}
282 .highlight.token-2 { background-color: #afa;}
282 .highlight.token-3 { background-color: #aaf;}
283 .highlight.token-3 { background-color: #aaf;}
283
284
284 .box{
285 .box{
285 padding:6px;
286 padding:6px;
286 margin-bottom: 10px;
287 margin-bottom: 10px;
287 background-color:#f6f6f6;
288 background-color:#f6f6f6;
288 color:#505050;
289 color:#505050;
289 line-height:1.5em;
290 line-height:1.5em;
290 border: 1px solid #e4e4e4;
291 border: 1px solid #e4e4e4;
291 }
292 }
292
293
293 div.square {
294 div.square {
294 border: 1px solid #999;
295 border: 1px solid #999;
295 float: left;
296 float: left;
296 margin: .3em .4em 0 .4em;
297 margin: .3em .4em 0 .4em;
297 overflow: hidden;
298 overflow: hidden;
298 width: .6em; height: .6em;
299 width: .6em; height: .6em;
299 }
300 }
300 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
301 .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;}
301 .contextual input, .contextual select {font-size:0.9em;}
302 .contextual input, .contextual select {font-size:0.9em;}
302 .message .contextual { margin-top: 0; }
303 .message .contextual { margin-top: 0; }
303
304
304 .splitcontent {overflow:auto;}
305 .splitcontent {overflow:auto;}
305 .splitcontentleft{float:left; width:49%;}
306 .splitcontentleft{float:left; width:49%;}
306 .splitcontentright{float:right; width:49%;}
307 .splitcontentright{float:right; width:49%;}
307 form {display: inline;}
308 form {display: inline;}
308 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
309 input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;}
309 fieldset {border: 1px solid #e4e4e4; margin:0;}
310 fieldset {border: 1px solid #e4e4e4; margin:0;}
310 legend {color: #484848;}
311 legend {color: #484848;}
311 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
312 hr { width: 100%; height: 1px; background: #ccc; border: 0;}
312 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
313 blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;}
313 blockquote blockquote { margin-left: 0;}
314 blockquote blockquote { margin-left: 0;}
314 acronym { border-bottom: 1px dotted; cursor: help; }
315 acronym { border-bottom: 1px dotted; cursor: help; }
315 textarea.wiki-edit { width: 99%; }
316 textarea.wiki-edit { width: 99%; }
316 li p {margin-top: 0;}
317 li p {margin-top: 0;}
317 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
318 div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;}
318 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
319 p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;}
319 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
320 p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; }
320 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
321 p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; }
321
322
322 div.issue div.subject div div { padding-left: 16px; }
323 div.issue div.subject div div { padding-left: 16px; }
323 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
324 div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;}
324 div.issue div.subject>div>p { margin-top: 0.5em; }
325 div.issue div.subject>div>p { margin-top: 0.5em; }
325 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
326 div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;}
326 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;}
327 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;}
327 div.issue .next-prev-links {color:#999;}
328 div.issue .next-prev-links {color:#999;}
328 div.issue table.attributes th {width:22%;}
329 div.issue table.attributes th {width:22%;}
329 div.issue table.attributes td {width:28%;}
330 div.issue table.attributes td {width:28%;}
330
331
331 #issue_tree table.issues, #relations table.issues { border: 0; }
332 #issue_tree table.issues, #relations table.issues { border: 0; }
332 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
333 #issue_tree td.checkbox, #relations td.checkbox {display:none;}
333 #relations td.buttons {padding:0;}
334 #relations td.buttons {padding:0;}
334
335
335 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
336 fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; }
336 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
337 fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; }
337 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
338 fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); }
338
339
339 fieldset#date-range p { margin: 2px 0 2px 0; }
340 fieldset#date-range p { margin: 2px 0 2px 0; }
340 fieldset#filters table { border-collapse: collapse; }
341 fieldset#filters table { border-collapse: collapse; }
341 fieldset#filters table td { padding: 0; vertical-align: middle; }
342 fieldset#filters table td { padding: 0; vertical-align: middle; }
342 fieldset#filters tr.filter { height: 2.1em; }
343 fieldset#filters tr.filter { height: 2.1em; }
343 fieldset#filters td.field { width:250px; }
344 fieldset#filters td.field { width:230px; }
344 fieldset#filters td.operator { width:170px; }
345 fieldset#filters td.operator { width:180px; }
346 fieldset#filters td.operator select {max-width:170px;}
345 fieldset#filters td.values { white-space:nowrap; }
347 fieldset#filters td.values { white-space:nowrap; }
346 fieldset#filters td.values select {min-width:130px;}
348 fieldset#filters td.values select {min-width:130px;}
347 fieldset#filters td.values input {height:1em;}
349 fieldset#filters td.values input {height:1em;}
348 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
350 fieldset#filters td.add-filter { text-align: right; vertical-align: top; }
351
349 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
352 .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;}
350 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
353 .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; }
351
354
352 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
355 div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;}
353 div#issue-changesets div.changeset { padding: 4px;}
356 div#issue-changesets div.changeset { padding: 4px;}
354 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
357 div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; }
355 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
358 div#issue-changesets p { margin-top: 0; margin-bottom: 1em;}
356
359
357 .journal ul.details img {margin:0 0 -3px 4px;}
360 .journal ul.details img {margin:0 0 -3px 4px;}
358
361
359 div#activity dl, #search-results { margin-left: 2em; }
362 div#activity dl, #search-results { margin-left: 2em; }
360 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
363 div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; }
361 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
364 div#activity dt, #search-results dt { margin-bottom: 0px; padding-left: 20px; line-height: 18px; background-position: 0 50%; background-repeat: no-repeat; }
362 div#activity dt.me .time { border-bottom: 1px solid #999; }
365 div#activity dt.me .time { border-bottom: 1px solid #999; }
363 div#activity dt .time { color: #777; font-size: 80%; }
366 div#activity dt .time { color: #777; font-size: 80%; }
364 div#activity dd .description, #search-results dd .description { font-style: italic; }
367 div#activity dd .description, #search-results dd .description { font-style: italic; }
365 div#activity span.project:after, #search-results span.project:after { content: " -"; }
368 div#activity span.project:after, #search-results span.project:after { content: " -"; }
366 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
369 div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; }
367
370
368 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
371 #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; }
369
372
370 div#search-results-counts {float:right;}
373 div#search-results-counts {float:right;}
371 div#search-results-counts ul { margin-top: 0.5em; }
374 div#search-results-counts ul { margin-top: 0.5em; }
372 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
375 div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; }
373
376
374 dt.issue { background-image: url(../images/ticket.png); }
377 dt.issue { background-image: url(../images/ticket.png); }
375 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
378 dt.issue-edit { background-image: url(../images/ticket_edit.png); }
376 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
379 dt.issue-closed { background-image: url(../images/ticket_checked.png); }
377 dt.issue-note { background-image: url(../images/ticket_note.png); }
380 dt.issue-note { background-image: url(../images/ticket_note.png); }
378 dt.changeset { background-image: url(../images/changeset.png); }
381 dt.changeset { background-image: url(../images/changeset.png); }
379 dt.news { background-image: url(../images/news.png); }
382 dt.news { background-image: url(../images/news.png); }
380 dt.message { background-image: url(../images/message.png); }
383 dt.message { background-image: url(../images/message.png); }
381 dt.reply { background-image: url(../images/comments.png); }
384 dt.reply { background-image: url(../images/comments.png); }
382 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
385 dt.wiki-page { background-image: url(../images/wiki_edit.png); }
383 dt.attachment { background-image: url(../images/attachment.png); }
386 dt.attachment { background-image: url(../images/attachment.png); }
384 dt.document { background-image: url(../images/document.png); }
387 dt.document { background-image: url(../images/document.png); }
385 dt.project { background-image: url(../images/projects.png); }
388 dt.project { background-image: url(../images/projects.png); }
386 dt.time-entry { background-image: url(../images/time.png); }
389 dt.time-entry { background-image: url(../images/time.png); }
387
390
388 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
391 #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); }
389
392
390 div#roadmap .related-issues { margin-bottom: 1em; }
393 div#roadmap .related-issues { margin-bottom: 1em; }
391 div#roadmap .related-issues td.checkbox { display: none; }
394 div#roadmap .related-issues td.checkbox { display: none; }
392 div#roadmap .wiki h1:first-child { display: none; }
395 div#roadmap .wiki h1:first-child { display: none; }
393 div#roadmap .wiki h1 { font-size: 120%; }
396 div#roadmap .wiki h1 { font-size: 120%; }
394 div#roadmap .wiki h2 { font-size: 110%; }
397 div#roadmap .wiki h2 { font-size: 110%; }
395 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
398 body.controller-versions.action-show div#roadmap .related-issues {width:70%;}
396
399
397 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
400 div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; }
398 div#version-summary fieldset { margin-bottom: 1em; }
401 div#version-summary fieldset { margin-bottom: 1em; }
399 div#version-summary fieldset.time-tracking table { width:100%; }
402 div#version-summary fieldset.time-tracking table { width:100%; }
400 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
403 div#version-summary th, div#version-summary td.total-hours { text-align: right; }
401
404
402 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
405 table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; }
403 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
406 table#time-report tbody tr.subtotal { font-style: italic; color:#777;}
404 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
407 table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; }
405 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
408 table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;}
406 table#time-report .hours-dec { font-size: 0.9em; }
409 table#time-report .hours-dec { font-size: 0.9em; }
407
410
408 div.wiki-page .contextual a {opacity: 0.4}
411 div.wiki-page .contextual a {opacity: 0.4}
409 div.wiki-page .contextual a:hover {opacity: 1}
412 div.wiki-page .contextual a:hover {opacity: 1}
410
413
411 form .attributes select { width: 60%; }
414 form .attributes select { width: 60%; }
412 input#issue_subject { width: 99%; }
415 input#issue_subject { width: 99%; }
413 select#issue_done_ratio { width: 95px; }
416 select#issue_done_ratio { width: 95px; }
414
417
415 ul.projects {margin:0; padding-left:1em;}
418 ul.projects {margin:0; padding-left:1em;}
416 ul.projects ul {padding-left:1.6em;}
419 ul.projects ul {padding-left:1.6em;}
417 ul.projects.root {margin:0; padding:0;}
420 ul.projects.root {margin:0; padding:0;}
418 ul.projects li {list-style-type:none;}
421 ul.projects li {list-style-type:none;}
419
422
420 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
423 #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;}
421 #projects-index ul.projects li.root {margin-bottom: 1em;}
424 #projects-index ul.projects li.root {margin-bottom: 1em;}
422 #projects-index ul.projects li.child {margin-top: 1em;}
425 #projects-index ul.projects li.child {margin-top: 1em;}
423 #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; }
426 #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; }
424 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
427 .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
425
428
426 #notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;}
429 #notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;}
427
430
428 #related-issues li img {vertical-align:middle;}
431 #related-issues li img {vertical-align:middle;}
429
432
430 ul.properties {padding:0; font-size: 0.9em; color: #777;}
433 ul.properties {padding:0; font-size: 0.9em; color: #777;}
431 ul.properties li {list-style-type:none;}
434 ul.properties li {list-style-type:none;}
432 ul.properties li span {font-style:italic;}
435 ul.properties li span {font-style:italic;}
433
436
434 .total-hours { font-size: 110%; font-weight: bold; }
437 .total-hours { font-size: 110%; font-weight: bold; }
435 .total-hours span.hours-int { font-size: 120%; }
438 .total-hours span.hours-int { font-size: 120%; }
436
439
437 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
440 .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;}
438 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
441 #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; }
439
442
440 #workflow_copy_form select { width: 200px; }
443 #workflow_copy_form select { width: 200px; }
441 table.transitions td.enabled {background: #bfb;}
444 table.transitions td.enabled {background: #bfb;}
442 table.fields_permissions select {font-size:90%}
445 table.fields_permissions select {font-size:90%}
443 table.fields_permissions td.readonly {background:#ddd;}
446 table.fields_permissions td.readonly {background:#ddd;}
444 table.fields_permissions td.required {background:#d88;}
447 table.fields_permissions td.required {background:#d88;}
445
448
446 textarea#custom_field_possible_values {width: 99%}
449 textarea#custom_field_possible_values {width: 99%}
447 input#content_comments {width: 99%}
450 input#content_comments {width: 99%}
448
451
449 .pagination {font-size: 90%}
452 .pagination {font-size: 90%}
450 p.pagination {margin-top:8px;}
453 p.pagination {margin-top:8px;}
451
454
452 /***** Tabular forms ******/
455 /***** Tabular forms ******/
453 .tabular p{
456 .tabular p{
454 margin: 0;
457 margin: 0;
455 padding: 3px 0 3px 0;
458 padding: 3px 0 3px 0;
456 padding-left: 180px; /* width of left column containing the label elements */
459 padding-left: 180px; /* width of left column containing the label elements */
457 min-height: 1.8em;
460 min-height: 1.8em;
458 clear:left;
461 clear:left;
459 }
462 }
460
463
461 html>body .tabular p {overflow:hidden;}
464 html>body .tabular p {overflow:hidden;}
462
465
463 .tabular label{
466 .tabular label{
464 font-weight: bold;
467 font-weight: bold;
465 float: left;
468 float: left;
466 text-align: right;
469 text-align: right;
467 /* width of left column */
470 /* width of left column */
468 margin-left: -180px;
471 margin-left: -180px;
469 /* width of labels. Should be smaller than left column to create some right margin */
472 /* width of labels. Should be smaller than left column to create some right margin */
470 width: 175px;
473 width: 175px;
471 }
474 }
472
475
473 .tabular label.floating{
476 .tabular label.floating{
474 font-weight: normal;
477 font-weight: normal;
475 margin-left: 0px;
478 margin-left: 0px;
476 text-align: left;
479 text-align: left;
477 width: 270px;
480 width: 270px;
478 }
481 }
479
482
480 .tabular label.block{
483 .tabular label.block{
481 font-weight: normal;
484 font-weight: normal;
482 margin-left: 0px !important;
485 margin-left: 0px !important;
483 text-align: left;
486 text-align: left;
484 float: none;
487 float: none;
485 display: block;
488 display: block;
486 width: auto;
489 width: auto;
487 }
490 }
488
491
489 .tabular label.inline{
492 .tabular label.inline{
490 float:none;
493 float:none;
491 margin-left: 5px !important;
494 margin-left: 5px !important;
492 width: auto;
495 width: auto;
493 }
496 }
494
497
495 label.no-css {
498 label.no-css {
496 font-weight: inherit;
499 font-weight: inherit;
497 float:none;
500 float:none;
498 text-align:left;
501 text-align:left;
499 margin-left:0px;
502 margin-left:0px;
500 width:auto;
503 width:auto;
501 }
504 }
502 input#time_entry_comments { width: 90%;}
505 input#time_entry_comments { width: 90%;}
503
506
504 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
507 #preview fieldset {margin-top: 1em; background: url(../images/draft.png)}
505
508
506 .tabular.settings p{ padding-left: 300px; }
509 .tabular.settings p{ padding-left: 300px; }
507 .tabular.settings label{ margin-left: -300px; width: 295px; }
510 .tabular.settings label{ margin-left: -300px; width: 295px; }
508 .tabular.settings textarea { width: 99%; }
511 .tabular.settings textarea { width: 99%; }
509
512
510 .settings.enabled_scm table {width:100%}
513 .settings.enabled_scm table {width:100%}
511 .settings.enabled_scm td.scm_name{ font-weight: bold; }
514 .settings.enabled_scm td.scm_name{ font-weight: bold; }
512
515
513 fieldset.settings label { display: block; }
516 fieldset.settings label { display: block; }
514 fieldset#notified_events .parent { padding-left: 20px; }
517 fieldset#notified_events .parent { padding-left: 20px; }
515
518
516 span.required {color: #bb0000;}
519 span.required {color: #bb0000;}
517 .summary {font-style: italic;}
520 .summary {font-style: italic;}
518
521
519 #attachments_fields input.description {margin-left: 8px; width:340px;}
522 #attachments_fields input.description {margin-left: 8px; width:340px;}
520 #attachments_fields span {display:block; white-space:nowrap;}
523 #attachments_fields span {display:block; white-space:nowrap;}
521 #attachments_fields img {vertical-align: middle;}
524 #attachments_fields img {vertical-align: middle;}
522
525
523 div.attachments { margin-top: 12px; }
526 div.attachments { margin-top: 12px; }
524 div.attachments p { margin:4px 0 2px 0; }
527 div.attachments p { margin:4px 0 2px 0; }
525 div.attachments img { vertical-align: middle; }
528 div.attachments img { vertical-align: middle; }
526 div.attachments span.author { font-size: 0.9em; color: #888; }
529 div.attachments span.author { font-size: 0.9em; color: #888; }
527
530
528 div.thumbnails {margin-top:0.6em;}
531 div.thumbnails {margin-top:0.6em;}
529 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
532 div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;}
530 div.thumbnails img {margin: 3px;}
533 div.thumbnails img {margin: 3px;}
531
534
532 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
535 p.other-formats { text-align: right; font-size:0.9em; color: #666; }
533 .other-formats span + span:before { content: "| "; }
536 .other-formats span + span:before { content: "| "; }
534
537
535 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
538 a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; }
536
539
537 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
540 em.info {font-style:normal;font-size:90%;color:#888;display:block;}
538 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
541 em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;}
539
542
540 textarea.text_cf {width:90%;}
543 textarea.text_cf {width:90%;}
541
544
542 /* Project members tab */
545 /* Project members tab */
543 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
546 div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% }
544 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
547 div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% }
545 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
548 div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; }
546 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
549 div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; }
547 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
550 div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; }
548 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
551 div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; }
549
552
550 #users_for_watcher {height: 200px; overflow:auto;}
553 #users_for_watcher {height: 200px; overflow:auto;}
551 #users_for_watcher label {display: block;}
554 #users_for_watcher label {display: block;}
552
555
553 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
556 table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; }
554
557
555 input#principal_search, input#user_search {width:100%}
558 input#principal_search, input#user_search {width:100%}
556 input#principal_search, input#user_search {
559 input#principal_search, input#user_search {
557 background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
560 background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px;
558 border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%;
561 border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%;
559 }
562 }
560 input#principal_search.ajax-loading, input#user_search.ajax-loading {
563 input#principal_search.ajax-loading, input#user_search.ajax-loading {
561 background-image: url(../images/loading.gif);
564 background-image: url(../images/loading.gif);
562 }
565 }
563
566
564 * html div#tab-content-members fieldset div { height: 450px; }
567 * html div#tab-content-members fieldset div { height: 450px; }
565
568
566 /***** Flash & error messages ****/
569 /***** Flash & error messages ****/
567 #errorExplanation, div.flash, .nodata, .warning, .conflict {
570 #errorExplanation, div.flash, .nodata, .warning, .conflict {
568 padding: 4px 4px 4px 30px;
571 padding: 4px 4px 4px 30px;
569 margin-bottom: 12px;
572 margin-bottom: 12px;
570 font-size: 1.1em;
573 font-size: 1.1em;
571 border: 2px solid;
574 border: 2px solid;
572 }
575 }
573
576
574 div.flash {margin-top: 8px;}
577 div.flash {margin-top: 8px;}
575
578
576 div.flash.error, #errorExplanation {
579 div.flash.error, #errorExplanation {
577 background: url(../images/exclamation.png) 8px 50% no-repeat;
580 background: url(../images/exclamation.png) 8px 50% no-repeat;
578 background-color: #ffe3e3;
581 background-color: #ffe3e3;
579 border-color: #dd0000;
582 border-color: #dd0000;
580 color: #880000;
583 color: #880000;
581 }
584 }
582
585
583 div.flash.notice {
586 div.flash.notice {
584 background: url(../images/true.png) 8px 5px no-repeat;
587 background: url(../images/true.png) 8px 5px no-repeat;
585 background-color: #dfffdf;
588 background-color: #dfffdf;
586 border-color: #9fcf9f;
589 border-color: #9fcf9f;
587 color: #005f00;
590 color: #005f00;
588 }
591 }
589
592
590 div.flash.warning, .conflict {
593 div.flash.warning, .conflict {
591 background: url(../images/warning.png) 8px 5px no-repeat;
594 background: url(../images/warning.png) 8px 5px no-repeat;
592 background-color: #FFEBC1;
595 background-color: #FFEBC1;
593 border-color: #FDBF3B;
596 border-color: #FDBF3B;
594 color: #A6750C;
597 color: #A6750C;
595 text-align: left;
598 text-align: left;
596 }
599 }
597
600
598 .nodata, .warning {
601 .nodata, .warning {
599 text-align: center;
602 text-align: center;
600 background-color: #FFEBC1;
603 background-color: #FFEBC1;
601 border-color: #FDBF3B;
604 border-color: #FDBF3B;
602 color: #A6750C;
605 color: #A6750C;
603 }
606 }
604
607
605 #errorExplanation ul { font-size: 0.9em;}
608 #errorExplanation ul { font-size: 0.9em;}
606 #errorExplanation h2, #errorExplanation p { display: none; }
609 #errorExplanation h2, #errorExplanation p { display: none; }
607
610
608 .conflict-details {font-size:80%;}
611 .conflict-details {font-size:80%;}
609
612
610 /***** Ajax indicator ******/
613 /***** Ajax indicator ******/
611 #ajax-indicator {
614 #ajax-indicator {
612 position: absolute; /* fixed not supported by IE */
615 position: absolute; /* fixed not supported by IE */
613 background-color:#eee;
616 background-color:#eee;
614 border: 1px solid #bbb;
617 border: 1px solid #bbb;
615 top:35%;
618 top:35%;
616 left:40%;
619 left:40%;
617 width:20%;
620 width:20%;
618 font-weight:bold;
621 font-weight:bold;
619 text-align:center;
622 text-align:center;
620 padding:0.6em;
623 padding:0.6em;
621 z-index:100;
624 z-index:100;
622 opacity: 0.5;
625 opacity: 0.5;
623 }
626 }
624
627
625 html>body #ajax-indicator { position: fixed; }
628 html>body #ajax-indicator { position: fixed; }
626
629
627 #ajax-indicator span {
630 #ajax-indicator span {
628 background-position: 0% 40%;
631 background-position: 0% 40%;
629 background-repeat: no-repeat;
632 background-repeat: no-repeat;
630 background-image: url(../images/loading.gif);
633 background-image: url(../images/loading.gif);
631 padding-left: 26px;
634 padding-left: 26px;
632 vertical-align: bottom;
635 vertical-align: bottom;
633 }
636 }
634
637
635 /***** Calendar *****/
638 /***** Calendar *****/
636 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
639 table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;}
637 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
640 table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; }
638 table.cal thead th.week-number {width: auto;}
641 table.cal thead th.week-number {width: auto;}
639 table.cal tbody tr {height: 100px;}
642 table.cal tbody tr {height: 100px;}
640 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
643 table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;}
641 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
644 table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;}
642 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
645 table.cal td p.day-num {font-size: 1.1em; text-align:right;}
643 table.cal td.odd p.day-num {color: #bbb;}
646 table.cal td.odd p.day-num {color: #bbb;}
644 table.cal td.today {background:#ffffdd;}
647 table.cal td.today {background:#ffffdd;}
645 table.cal td.today p.day-num {font-weight: bold;}
648 table.cal td.today p.day-num {font-weight: bold;}
646 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
649 table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
647 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
650 table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}
648 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
651 table.cal .starting.ending a, p.cal.legend .starting.ending {background: url(../images/bullet_diamond.png) no-repeat -1px -2px; padding-left:16px;}
649 p.cal.legend span {display:block;}
652 p.cal.legend span {display:block;}
650
653
651 /***** Tooltips ******/
654 /***** Tooltips ******/
652 .tooltip{position:relative;z-index:24;}
655 .tooltip{position:relative;z-index:24;}
653 .tooltip:hover{z-index:25;color:#000;}
656 .tooltip:hover{z-index:25;color:#000;}
654 .tooltip span.tip{display: none; text-align:left;}
657 .tooltip span.tip{display: none; text-align:left;}
655
658
656 div.tooltip:hover span.tip{
659 div.tooltip:hover span.tip{
657 display:block;
660 display:block;
658 position:absolute;
661 position:absolute;
659 top:12px; left:24px; width:270px;
662 top:12px; left:24px; width:270px;
660 border:1px solid #555;
663 border:1px solid #555;
661 background-color:#fff;
664 background-color:#fff;
662 padding: 4px;
665 padding: 4px;
663 font-size: 0.8em;
666 font-size: 0.8em;
664 color:#505050;
667 color:#505050;
665 }
668 }
666
669
667 img.ui-datepicker-trigger {
670 img.ui-datepicker-trigger {
668 cursor: pointer;
671 cursor: pointer;
669 vertical-align: middle;
672 vertical-align: middle;
670 margin-left: 4px;
673 margin-left: 4px;
671 }
674 }
672
675
673 /***** Progress bar *****/
676 /***** Progress bar *****/
674 table.progress {
677 table.progress {
675 border-collapse: collapse;
678 border-collapse: collapse;
676 border-spacing: 0pt;
679 border-spacing: 0pt;
677 empty-cells: show;
680 empty-cells: show;
678 text-align: center;
681 text-align: center;
679 float:left;
682 float:left;
680 margin: 1px 6px 1px 0px;
683 margin: 1px 6px 1px 0px;
681 }
684 }
682
685
683 table.progress td { height: 1em; }
686 table.progress td { height: 1em; }
684 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
687 table.progress td.closed { background: #BAE0BA none repeat scroll 0%; }
685 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
688 table.progress td.done { background: #D3EDD3 none repeat scroll 0%; }
686 table.progress td.todo { background: #eee none repeat scroll 0%; }
689 table.progress td.todo { background: #eee none repeat scroll 0%; }
687 p.pourcent {font-size: 80%;}
690 p.pourcent {font-size: 80%;}
688 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
691 p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;}
689
692
690 #roadmap table.progress td { height: 1.2em; }
693 #roadmap table.progress td { height: 1.2em; }
691 /***** Tabs *****/
694 /***** Tabs *****/
692 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
695 #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;}
693 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
696 #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;}
694 #content .tabs ul li {
697 #content .tabs ul li {
695 float:left;
698 float:left;
696 list-style-type:none;
699 list-style-type:none;
697 white-space:nowrap;
700 white-space:nowrap;
698 margin-right:4px;
701 margin-right:4px;
699 background:#fff;
702 background:#fff;
700 position:relative;
703 position:relative;
701 margin-bottom:-1px;
704 margin-bottom:-1px;
702 }
705 }
703 #content .tabs ul li a{
706 #content .tabs ul li a{
704 display:block;
707 display:block;
705 font-size: 0.9em;
708 font-size: 0.9em;
706 text-decoration:none;
709 text-decoration:none;
707 line-height:1.3em;
710 line-height:1.3em;
708 padding:4px 6px 4px 6px;
711 padding:4px 6px 4px 6px;
709 border: 1px solid #ccc;
712 border: 1px solid #ccc;
710 border-bottom: 1px solid #bbbbbb;
713 border-bottom: 1px solid #bbbbbb;
711 background-color: #f6f6f6;
714 background-color: #f6f6f6;
712 color:#999;
715 color:#999;
713 font-weight:bold;
716 font-weight:bold;
714 border-top-left-radius:3px;
717 border-top-left-radius:3px;
715 border-top-right-radius:3px;
718 border-top-right-radius:3px;
716 }
719 }
717
720
718 #content .tabs ul li a:hover {
721 #content .tabs ul li a:hover {
719 background-color: #ffffdd;
722 background-color: #ffffdd;
720 text-decoration:none;
723 text-decoration:none;
721 }
724 }
722
725
723 #content .tabs ul li a.selected {
726 #content .tabs ul li a.selected {
724 background-color: #fff;
727 background-color: #fff;
725 border: 1px solid #bbbbbb;
728 border: 1px solid #bbbbbb;
726 border-bottom: 1px solid #fff;
729 border-bottom: 1px solid #fff;
727 color:#444;
730 color:#444;
728 }
731 }
729
732
730 #content .tabs ul li a.selected:hover {background-color: #fff;}
733 #content .tabs ul li a.selected:hover {background-color: #fff;}
731
734
732 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
735 div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; }
733
736
734 button.tab-left, button.tab-right {
737 button.tab-left, button.tab-right {
735 font-size: 0.9em;
738 font-size: 0.9em;
736 cursor: pointer;
739 cursor: pointer;
737 height:24px;
740 height:24px;
738 border: 1px solid #ccc;
741 border: 1px solid #ccc;
739 border-bottom: 1px solid #bbbbbb;
742 border-bottom: 1px solid #bbbbbb;
740 position:absolute;
743 position:absolute;
741 padding:4px;
744 padding:4px;
742 width: 20px;
745 width: 20px;
743 bottom: -1px;
746 bottom: -1px;
744 }
747 }
745
748
746 button.tab-left {
749 button.tab-left {
747 right: 20px;
750 right: 20px;
748 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
751 background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%;
749 border-top-left-radius:3px;
752 border-top-left-radius:3px;
750 }
753 }
751
754
752 button.tab-right {
755 button.tab-right {
753 right: 0;
756 right: 0;
754 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
757 background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%;
755 border-top-right-radius:3px;
758 border-top-right-radius:3px;
756 }
759 }
757
760
758 /***** Diff *****/
761 /***** Diff *****/
759 .diff_out { background: #fcc; }
762 .diff_out { background: #fcc; }
760 .diff_out span { background: #faa; }
763 .diff_out span { background: #faa; }
761 .diff_in { background: #cfc; }
764 .diff_in { background: #cfc; }
762 .diff_in span { background: #afa; }
765 .diff_in span { background: #afa; }
763
766
764 .text-diff {
767 .text-diff {
765 padding: 1em;
768 padding: 1em;
766 background-color:#f6f6f6;
769 background-color:#f6f6f6;
767 color:#505050;
770 color:#505050;
768 border: 1px solid #e4e4e4;
771 border: 1px solid #e4e4e4;
769 }
772 }
770
773
771 /***** Wiki *****/
774 /***** Wiki *****/
772 div.wiki table {
775 div.wiki table {
773 border-collapse: collapse;
776 border-collapse: collapse;
774 margin-bottom: 1em;
777 margin-bottom: 1em;
775 }
778 }
776
779
777 div.wiki table, div.wiki td, div.wiki th {
780 div.wiki table, div.wiki td, div.wiki th {
778 border: 1px solid #bbb;
781 border: 1px solid #bbb;
779 padding: 4px;
782 padding: 4px;
780 }
783 }
781
784
782 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
785 div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;}
783
786
784 div.wiki .external {
787 div.wiki .external {
785 background-position: 0% 60%;
788 background-position: 0% 60%;
786 background-repeat: no-repeat;
789 background-repeat: no-repeat;
787 padding-left: 12px;
790 padding-left: 12px;
788 background-image: url(../images/external.png);
791 background-image: url(../images/external.png);
789 }
792 }
790
793
791 div.wiki a.new {color: #b73535;}
794 div.wiki a.new {color: #b73535;}
792
795
793 div.wiki ul, div.wiki ol {margin-bottom:1em;}
796 div.wiki ul, div.wiki ol {margin-bottom:1em;}
794
797
795 div.wiki pre {
798 div.wiki pre {
796 margin: 1em 1em 1em 1.6em;
799 margin: 1em 1em 1em 1.6em;
797 padding: 8px;
800 padding: 8px;
798 background-color: #fafafa;
801 background-color: #fafafa;
799 border: 1px solid #e2e2e2;
802 border: 1px solid #e2e2e2;
800 width:auto;
803 width:auto;
801 overflow-x: auto;
804 overflow-x: auto;
802 overflow-y: hidden;
805 overflow-y: hidden;
803 }
806 }
804
807
805 div.wiki ul.toc {
808 div.wiki ul.toc {
806 background-color: #ffffdd;
809 background-color: #ffffdd;
807 border: 1px solid #e4e4e4;
810 border: 1px solid #e4e4e4;
808 padding: 4px;
811 padding: 4px;
809 line-height: 1.2em;
812 line-height: 1.2em;
810 margin-bottom: 12px;
813 margin-bottom: 12px;
811 margin-right: 12px;
814 margin-right: 12px;
812 margin-left: 0;
815 margin-left: 0;
813 display: table
816 display: table
814 }
817 }
815 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
818 * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */
816
819
817 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
820 div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; }
818 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
821 div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; }
819 div.wiki ul.toc ul { margin: 0; padding: 0; }
822 div.wiki ul.toc ul { margin: 0; padding: 0; }
820 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
823 div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;}
821 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
824 div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;}
822 div.wiki ul.toc a {
825 div.wiki ul.toc a {
823 font-size: 0.9em;
826 font-size: 0.9em;
824 font-weight: normal;
827 font-weight: normal;
825 text-decoration: none;
828 text-decoration: none;
826 color: #606060;
829 color: #606060;
827 }
830 }
828 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
831 div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;}
829
832
830 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
833 a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; }
831 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
834 a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; }
832 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
835 h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; }
833
836
834 div.wiki img { vertical-align: middle; }
837 div.wiki img { vertical-align: middle; }
835
838
836 /***** My page layout *****/
839 /***** My page layout *****/
837 .block-receiver {
840 .block-receiver {
838 border:1px dashed #c0c0c0;
841 border:1px dashed #c0c0c0;
839 margin-bottom: 20px;
842 margin-bottom: 20px;
840 padding: 15px 0 15px 0;
843 padding: 15px 0 15px 0;
841 }
844 }
842
845
843 .mypage-box {
846 .mypage-box {
844 margin:0 0 20px 0;
847 margin:0 0 20px 0;
845 color:#505050;
848 color:#505050;
846 line-height:1.5em;
849 line-height:1.5em;
847 }
850 }
848
851
849 .handle {cursor: move;}
852 .handle {cursor: move;}
850
853
851 a.close-icon {
854 a.close-icon {
852 display:block;
855 display:block;
853 margin-top:3px;
856 margin-top:3px;
854 overflow:hidden;
857 overflow:hidden;
855 width:12px;
858 width:12px;
856 height:12px;
859 height:12px;
857 background-repeat: no-repeat;
860 background-repeat: no-repeat;
858 cursor:pointer;
861 cursor:pointer;
859 background-image:url('../images/close.png');
862 background-image:url('../images/close.png');
860 }
863 }
861 a.close-icon:hover {background-image:url('../images/close_hl.png');}
864 a.close-icon:hover {background-image:url('../images/close_hl.png');}
862
865
863 /***** Gantt chart *****/
866 /***** Gantt chart *****/
864 .gantt_hdr {
867 .gantt_hdr {
865 position:absolute;
868 position:absolute;
866 top:0;
869 top:0;
867 height:16px;
870 height:16px;
868 border-top: 1px solid #c0c0c0;
871 border-top: 1px solid #c0c0c0;
869 border-bottom: 1px solid #c0c0c0;
872 border-bottom: 1px solid #c0c0c0;
870 border-right: 1px solid #c0c0c0;
873 border-right: 1px solid #c0c0c0;
871 text-align: center;
874 text-align: center;
872 overflow: hidden;
875 overflow: hidden;
873 }
876 }
874
877
875 .gantt_subjects { font-size: 0.8em; }
878 .gantt_subjects { font-size: 0.8em; }
876 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
879 .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; }
877
880
878 .task {
881 .task {
879 position: absolute;
882 position: absolute;
880 height:8px;
883 height:8px;
881 font-size:0.8em;
884 font-size:0.8em;
882 color:#888;
885 color:#888;
883 padding:0;
886 padding:0;
884 margin:0;
887 margin:0;
885 line-height:16px;
888 line-height:16px;
886 white-space:nowrap;
889 white-space:nowrap;
887 }
890 }
888
891
889 .task.label {width:100%;}
892 .task.label {width:100%;}
890 .task.label.project, .task.label.version { font-weight: bold; }
893 .task.label.project, .task.label.version { font-weight: bold; }
891
894
892 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
895 .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
893 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
896 .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
894 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
897 .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
895
898
896 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
899 .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;}
897 .task_late.parent, .task_done.parent { height: 3px;}
900 .task_late.parent, .task_done.parent { height: 3px;}
898 .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;}
901 .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;}
899 .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;}
902 .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;}
900
903
901 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
904 .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
902 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
905 .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
903 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
906 .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
904 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
907 .version.marker { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
905
908
906 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
909 .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
907 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
910 .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
908 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
911 .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
909 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
912 .project.marker { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; margin-left: -4px; margin-top: 1px; }
910
913
911 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
914 .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
912 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
915 .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
913
916
914 /***** Icons *****/
917 /***** Icons *****/
915 .icon {
918 .icon {
916 background-position: 0% 50%;
919 background-position: 0% 50%;
917 background-repeat: no-repeat;
920 background-repeat: no-repeat;
918 padding-left: 20px;
921 padding-left: 20px;
919 padding-top: 2px;
922 padding-top: 2px;
920 padding-bottom: 3px;
923 padding-bottom: 3px;
921 }
924 }
922
925
923 .icon-add { background-image: url(../images/add.png); }
926 .icon-add { background-image: url(../images/add.png); }
924 .icon-edit { background-image: url(../images/edit.png); }
927 .icon-edit { background-image: url(../images/edit.png); }
925 .icon-copy { background-image: url(../images/copy.png); }
928 .icon-copy { background-image: url(../images/copy.png); }
926 .icon-duplicate { background-image: url(../images/duplicate.png); }
929 .icon-duplicate { background-image: url(../images/duplicate.png); }
927 .icon-del { background-image: url(../images/delete.png); }
930 .icon-del { background-image: url(../images/delete.png); }
928 .icon-move { background-image: url(../images/move.png); }
931 .icon-move { background-image: url(../images/move.png); }
929 .icon-save { background-image: url(../images/save.png); }
932 .icon-save { background-image: url(../images/save.png); }
930 .icon-cancel { background-image: url(../images/cancel.png); }
933 .icon-cancel { background-image: url(../images/cancel.png); }
931 .icon-multiple { background-image: url(../images/table_multiple.png); }
934 .icon-multiple { background-image: url(../images/table_multiple.png); }
932 .icon-folder { background-image: url(../images/folder.png); }
935 .icon-folder { background-image: url(../images/folder.png); }
933 .open .icon-folder { background-image: url(../images/folder_open.png); }
936 .open .icon-folder { background-image: url(../images/folder_open.png); }
934 .icon-package { background-image: url(../images/package.png); }
937 .icon-package { background-image: url(../images/package.png); }
935 .icon-user { background-image: url(../images/user.png); }
938 .icon-user { background-image: url(../images/user.png); }
936 .icon-projects { background-image: url(../images/projects.png); }
939 .icon-projects { background-image: url(../images/projects.png); }
937 .icon-help { background-image: url(../images/help.png); }
940 .icon-help { background-image: url(../images/help.png); }
938 .icon-attachment { background-image: url(../images/attachment.png); }
941 .icon-attachment { background-image: url(../images/attachment.png); }
939 .icon-history { background-image: url(../images/history.png); }
942 .icon-history { background-image: url(../images/history.png); }
940 .icon-time { background-image: url(../images/time.png); }
943 .icon-time { background-image: url(../images/time.png); }
941 .icon-time-add { background-image: url(../images/time_add.png); }
944 .icon-time-add { background-image: url(../images/time_add.png); }
942 .icon-stats { background-image: url(../images/stats.png); }
945 .icon-stats { background-image: url(../images/stats.png); }
943 .icon-warning { background-image: url(../images/warning.png); }
946 .icon-warning { background-image: url(../images/warning.png); }
944 .icon-fav { background-image: url(../images/fav.png); }
947 .icon-fav { background-image: url(../images/fav.png); }
945 .icon-fav-off { background-image: url(../images/fav_off.png); }
948 .icon-fav-off { background-image: url(../images/fav_off.png); }
946 .icon-reload { background-image: url(../images/reload.png); }
949 .icon-reload { background-image: url(../images/reload.png); }
947 .icon-lock { background-image: url(../images/locked.png); }
950 .icon-lock { background-image: url(../images/locked.png); }
948 .icon-unlock { background-image: url(../images/unlock.png); }
951 .icon-unlock { background-image: url(../images/unlock.png); }
949 .icon-checked { background-image: url(../images/true.png); }
952 .icon-checked { background-image: url(../images/true.png); }
950 .icon-details { background-image: url(../images/zoom_in.png); }
953 .icon-details { background-image: url(../images/zoom_in.png); }
951 .icon-report { background-image: url(../images/report.png); }
954 .icon-report { background-image: url(../images/report.png); }
952 .icon-comment { background-image: url(../images/comment.png); }
955 .icon-comment { background-image: url(../images/comment.png); }
953 .icon-summary { background-image: url(../images/lightning.png); }
956 .icon-summary { background-image: url(../images/lightning.png); }
954 .icon-server-authentication { background-image: url(../images/server_key.png); }
957 .icon-server-authentication { background-image: url(../images/server_key.png); }
955 .icon-issue { background-image: url(../images/ticket.png); }
958 .icon-issue { background-image: url(../images/ticket.png); }
956 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
959 .icon-zoom-in { background-image: url(../images/zoom_in.png); }
957 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
960 .icon-zoom-out { background-image: url(../images/zoom_out.png); }
958 .icon-passwd { background-image: url(../images/textfield_key.png); }
961 .icon-passwd { background-image: url(../images/textfield_key.png); }
959 .icon-test { background-image: url(../images/bullet_go.png); }
962 .icon-test { background-image: url(../images/bullet_go.png); }
960
963
961 .icon-file { background-image: url(../images/files/default.png); }
964 .icon-file { background-image: url(../images/files/default.png); }
962 .icon-file.text-plain { background-image: url(../images/files/text.png); }
965 .icon-file.text-plain { background-image: url(../images/files/text.png); }
963 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
966 .icon-file.text-x-c { background-image: url(../images/files/c.png); }
964 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
967 .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); }
965 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
968 .icon-file.text-x-java { background-image: url(../images/files/java.png); }
966 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
969 .icon-file.text-x-javascript { background-image: url(../images/files/js.png); }
967 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
970 .icon-file.text-x-php { background-image: url(../images/files/php.png); }
968 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
971 .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); }
969 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
972 .icon-file.text-xml { background-image: url(../images/files/xml.png); }
970 .icon-file.text-css { background-image: url(../images/files/css.png); }
973 .icon-file.text-css { background-image: url(../images/files/css.png); }
971 .icon-file.text-html { background-image: url(../images/files/html.png); }
974 .icon-file.text-html { background-image: url(../images/files/html.png); }
972 .icon-file.image-gif { background-image: url(../images/files/image.png); }
975 .icon-file.image-gif { background-image: url(../images/files/image.png); }
973 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
976 .icon-file.image-jpeg { background-image: url(../images/files/image.png); }
974 .icon-file.image-png { background-image: url(../images/files/image.png); }
977 .icon-file.image-png { background-image: url(../images/files/image.png); }
975 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
978 .icon-file.image-tiff { background-image: url(../images/files/image.png); }
976 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
979 .icon-file.application-pdf { background-image: url(../images/files/pdf.png); }
977 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
980 .icon-file.application-zip { background-image: url(../images/files/zip.png); }
978 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
981 .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); }
979
982
980 img.gravatar {
983 img.gravatar {
981 padding: 2px;
984 padding: 2px;
982 border: solid 1px #d5d5d5;
985 border: solid 1px #d5d5d5;
983 background: #fff;
986 background: #fff;
984 vertical-align: middle;
987 vertical-align: middle;
985 }
988 }
986
989
987 div.issue img.gravatar {
990 div.issue img.gravatar {
988 float: left;
991 float: left;
989 margin: 0 6px 0 0;
992 margin: 0 6px 0 0;
990 padding: 5px;
993 padding: 5px;
991 }
994 }
992
995
993 div.issue table img.gravatar {
996 div.issue table img.gravatar {
994 height: 14px;
997 height: 14px;
995 width: 14px;
998 width: 14px;
996 padding: 2px;
999 padding: 2px;
997 float: left;
1000 float: left;
998 margin: 0 0.5em 0 0;
1001 margin: 0 0.5em 0 0;
999 }
1002 }
1000
1003
1001 h2 img.gravatar {margin: -2px 4px -4px 0;}
1004 h2 img.gravatar {margin: -2px 4px -4px 0;}
1002 h3 img.gravatar {margin: -4px 4px -4px 0;}
1005 h3 img.gravatar {margin: -4px 4px -4px 0;}
1003 h4 img.gravatar {margin: -6px 4px -4px 0;}
1006 h4 img.gravatar {margin: -6px 4px -4px 0;}
1004 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1007 td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;}
1005 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1008 #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;}
1006 /* Used on 12px Gravatar img tags without the icon background */
1009 /* Used on 12px Gravatar img tags without the icon background */
1007 .icon-gravatar {float: left; margin-right: 4px;}
1010 .icon-gravatar {float: left; margin-right: 4px;}
1008
1011
1009 #activity dt, .journal {clear: left;}
1012 #activity dt, .journal {clear: left;}
1010
1013
1011 .journal-link {float: right;}
1014 .journal-link {float: right;}
1012
1015
1013 h2 img { vertical-align:middle; }
1016 h2 img { vertical-align:middle; }
1014
1017
1015 .hascontextmenu { cursor: context-menu; }
1018 .hascontextmenu { cursor: context-menu; }
1016
1019
1017 /************* CodeRay styles *************/
1020 /************* CodeRay styles *************/
1018 .syntaxhl div {display: inline;}
1021 .syntaxhl div {display: inline;}
1019 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1022 .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;}
1020 .syntaxhl .code pre { overflow: auto }
1023 .syntaxhl .code pre { overflow: auto }
1021 .syntaxhl .debug { color: white !important; background: blue !important; }
1024 .syntaxhl .debug { color: white !important; background: blue !important; }
1022
1025
1023 .syntaxhl .annotation { color:#007 }
1026 .syntaxhl .annotation { color:#007 }
1024 .syntaxhl .attribute-name { color:#b48 }
1027 .syntaxhl .attribute-name { color:#b48 }
1025 .syntaxhl .attribute-value { color:#700 }
1028 .syntaxhl .attribute-value { color:#700 }
1026 .syntaxhl .binary { color:#509 }
1029 .syntaxhl .binary { color:#509 }
1027 .syntaxhl .char .content { color:#D20 }
1030 .syntaxhl .char .content { color:#D20 }
1028 .syntaxhl .char .delimiter { color:#710 }
1031 .syntaxhl .char .delimiter { color:#710 }
1029 .syntaxhl .char { color:#D20 }
1032 .syntaxhl .char { color:#D20 }
1030 .syntaxhl .class { color:#258; font-weight:bold }
1033 .syntaxhl .class { color:#258; font-weight:bold }
1031 .syntaxhl .class-variable { color:#369 }
1034 .syntaxhl .class-variable { color:#369 }
1032 .syntaxhl .color { color:#0A0 }
1035 .syntaxhl .color { color:#0A0 }
1033 .syntaxhl .comment { color:#385 }
1036 .syntaxhl .comment { color:#385 }
1034 .syntaxhl .comment .char { color:#385 }
1037 .syntaxhl .comment .char { color:#385 }
1035 .syntaxhl .comment .delimiter { color:#385 }
1038 .syntaxhl .comment .delimiter { color:#385 }
1036 .syntaxhl .complex { color:#A08 }
1039 .syntaxhl .complex { color:#A08 }
1037 .syntaxhl .constant { color:#258; font-weight:bold }
1040 .syntaxhl .constant { color:#258; font-weight:bold }
1038 .syntaxhl .decorator { color:#B0B }
1041 .syntaxhl .decorator { color:#B0B }
1039 .syntaxhl .definition { color:#099; font-weight:bold }
1042 .syntaxhl .definition { color:#099; font-weight:bold }
1040 .syntaxhl .delimiter { color:black }
1043 .syntaxhl .delimiter { color:black }
1041 .syntaxhl .directive { color:#088; font-weight:bold }
1044 .syntaxhl .directive { color:#088; font-weight:bold }
1042 .syntaxhl .doc { color:#970 }
1045 .syntaxhl .doc { color:#970 }
1043 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1046 .syntaxhl .doc-string { color:#D42; font-weight:bold }
1044 .syntaxhl .doctype { color:#34b }
1047 .syntaxhl .doctype { color:#34b }
1045 .syntaxhl .entity { color:#800; font-weight:bold }
1048 .syntaxhl .entity { color:#800; font-weight:bold }
1046 .syntaxhl .error { color:#F00; background-color:#FAA }
1049 .syntaxhl .error { color:#F00; background-color:#FAA }
1047 .syntaxhl .escape { color:#666 }
1050 .syntaxhl .escape { color:#666 }
1048 .syntaxhl .exception { color:#C00; font-weight:bold }
1051 .syntaxhl .exception { color:#C00; font-weight:bold }
1049 .syntaxhl .float { color:#06D }
1052 .syntaxhl .float { color:#06D }
1050 .syntaxhl .function { color:#06B; font-weight:bold }
1053 .syntaxhl .function { color:#06B; font-weight:bold }
1051 .syntaxhl .global-variable { color:#d70 }
1054 .syntaxhl .global-variable { color:#d70 }
1052 .syntaxhl .hex { color:#02b }
1055 .syntaxhl .hex { color:#02b }
1053 .syntaxhl .imaginary { color:#f00 }
1056 .syntaxhl .imaginary { color:#f00 }
1054 .syntaxhl .include { color:#B44; font-weight:bold }
1057 .syntaxhl .include { color:#B44; font-weight:bold }
1055 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1058 .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black }
1056 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1059 .syntaxhl .inline-delimiter { font-weight: bold; color: #666 }
1057 .syntaxhl .instance-variable { color:#33B }
1060 .syntaxhl .instance-variable { color:#33B }
1058 .syntaxhl .integer { color:#06D }
1061 .syntaxhl .integer { color:#06D }
1059 .syntaxhl .key .char { color: #60f }
1062 .syntaxhl .key .char { color: #60f }
1060 .syntaxhl .key .delimiter { color: #404 }
1063 .syntaxhl .key .delimiter { color: #404 }
1061 .syntaxhl .key { color: #606 }
1064 .syntaxhl .key { color: #606 }
1062 .syntaxhl .keyword { color:#939; font-weight:bold }
1065 .syntaxhl .keyword { color:#939; font-weight:bold }
1063 .syntaxhl .label { color:#970; font-weight:bold }
1066 .syntaxhl .label { color:#970; font-weight:bold }
1064 .syntaxhl .local-variable { color:#963 }
1067 .syntaxhl .local-variable { color:#963 }
1065 .syntaxhl .namespace { color:#707; font-weight:bold }
1068 .syntaxhl .namespace { color:#707; font-weight:bold }
1066 .syntaxhl .octal { color:#40E }
1069 .syntaxhl .octal { color:#40E }
1067 .syntaxhl .operator { }
1070 .syntaxhl .operator { }
1068 .syntaxhl .predefined { color:#369; font-weight:bold }
1071 .syntaxhl .predefined { color:#369; font-weight:bold }
1069 .syntaxhl .predefined-constant { color:#069 }
1072 .syntaxhl .predefined-constant { color:#069 }
1070 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1073 .syntaxhl .predefined-type { color:#0a5; font-weight:bold }
1071 .syntaxhl .preprocessor { color:#579 }
1074 .syntaxhl .preprocessor { color:#579 }
1072 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1075 .syntaxhl .pseudo-class { color:#00C; font-weight:bold }
1073 .syntaxhl .regexp .content { color:#808 }
1076 .syntaxhl .regexp .content { color:#808 }
1074 .syntaxhl .regexp .delimiter { color:#404 }
1077 .syntaxhl .regexp .delimiter { color:#404 }
1075 .syntaxhl .regexp .modifier { color:#C2C }
1078 .syntaxhl .regexp .modifier { color:#C2C }
1076 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1079 .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); }
1077 .syntaxhl .reserved { color:#080; font-weight:bold }
1080 .syntaxhl .reserved { color:#080; font-weight:bold }
1078 .syntaxhl .shell .content { color:#2B2 }
1081 .syntaxhl .shell .content { color:#2B2 }
1079 .syntaxhl .shell .delimiter { color:#161 }
1082 .syntaxhl .shell .delimiter { color:#161 }
1080 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1083 .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); }
1081 .syntaxhl .string .char { color: #46a }
1084 .syntaxhl .string .char { color: #46a }
1082 .syntaxhl .string .content { color: #46a }
1085 .syntaxhl .string .content { color: #46a }
1083 .syntaxhl .string .delimiter { color: #46a }
1086 .syntaxhl .string .delimiter { color: #46a }
1084 .syntaxhl .string .modifier { color: #46a }
1087 .syntaxhl .string .modifier { color: #46a }
1085 .syntaxhl .symbol .content { color:#d33 }
1088 .syntaxhl .symbol .content { color:#d33 }
1086 .syntaxhl .symbol .delimiter { color:#d33 }
1089 .syntaxhl .symbol .delimiter { color:#d33 }
1087 .syntaxhl .symbol { color:#d33 }
1090 .syntaxhl .symbol { color:#d33 }
1088 .syntaxhl .tag { color:#070 }
1091 .syntaxhl .tag { color:#070 }
1089 .syntaxhl .type { color:#339; font-weight:bold }
1092 .syntaxhl .type { color:#339; font-weight:bold }
1090 .syntaxhl .value { color: #088; }
1093 .syntaxhl .value { color: #088; }
1091 .syntaxhl .variable { color:#037 }
1094 .syntaxhl .variable { color:#037 }
1092
1095
1093 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1096 .syntaxhl .insert { background: hsla(120,100%,50%,0.12) }
1094 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1097 .syntaxhl .delete { background: hsla(0,100%,50%,0.12) }
1095 .syntaxhl .change { color: #bbf; background: #007; }
1098 .syntaxhl .change { color: #bbf; background: #007; }
1096 .syntaxhl .head { color: #f8f; background: #505 }
1099 .syntaxhl .head { color: #f8f; background: #505 }
1097 .syntaxhl .head .filename { color: white; }
1100 .syntaxhl .head .filename { color: white; }
1098
1101
1099 .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; }
1102 .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; }
1100 .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; }
1103 .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; }
1101
1104
1102 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1105 .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold }
1103 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1106 .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold }
1104 .syntaxhl .change .change { color: #88f }
1107 .syntaxhl .change .change { color: #88f }
1105 .syntaxhl .head .head { color: #f4f }
1108 .syntaxhl .head .head { color: #f4f }
1106
1109
1107 /***** Media print specific styles *****/
1110 /***** Media print specific styles *****/
1108 @media print {
1111 @media print {
1109 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1112 #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; }
1110 #main { background: #fff; }
1113 #main { background: #fff; }
1111 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1114 #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
1112 #wiki_add_attachment { display:none; }
1115 #wiki_add_attachment { display:none; }
1113 .hide-when-print { display: none; }
1116 .hide-when-print { display: none; }
1114 .autoscroll {overflow-x: visible;}
1117 .autoscroll {overflow-x: visible;}
1115 table.list {margin-top:0.5em;}
1118 table.list {margin-top:0.5em;}
1116 table.list th, table.list td {border: 1px solid #aaa;}
1119 table.list th, table.list td {border: 1px solid #aaa;}
1117 }
1120 }
1118
1121
1119 /* Accessibility specific styles */
1122 /* Accessibility specific styles */
1120 .hidden-for-sighted {
1123 .hidden-for-sighted {
1121 position:absolute;
1124 position:absolute;
1122 left:-10000px;
1125 left:-10000px;
1123 top:auto;
1126 top:auto;
1124 width:1px;
1127 width:1px;
1125 height:1px;
1128 height:1px;
1126 overflow:hidden;
1129 overflow:hidden;
1127 }
1130 }
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 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