The requested changes are too big and content was truncated. Show full diff
@@ -1,1274 +1,1276 | |||
|
1 | 1 | # encoding: utf-8 |
|
2 | 2 | # |
|
3 | 3 | # Redmine - project management software |
|
4 | 4 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
5 | 5 | # |
|
6 | 6 | # This program is free software; you can redistribute it and/or |
|
7 | 7 | # modify it under the terms of the GNU General Public License |
|
8 | 8 | # as published by the Free Software Foundation; either version 2 |
|
9 | 9 | # of the License, or (at your option) any later version. |
|
10 | 10 | # |
|
11 | 11 | # This program is distributed in the hope that it will be useful, |
|
12 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 | 14 | # GNU General Public License for more details. |
|
15 | 15 | # |
|
16 | 16 | # You should have received a copy of the GNU General Public License |
|
17 | 17 | # along with this program; if not, write to the Free Software |
|
18 | 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
19 | 19 | |
|
20 | 20 | require 'forwardable' |
|
21 | 21 | require 'cgi' |
|
22 | 22 | |
|
23 | 23 | module ApplicationHelper |
|
24 | 24 | include Redmine::WikiFormatting::Macros::Definitions |
|
25 | 25 | include Redmine::I18n |
|
26 | 26 | include GravatarHelper::PublicMethods |
|
27 | 27 | |
|
28 | 28 | extend Forwardable |
|
29 | 29 | def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter |
|
30 | 30 | |
|
31 | 31 | # Return true if user is authorized for controller/action, otherwise false |
|
32 | 32 | def authorize_for(controller, action) |
|
33 | 33 | User.current.allowed_to?({:controller => controller, :action => action}, @project) |
|
34 | 34 | end |
|
35 | 35 | |
|
36 | 36 | # Display a link if user is authorized |
|
37 | 37 | # |
|
38 | 38 | # @param [String] name Anchor text (passed to link_to) |
|
39 | 39 | # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized |
|
40 | 40 | # @param [optional, Hash] html_options Options passed to link_to |
|
41 | 41 | # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to |
|
42 | 42 | def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference) |
|
43 | 43 | link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action]) |
|
44 | 44 | end |
|
45 | 45 | |
|
46 | 46 | # Displays a link to user's account page if active |
|
47 | 47 | def link_to_user(user, options={}) |
|
48 | 48 | if user.is_a?(User) |
|
49 | 49 | name = h(user.name(options[:format])) |
|
50 | 50 | if user.active? |
|
51 | 51 | link_to name, :controller => 'users', :action => 'show', :id => user |
|
52 | 52 | else |
|
53 | 53 | name |
|
54 | 54 | end |
|
55 | 55 | else |
|
56 | 56 | h(user.to_s) |
|
57 | 57 | end |
|
58 | 58 | end |
|
59 | 59 | |
|
60 | 60 | # Displays a link to +issue+ with its subject. |
|
61 | 61 | # Examples: |
|
62 | 62 | # |
|
63 | 63 | # link_to_issue(issue) # => Defect #6: This is the subject |
|
64 | 64 | # link_to_issue(issue, :truncate => 6) # => Defect #6: This i... |
|
65 | 65 | # link_to_issue(issue, :subject => false) # => Defect #6 |
|
66 | 66 | # link_to_issue(issue, :project => true) # => Foo - Defect #6 |
|
67 | # link_to_issue(issue, :subject => false, :tracker => false) # => #6 | |
|
67 | 68 | # |
|
68 | 69 | def link_to_issue(issue, options={}) |
|
69 | 70 | title = nil |
|
70 | 71 | subject = nil |
|
72 | text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}" | |
|
71 | 73 | if options[:subject] == false |
|
72 | 74 | title = truncate(issue.subject, :length => 60) |
|
73 | 75 | else |
|
74 | 76 | subject = issue.subject |
|
75 | 77 | if options[:truncate] |
|
76 | 78 | subject = truncate(subject, :length => options[:truncate]) |
|
77 | 79 | end |
|
78 | 80 | end |
|
79 |
s = link_to |
|
|
81 | s = link_to text, {:controller => "issues", :action => "show", :id => issue}, | |
|
80 | 82 | :class => issue.css_classes, |
|
81 | 83 | :title => title |
|
82 | 84 | s << h(": #{subject}") if subject |
|
83 | 85 | s = h("#{issue.project} - ") + s if options[:project] |
|
84 | 86 | s |
|
85 | 87 | end |
|
86 | 88 | |
|
87 | 89 | # Generates a link to an attachment. |
|
88 | 90 | # Options: |
|
89 | 91 | # * :text - Link text (default to attachment filename) |
|
90 | 92 | # * :download - Force download (default: false) |
|
91 | 93 | def link_to_attachment(attachment, options={}) |
|
92 | 94 | text = options.delete(:text) || attachment.filename |
|
93 | 95 | action = options.delete(:download) ? 'download' : 'show' |
|
94 | 96 | opt_only_path = {} |
|
95 | 97 | opt_only_path[:only_path] = (options[:only_path] == false ? false : true) |
|
96 | 98 | options.delete(:only_path) |
|
97 | 99 | link_to(h(text), |
|
98 | 100 | {:controller => 'attachments', :action => action, |
|
99 | 101 | :id => attachment, :filename => attachment.filename}.merge(opt_only_path), |
|
100 | 102 | options) |
|
101 | 103 | end |
|
102 | 104 | |
|
103 | 105 | # Generates a link to a SCM revision |
|
104 | 106 | # Options: |
|
105 | 107 | # * :text - Link text (default to the formatted revision) |
|
106 | 108 | def link_to_revision(revision, repository, options={}) |
|
107 | 109 | if repository.is_a?(Project) |
|
108 | 110 | repository = repository.repository |
|
109 | 111 | end |
|
110 | 112 | text = options.delete(:text) || format_revision(revision) |
|
111 | 113 | rev = revision.respond_to?(:identifier) ? revision.identifier : revision |
|
112 | 114 | link_to( |
|
113 | 115 | h(text), |
|
114 | 116 | {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev}, |
|
115 | 117 | :title => l(:label_revision_id, format_revision(revision)) |
|
116 | 118 | ) |
|
117 | 119 | end |
|
118 | 120 | |
|
119 | 121 | # Generates a link to a message |
|
120 | 122 | def link_to_message(message, options={}, html_options = nil) |
|
121 | 123 | link_to( |
|
122 | 124 | h(truncate(message.subject, :length => 60)), |
|
123 | 125 | { :controller => 'messages', :action => 'show', |
|
124 | 126 | :board_id => message.board_id, |
|
125 | 127 | :id => (message.parent_id || message.id), |
|
126 | 128 | :r => (message.parent_id && message.id), |
|
127 | 129 | :anchor => (message.parent_id ? "message-#{message.id}" : nil) |
|
128 | 130 | }.merge(options), |
|
129 | 131 | html_options |
|
130 | 132 | ) |
|
131 | 133 | end |
|
132 | 134 | |
|
133 | 135 | # Generates a link to a project if active |
|
134 | 136 | # Examples: |
|
135 | 137 | # |
|
136 | 138 | # link_to_project(project) # => link to the specified project overview |
|
137 | 139 | # link_to_project(project, :action=>'settings') # => link to project settings |
|
138 | 140 | # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options |
|
139 | 141 | # link_to_project(project, {}, :class => "project") # => html options with default url (project overview) |
|
140 | 142 | # |
|
141 | 143 | def link_to_project(project, options={}, html_options = nil) |
|
142 | 144 | if project.archived? |
|
143 | 145 | h(project) |
|
144 | 146 | else |
|
145 | 147 | url = {:controller => 'projects', :action => 'show', :id => project}.merge(options) |
|
146 | 148 | link_to(h(project), url, html_options) |
|
147 | 149 | end |
|
148 | 150 | end |
|
149 | 151 | |
|
150 | 152 | def thumbnail_tag(attachment) |
|
151 | 153 | link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)), |
|
152 | 154 | {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename}, |
|
153 | 155 | :title => attachment.filename |
|
154 | 156 | end |
|
155 | 157 | |
|
156 | 158 | def toggle_link(name, id, options={}) |
|
157 | 159 | onclick = "$('##{id}').toggle(); " |
|
158 | 160 | onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ") |
|
159 | 161 | onclick << "return false;" |
|
160 | 162 | link_to(name, "#", :onclick => onclick) |
|
161 | 163 | end |
|
162 | 164 | |
|
163 | 165 | def image_to_function(name, function, html_options = {}) |
|
164 | 166 | html_options.symbolize_keys! |
|
165 | 167 | tag(:input, html_options.merge({ |
|
166 | 168 | :type => "image", :src => image_path(name), |
|
167 | 169 | :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};" |
|
168 | 170 | })) |
|
169 | 171 | end |
|
170 | 172 | |
|
171 | 173 | def format_activity_title(text) |
|
172 | 174 | h(truncate_single_line(text, :length => 100)) |
|
173 | 175 | end |
|
174 | 176 | |
|
175 | 177 | def format_activity_day(date) |
|
176 | 178 | date == User.current.today ? l(:label_today).titleize : format_date(date) |
|
177 | 179 | end |
|
178 | 180 | |
|
179 | 181 | def format_activity_description(text) |
|
180 | 182 | h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...') |
|
181 | 183 | ).gsub(/[\r\n]+/, "<br />").html_safe |
|
182 | 184 | end |
|
183 | 185 | |
|
184 | 186 | def format_version_name(version) |
|
185 | 187 | if version.project == @project |
|
186 | 188 | h(version) |
|
187 | 189 | else |
|
188 | 190 | h("#{version.project} - #{version}") |
|
189 | 191 | end |
|
190 | 192 | end |
|
191 | 193 | |
|
192 | 194 | def due_date_distance_in_words(date) |
|
193 | 195 | if date |
|
194 | 196 | l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date)) |
|
195 | 197 | end |
|
196 | 198 | end |
|
197 | 199 | |
|
198 | 200 | # Renders a tree of projects as a nested set of unordered lists |
|
199 | 201 | # The given collection may be a subset of the whole project tree |
|
200 | 202 | # (eg. some intermediate nodes are private and can not be seen) |
|
201 | 203 | def render_project_nested_lists(projects) |
|
202 | 204 | s = '' |
|
203 | 205 | if projects.any? |
|
204 | 206 | ancestors = [] |
|
205 | 207 | original_project = @project |
|
206 | 208 | projects.sort_by(&:lft).each do |project| |
|
207 | 209 | # set the project environment to please macros. |
|
208 | 210 | @project = project |
|
209 | 211 | if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) |
|
210 | 212 | s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n" |
|
211 | 213 | else |
|
212 | 214 | ancestors.pop |
|
213 | 215 | s << "</li>" |
|
214 | 216 | while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) |
|
215 | 217 | ancestors.pop |
|
216 | 218 | s << "</ul></li>\n" |
|
217 | 219 | end |
|
218 | 220 | end |
|
219 | 221 | classes = (ancestors.empty? ? 'root' : 'child') |
|
220 | 222 | s << "<li class='#{classes}'><div class='#{classes}'>" |
|
221 | 223 | s << h(block_given? ? yield(project) : project.name) |
|
222 | 224 | s << "</div>\n" |
|
223 | 225 | ancestors << project |
|
224 | 226 | end |
|
225 | 227 | s << ("</li></ul>\n" * ancestors.size) |
|
226 | 228 | @project = original_project |
|
227 | 229 | end |
|
228 | 230 | s.html_safe |
|
229 | 231 | end |
|
230 | 232 | |
|
231 | 233 | def render_page_hierarchy(pages, node=nil, options={}) |
|
232 | 234 | content = '' |
|
233 | 235 | if pages[node] |
|
234 | 236 | content << "<ul class=\"pages-hierarchy\">\n" |
|
235 | 237 | pages[node].each do |page| |
|
236 | 238 | content << "<li>" |
|
237 | 239 | content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}, |
|
238 | 240 | :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil)) |
|
239 | 241 | content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id] |
|
240 | 242 | content << "</li>\n" |
|
241 | 243 | end |
|
242 | 244 | content << "</ul>\n" |
|
243 | 245 | end |
|
244 | 246 | content.html_safe |
|
245 | 247 | end |
|
246 | 248 | |
|
247 | 249 | # Renders flash messages |
|
248 | 250 | def render_flash_messages |
|
249 | 251 | s = '' |
|
250 | 252 | flash.each do |k,v| |
|
251 | 253 | s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}") |
|
252 | 254 | end |
|
253 | 255 | s.html_safe |
|
254 | 256 | end |
|
255 | 257 | |
|
256 | 258 | # Renders tabs and their content |
|
257 | 259 | def render_tabs(tabs) |
|
258 | 260 | if tabs.any? |
|
259 | 261 | render :partial => 'common/tabs', :locals => {:tabs => tabs} |
|
260 | 262 | else |
|
261 | 263 | content_tag 'p', l(:label_no_data), :class => "nodata" |
|
262 | 264 | end |
|
263 | 265 | end |
|
264 | 266 | |
|
265 | 267 | # Renders the project quick-jump box |
|
266 | 268 | def render_project_jump_box |
|
267 | 269 | return unless User.current.logged? |
|
268 | 270 | projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq |
|
269 | 271 | if projects.any? |
|
270 | 272 | options = |
|
271 | 273 | ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" + |
|
272 | 274 | '<option value="" disabled="disabled">---</option>').html_safe |
|
273 | 275 | |
|
274 | 276 | options << project_tree_options_for_select(projects, :selected => @project) do |p| |
|
275 | 277 | { :value => project_path(:id => p, :jump => current_menu_item) } |
|
276 | 278 | end |
|
277 | 279 | |
|
278 | 280 | select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }') |
|
279 | 281 | end |
|
280 | 282 | end |
|
281 | 283 | |
|
282 | 284 | def project_tree_options_for_select(projects, options = {}) |
|
283 | 285 | s = '' |
|
284 | 286 | project_tree(projects) do |project, level| |
|
285 | 287 | name_prefix = (level > 0 ? ' ' * 2 * level + '» ' : '').html_safe |
|
286 | 288 | tag_options = {:value => project.id} |
|
287 | 289 | if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project)) |
|
288 | 290 | tag_options[:selected] = 'selected' |
|
289 | 291 | else |
|
290 | 292 | tag_options[:selected] = nil |
|
291 | 293 | end |
|
292 | 294 | tag_options.merge!(yield(project)) if block_given? |
|
293 | 295 | s << content_tag('option', name_prefix + h(project), tag_options) |
|
294 | 296 | end |
|
295 | 297 | s.html_safe |
|
296 | 298 | end |
|
297 | 299 | |
|
298 | 300 | # Yields the given block for each project with its level in the tree |
|
299 | 301 | # |
|
300 | 302 | # Wrapper for Project#project_tree |
|
301 | 303 | def project_tree(projects, &block) |
|
302 | 304 | Project.project_tree(projects, &block) |
|
303 | 305 | end |
|
304 | 306 | |
|
305 | 307 | def principals_check_box_tags(name, principals) |
|
306 | 308 | s = '' |
|
307 | 309 | principals.sort.each do |principal| |
|
308 | 310 | s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n" |
|
309 | 311 | end |
|
310 | 312 | s.html_safe |
|
311 | 313 | end |
|
312 | 314 | |
|
313 | 315 | # Returns a string for users/groups option tags |
|
314 | 316 | def principals_options_for_select(collection, selected=nil) |
|
315 | 317 | s = '' |
|
316 | 318 | if collection.include?(User.current) |
|
317 | 319 | s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id) |
|
318 | 320 | end |
|
319 | 321 | groups = '' |
|
320 | 322 | collection.sort.each do |element| |
|
321 | 323 | selected_attribute = ' selected="selected"' if option_value_selected?(element, selected) |
|
322 | 324 | (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>) |
|
323 | 325 | end |
|
324 | 326 | unless groups.empty? |
|
325 | 327 | s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>) |
|
326 | 328 | end |
|
327 | 329 | s.html_safe |
|
328 | 330 | end |
|
329 | 331 | |
|
330 | 332 | # Truncates and returns the string as a single line |
|
331 | 333 | def truncate_single_line(string, *args) |
|
332 | 334 | truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ') |
|
333 | 335 | end |
|
334 | 336 | |
|
335 | 337 | # Truncates at line break after 250 characters or options[:length] |
|
336 | 338 | def truncate_lines(string, options={}) |
|
337 | 339 | length = options[:length] || 250 |
|
338 | 340 | if string.to_s =~ /\A(.{#{length}}.*?)$/m |
|
339 | 341 | "#{$1}..." |
|
340 | 342 | else |
|
341 | 343 | string |
|
342 | 344 | end |
|
343 | 345 | end |
|
344 | 346 | |
|
345 | 347 | def anchor(text) |
|
346 | 348 | text.to_s.gsub(' ', '_') |
|
347 | 349 | end |
|
348 | 350 | |
|
349 | 351 | def html_hours(text) |
|
350 | 352 | text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe |
|
351 | 353 | end |
|
352 | 354 | |
|
353 | 355 | def authoring(created, author, options={}) |
|
354 | 356 | l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe |
|
355 | 357 | end |
|
356 | 358 | |
|
357 | 359 | def time_tag(time) |
|
358 | 360 | text = distance_of_time_in_words(Time.now, time) |
|
359 | 361 | if @project |
|
360 | 362 | link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time)) |
|
361 | 363 | else |
|
362 | 364 | content_tag('acronym', text, :title => format_time(time)) |
|
363 | 365 | end |
|
364 | 366 | end |
|
365 | 367 | |
|
366 | 368 | def syntax_highlight_lines(name, content) |
|
367 | 369 | lines = [] |
|
368 | 370 | syntax_highlight(name, content).each_line { |line| lines << line } |
|
369 | 371 | lines |
|
370 | 372 | end |
|
371 | 373 | |
|
372 | 374 | def syntax_highlight(name, content) |
|
373 | 375 | Redmine::SyntaxHighlighting.highlight_by_filename(content, name) |
|
374 | 376 | end |
|
375 | 377 | |
|
376 | 378 | def to_path_param(path) |
|
377 | 379 | str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/") |
|
378 | 380 | str.blank? ? nil : str |
|
379 | 381 | end |
|
380 | 382 | |
|
381 | 383 | def pagination_links_full(paginator, count=nil, options={}) |
|
382 | 384 | page_param = options.delete(:page_param) || :page |
|
383 | 385 | per_page_links = options.delete(:per_page_links) |
|
384 | 386 | url_param = params.dup |
|
385 | 387 | |
|
386 | 388 | html = '' |
|
387 | 389 | if paginator.current.previous |
|
388 | 390 | # \xc2\xab(utf-8) = « |
|
389 | 391 | html << link_to_content_update( |
|
390 | 392 | "\xc2\xab " + l(:label_previous), |
|
391 | 393 | url_param.merge(page_param => paginator.current.previous)) + ' ' |
|
392 | 394 | end |
|
393 | 395 | |
|
394 | 396 | html << (pagination_links_each(paginator, options) do |n| |
|
395 | 397 | link_to_content_update(n.to_s, url_param.merge(page_param => n)) |
|
396 | 398 | end || '') |
|
397 | 399 | |
|
398 | 400 | if paginator.current.next |
|
399 | 401 | # \xc2\xbb(utf-8) = » |
|
400 | 402 | html << ' ' + link_to_content_update( |
|
401 | 403 | (l(:label_next) + " \xc2\xbb"), |
|
402 | 404 | url_param.merge(page_param => paginator.current.next)) |
|
403 | 405 | end |
|
404 | 406 | |
|
405 | 407 | unless count.nil? |
|
406 | 408 | html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})" |
|
407 | 409 | if per_page_links != false && links = per_page_links(paginator.items_per_page, count) |
|
408 | 410 | html << " | #{links}" |
|
409 | 411 | end |
|
410 | 412 | end |
|
411 | 413 | |
|
412 | 414 | html.html_safe |
|
413 | 415 | end |
|
414 | 416 | |
|
415 | 417 | def per_page_links(selected=nil, item_count=nil) |
|
416 | 418 | values = Setting.per_page_options_array |
|
417 | 419 | if item_count && values.any? |
|
418 | 420 | if item_count > values.first |
|
419 | 421 | max = values.detect {|value| value >= item_count} || item_count |
|
420 | 422 | else |
|
421 | 423 | max = item_count |
|
422 | 424 | end |
|
423 | 425 | values = values.select {|value| value <= max || value == selected} |
|
424 | 426 | end |
|
425 | 427 | if values.empty? || (values.size == 1 && values.first == selected) |
|
426 | 428 | return nil |
|
427 | 429 | end |
|
428 | 430 | links = values.collect do |n| |
|
429 | 431 | n == selected ? n : link_to_content_update(n, params.merge(:per_page => n)) |
|
430 | 432 | end |
|
431 | 433 | l(:label_display_per_page, links.join(', ')) |
|
432 | 434 | end |
|
433 | 435 | |
|
434 | 436 | def reorder_links(name, url, method = :post) |
|
435 | 437 | link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), |
|
436 | 438 | url.merge({"#{name}[move_to]" => 'highest'}), |
|
437 | 439 | :method => method, :title => l(:label_sort_highest)) + |
|
438 | 440 | link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), |
|
439 | 441 | url.merge({"#{name}[move_to]" => 'higher'}), |
|
440 | 442 | :method => method, :title => l(:label_sort_higher)) + |
|
441 | 443 | link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), |
|
442 | 444 | url.merge({"#{name}[move_to]" => 'lower'}), |
|
443 | 445 | :method => method, :title => l(:label_sort_lower)) + |
|
444 | 446 | link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), |
|
445 | 447 | url.merge({"#{name}[move_to]" => 'lowest'}), |
|
446 | 448 | :method => method, :title => l(:label_sort_lowest)) |
|
447 | 449 | end |
|
448 | 450 | |
|
449 | 451 | def breadcrumb(*args) |
|
450 | 452 | elements = args.flatten |
|
451 | 453 | elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil |
|
452 | 454 | end |
|
453 | 455 | |
|
454 | 456 | def other_formats_links(&block) |
|
455 | 457 | concat('<p class="other-formats">'.html_safe + l(:label_export_to)) |
|
456 | 458 | yield Redmine::Views::OtherFormatsBuilder.new(self) |
|
457 | 459 | concat('</p>'.html_safe) |
|
458 | 460 | end |
|
459 | 461 | |
|
460 | 462 | def page_header_title |
|
461 | 463 | if @project.nil? || @project.new_record? |
|
462 | 464 | h(Setting.app_title) |
|
463 | 465 | else |
|
464 | 466 | b = [] |
|
465 | 467 | ancestors = (@project.root? ? [] : @project.ancestors.visible.all) |
|
466 | 468 | if ancestors.any? |
|
467 | 469 | root = ancestors.shift |
|
468 | 470 | b << link_to_project(root, {:jump => current_menu_item}, :class => 'root') |
|
469 | 471 | if ancestors.size > 2 |
|
470 | 472 | b << "\xe2\x80\xa6" |
|
471 | 473 | ancestors = ancestors[-2, 2] |
|
472 | 474 | end |
|
473 | 475 | b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') } |
|
474 | 476 | end |
|
475 | 477 | b << h(@project) |
|
476 | 478 | b.join(" \xc2\xbb ").html_safe |
|
477 | 479 | end |
|
478 | 480 | end |
|
479 | 481 | |
|
480 | 482 | def html_title(*args) |
|
481 | 483 | if args.empty? |
|
482 | 484 | title = @html_title || [] |
|
483 | 485 | title << @project.name if @project |
|
484 | 486 | title << Setting.app_title unless Setting.app_title == title.last |
|
485 | 487 | title.select {|t| !t.blank? }.join(' - ') |
|
486 | 488 | else |
|
487 | 489 | @html_title ||= [] |
|
488 | 490 | @html_title += args |
|
489 | 491 | end |
|
490 | 492 | end |
|
491 | 493 | |
|
492 | 494 | # Returns the theme, controller name, and action as css classes for the |
|
493 | 495 | # HTML body. |
|
494 | 496 | def body_css_classes |
|
495 | 497 | css = [] |
|
496 | 498 | if theme = Redmine::Themes.theme(Setting.ui_theme) |
|
497 | 499 | css << 'theme-' + theme.name |
|
498 | 500 | end |
|
499 | 501 | |
|
500 | 502 | css << 'controller-' + controller_name |
|
501 | 503 | css << 'action-' + action_name |
|
502 | 504 | css.join(' ') |
|
503 | 505 | end |
|
504 | 506 | |
|
505 | 507 | def accesskey(s) |
|
506 | 508 | Redmine::AccessKeys.key_for s |
|
507 | 509 | end |
|
508 | 510 | |
|
509 | 511 | # Formats text according to system settings. |
|
510 | 512 | # 2 ways to call this method: |
|
511 | 513 | # * with a String: textilizable(text, options) |
|
512 | 514 | # * with an object and one of its attribute: textilizable(issue, :description, options) |
|
513 | 515 | def textilizable(*args) |
|
514 | 516 | options = args.last.is_a?(Hash) ? args.pop : {} |
|
515 | 517 | case args.size |
|
516 | 518 | when 1 |
|
517 | 519 | obj = options[:object] |
|
518 | 520 | text = args.shift |
|
519 | 521 | when 2 |
|
520 | 522 | obj = args.shift |
|
521 | 523 | attr = args.shift |
|
522 | 524 | text = obj.send(attr).to_s |
|
523 | 525 | else |
|
524 | 526 | raise ArgumentError, 'invalid arguments to textilizable' |
|
525 | 527 | end |
|
526 | 528 | return '' if text.blank? |
|
527 | 529 | project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil) |
|
528 | 530 | only_path = options.delete(:only_path) == false ? false : true |
|
529 | 531 | |
|
530 | 532 | text = text.dup |
|
531 | 533 | macros = catch_macros(text) |
|
532 | 534 | text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) |
|
533 | 535 | |
|
534 | 536 | @parsed_headings = [] |
|
535 | 537 | @heading_anchors = {} |
|
536 | 538 | @current_section = 0 if options[:edit_section_links] |
|
537 | 539 | |
|
538 | 540 | parse_sections(text, project, obj, attr, only_path, options) |
|
539 | 541 | text = parse_non_pre_blocks(text, obj, macros) do |text| |
|
540 | 542 | [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name| |
|
541 | 543 | send method_name, text, project, obj, attr, only_path, options |
|
542 | 544 | end |
|
543 | 545 | end |
|
544 | 546 | parse_headings(text, project, obj, attr, only_path, options) |
|
545 | 547 | |
|
546 | 548 | if @parsed_headings.any? |
|
547 | 549 | replace_toc(text, @parsed_headings) |
|
548 | 550 | end |
|
549 | 551 | |
|
550 | 552 | text.html_safe |
|
551 | 553 | end |
|
552 | 554 | |
|
553 | 555 | def parse_non_pre_blocks(text, obj, macros) |
|
554 | 556 | s = StringScanner.new(text) |
|
555 | 557 | tags = [] |
|
556 | 558 | parsed = '' |
|
557 | 559 | while !s.eos? |
|
558 | 560 | s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im) |
|
559 | 561 | text, full_tag, closing, tag = s[1], s[2], s[3], s[4] |
|
560 | 562 | if tags.empty? |
|
561 | 563 | yield text |
|
562 | 564 | inject_macros(text, obj, macros) if macros.any? |
|
563 | 565 | else |
|
564 | 566 | inject_macros(text, obj, macros, false) if macros.any? |
|
565 | 567 | end |
|
566 | 568 | parsed << text |
|
567 | 569 | if tag |
|
568 | 570 | if closing |
|
569 | 571 | if tags.last == tag.downcase |
|
570 | 572 | tags.pop |
|
571 | 573 | end |
|
572 | 574 | else |
|
573 | 575 | tags << tag.downcase |
|
574 | 576 | end |
|
575 | 577 | parsed << full_tag |
|
576 | 578 | end |
|
577 | 579 | end |
|
578 | 580 | # Close any non closing tags |
|
579 | 581 | while tag = tags.pop |
|
580 | 582 | parsed << "</#{tag}>" |
|
581 | 583 | end |
|
582 | 584 | parsed |
|
583 | 585 | end |
|
584 | 586 | |
|
585 | 587 | def parse_inline_attachments(text, project, obj, attr, only_path, options) |
|
586 | 588 | # when using an image link, try to use an attachment, if possible |
|
587 | 589 | if options[:attachments] || (obj && obj.respond_to?(:attachments)) |
|
588 | 590 | attachments = options[:attachments] || obj.attachments |
|
589 | 591 | text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m| |
|
590 | 592 | filename, ext, alt, alttext = $1.downcase, $2, $3, $4 |
|
591 | 593 | # search for the picture in attachments |
|
592 | 594 | if found = Attachment.latest_attach(attachments, filename) |
|
593 | 595 | image_url = url_for :only_path => only_path, :controller => 'attachments', |
|
594 | 596 | :action => 'download', :id => found |
|
595 | 597 | desc = found.description.to_s.gsub('"', '') |
|
596 | 598 | if !desc.blank? && alttext.blank? |
|
597 | 599 | alt = " title=\"#{desc}\" alt=\"#{desc}\"" |
|
598 | 600 | end |
|
599 | 601 | "src=\"#{image_url}\"#{alt}" |
|
600 | 602 | else |
|
601 | 603 | m |
|
602 | 604 | end |
|
603 | 605 | end |
|
604 | 606 | end |
|
605 | 607 | end |
|
606 | 608 | |
|
607 | 609 | # Wiki links |
|
608 | 610 | # |
|
609 | 611 | # Examples: |
|
610 | 612 | # [[mypage]] |
|
611 | 613 | # [[mypage|mytext]] |
|
612 | 614 | # wiki links can refer other project wikis, using project name or identifier: |
|
613 | 615 | # [[project:]] -> wiki starting page |
|
614 | 616 | # [[project:|mytext]] |
|
615 | 617 | # [[project:mypage]] |
|
616 | 618 | # [[project:mypage|mytext]] |
|
617 | 619 | def parse_wiki_links(text, project, obj, attr, only_path, options) |
|
618 | 620 | text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m| |
|
619 | 621 | link_project = project |
|
620 | 622 | esc, all, page, title = $1, $2, $3, $5 |
|
621 | 623 | if esc.nil? |
|
622 | 624 | if page =~ /^([^\:]+)\:(.*)$/ |
|
623 | 625 | link_project = Project.find_by_identifier($1) || Project.find_by_name($1) |
|
624 | 626 | page = $2 |
|
625 | 627 | title ||= $1 if page.blank? |
|
626 | 628 | end |
|
627 | 629 | |
|
628 | 630 | if link_project && link_project.wiki |
|
629 | 631 | # extract anchor |
|
630 | 632 | anchor = nil |
|
631 | 633 | if page =~ /^(.+?)\#(.+)$/ |
|
632 | 634 | page, anchor = $1, $2 |
|
633 | 635 | end |
|
634 | 636 | anchor = sanitize_anchor_name(anchor) if anchor.present? |
|
635 | 637 | # check if page exists |
|
636 | 638 | wiki_page = link_project.wiki.find_page(page) |
|
637 | 639 | url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page |
|
638 | 640 | "##{anchor}" |
|
639 | 641 | else |
|
640 | 642 | case options[:wiki_links] |
|
641 | 643 | when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '') |
|
642 | 644 | when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export |
|
643 | 645 | else |
|
644 | 646 | wiki_page_id = page.present? ? Wiki.titleize(page) : nil |
|
645 | 647 | parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil |
|
646 | 648 | url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, |
|
647 | 649 | :id => wiki_page_id, :anchor => anchor, :parent => parent) |
|
648 | 650 | end |
|
649 | 651 | end |
|
650 | 652 | link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new'))) |
|
651 | 653 | else |
|
652 | 654 | # project or wiki doesn't exist |
|
653 | 655 | all |
|
654 | 656 | end |
|
655 | 657 | else |
|
656 | 658 | all |
|
657 | 659 | end |
|
658 | 660 | end |
|
659 | 661 | end |
|
660 | 662 | |
|
661 | 663 | # Redmine links |
|
662 | 664 | # |
|
663 | 665 | # Examples: |
|
664 | 666 | # Issues: |
|
665 | 667 | # #52 -> Link to issue #52 |
|
666 | 668 | # Changesets: |
|
667 | 669 | # r52 -> Link to revision 52 |
|
668 | 670 | # commit:a85130f -> Link to scmid starting with a85130f |
|
669 | 671 | # Documents: |
|
670 | 672 | # document#17 -> Link to document with id 17 |
|
671 | 673 | # document:Greetings -> Link to the document with title "Greetings" |
|
672 | 674 | # document:"Some document" -> Link to the document with title "Some document" |
|
673 | 675 | # Versions: |
|
674 | 676 | # version#3 -> Link to version with id 3 |
|
675 | 677 | # version:1.0.0 -> Link to version named "1.0.0" |
|
676 | 678 | # version:"1.0 beta 2" -> Link to version named "1.0 beta 2" |
|
677 | 679 | # Attachments: |
|
678 | 680 | # attachment:file.zip -> Link to the attachment of the current object named file.zip |
|
679 | 681 | # Source files: |
|
680 | 682 | # source:some/file -> Link to the file located at /some/file in the project's repository |
|
681 | 683 | # source:some/file@52 -> Link to the file's revision 52 |
|
682 | 684 | # source:some/file#L120 -> Link to line 120 of the file |
|
683 | 685 | # source:some/file@52#L120 -> Link to line 120 of the file's revision 52 |
|
684 | 686 | # export:some/file -> Force the download of the file |
|
685 | 687 | # Forum messages: |
|
686 | 688 | # message#1218 -> Link to message with id 1218 |
|
687 | 689 | # |
|
688 | 690 | # Links can refer other objects from other projects, using project identifier: |
|
689 | 691 | # identifier:r52 |
|
690 | 692 | # identifier:document:"Some document" |
|
691 | 693 | # identifier:version:1.0.0 |
|
692 | 694 | # identifier:source:some/file |
|
693 | 695 | def parse_redmine_links(text, project, obj, attr, only_path, options) |
|
694 | 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 | 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 | 698 | link = nil |
|
697 | 699 | if project_identifier |
|
698 | 700 | project = Project.visible.find_by_identifier(project_identifier) |
|
699 | 701 | end |
|
700 | 702 | if esc.nil? |
|
701 | 703 | if prefix.nil? && sep == 'r' |
|
702 | 704 | if project |
|
703 | 705 | repository = nil |
|
704 | 706 | if repo_identifier |
|
705 | 707 | repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} |
|
706 | 708 | else |
|
707 | 709 | repository = project.repository |
|
708 | 710 | end |
|
709 | 711 | # project.changesets.visible raises an SQL error because of a double join on repositories |
|
710 | 712 | if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier)) |
|
711 | 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 | 714 | :class => 'changeset', |
|
713 | 715 | :title => truncate_single_line(changeset.comments, :length => 100)) |
|
714 | 716 | end |
|
715 | 717 | end |
|
716 | 718 | elsif sep == '#' |
|
717 | 719 | oid = identifier.to_i |
|
718 | 720 | case prefix |
|
719 | 721 | when nil |
|
720 | 722 | if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status) |
|
721 | 723 | anchor = comment_id ? "note-#{comment_id}" : nil |
|
722 | 724 | link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor}, |
|
723 | 725 | :class => issue.css_classes, |
|
724 | 726 | :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})") |
|
725 | 727 | end |
|
726 | 728 | when 'document' |
|
727 | 729 | if document = Document.visible.find_by_id(oid) |
|
728 | 730 | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, |
|
729 | 731 | :class => 'document' |
|
730 | 732 | end |
|
731 | 733 | when 'version' |
|
732 | 734 | if version = Version.visible.find_by_id(oid) |
|
733 | 735 | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, |
|
734 | 736 | :class => 'version' |
|
735 | 737 | end |
|
736 | 738 | when 'message' |
|
737 | 739 | if message = Message.visible.find_by_id(oid, :include => :parent) |
|
738 | 740 | link = link_to_message(message, {:only_path => only_path}, :class => 'message') |
|
739 | 741 | end |
|
740 | 742 | when 'forum' |
|
741 | 743 | if board = Board.visible.find_by_id(oid) |
|
742 | 744 | link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, |
|
743 | 745 | :class => 'board' |
|
744 | 746 | end |
|
745 | 747 | when 'news' |
|
746 | 748 | if news = News.visible.find_by_id(oid) |
|
747 | 749 | link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, |
|
748 | 750 | :class => 'news' |
|
749 | 751 | end |
|
750 | 752 | when 'project' |
|
751 | 753 | if p = Project.visible.find_by_id(oid) |
|
752 | 754 | link = link_to_project(p, {:only_path => only_path}, :class => 'project') |
|
753 | 755 | end |
|
754 | 756 | end |
|
755 | 757 | elsif sep == ':' |
|
756 | 758 | # removes the double quotes if any |
|
757 | 759 | name = identifier.gsub(%r{^"(.*)"$}, "\\1") |
|
758 | 760 | case prefix |
|
759 | 761 | when 'document' |
|
760 | 762 | if project && document = project.documents.visible.find_by_title(name) |
|
761 | 763 | link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document}, |
|
762 | 764 | :class => 'document' |
|
763 | 765 | end |
|
764 | 766 | when 'version' |
|
765 | 767 | if project && version = project.versions.visible.find_by_name(name) |
|
766 | 768 | link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version}, |
|
767 | 769 | :class => 'version' |
|
768 | 770 | end |
|
769 | 771 | when 'forum' |
|
770 | 772 | if project && board = project.boards.visible.find_by_name(name) |
|
771 | 773 | link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project}, |
|
772 | 774 | :class => 'board' |
|
773 | 775 | end |
|
774 | 776 | when 'news' |
|
775 | 777 | if project && news = project.news.visible.find_by_title(name) |
|
776 | 778 | link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news}, |
|
777 | 779 | :class => 'news' |
|
778 | 780 | end |
|
779 | 781 | when 'commit', 'source', 'export' |
|
780 | 782 | if project |
|
781 | 783 | repository = nil |
|
782 | 784 | if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$} |
|
783 | 785 | repo_prefix, repo_identifier, name = $1, $2, $3 |
|
784 | 786 | repository = project.repositories.detect {|repo| repo.identifier == repo_identifier} |
|
785 | 787 | else |
|
786 | 788 | repository = project.repository |
|
787 | 789 | end |
|
788 | 790 | if prefix == 'commit' |
|
789 | 791 | if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"])) |
|
790 | 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 | 793 | :class => 'changeset', |
|
792 | 794 | :title => truncate_single_line(h(changeset.comments), :length => 100) |
|
793 | 795 | end |
|
794 | 796 | else |
|
795 | 797 | if repository && User.current.allowed_to?(:browse_repository, project) |
|
796 | 798 | name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$} |
|
797 | 799 | path, rev, anchor = $1, $3, $5 |
|
798 | 800 | link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => 'entry', :id => project, :repository_id => repository.identifier_param, |
|
799 | 801 | :path => to_path_param(path), |
|
800 | 802 | :rev => rev, |
|
801 | 803 | :anchor => anchor, |
|
802 | 804 | :format => (prefix == 'export' ? 'raw' : nil)}, |
|
803 | 805 | :class => (prefix == 'export' ? 'source download' : 'source') |
|
804 | 806 | end |
|
805 | 807 | end |
|
806 | 808 | repo_prefix = nil |
|
807 | 809 | end |
|
808 | 810 | when 'attachment' |
|
809 | 811 | attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil) |
|
810 | 812 | if attachments && attachment = attachments.detect {|a| a.filename == name } |
|
811 | 813 | link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment}, |
|
812 | 814 | :class => 'attachment' |
|
813 | 815 | end |
|
814 | 816 | when 'project' |
|
815 | 817 | if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}]) |
|
816 | 818 | link = link_to_project(p, {:only_path => only_path}, :class => 'project') |
|
817 | 819 | end |
|
818 | 820 | end |
|
819 | 821 | end |
|
820 | 822 | end |
|
821 | 823 | (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}")) |
|
822 | 824 | end |
|
823 | 825 | end |
|
824 | 826 | |
|
825 | 827 | HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE) |
|
826 | 828 | |
|
827 | 829 | def parse_sections(text, project, obj, attr, only_path, options) |
|
828 | 830 | return unless options[:edit_section_links] |
|
829 | 831 | text.gsub!(HEADING_RE) do |
|
830 | 832 | heading = $1 |
|
831 | 833 | @current_section += 1 |
|
832 | 834 | if @current_section > 1 |
|
833 | 835 | content_tag('div', |
|
834 | 836 | link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)), |
|
835 | 837 | :class => 'contextual', |
|
836 | 838 | :title => l(:button_edit_section)) + heading.html_safe |
|
837 | 839 | else |
|
838 | 840 | heading |
|
839 | 841 | end |
|
840 | 842 | end |
|
841 | 843 | end |
|
842 | 844 | |
|
843 | 845 | # Headings and TOC |
|
844 | 846 | # Adds ids and links to headings unless options[:headings] is set to false |
|
845 | 847 | def parse_headings(text, project, obj, attr, only_path, options) |
|
846 | 848 | return if options[:headings] == false |
|
847 | 849 | |
|
848 | 850 | text.gsub!(HEADING_RE) do |
|
849 | 851 | level, attrs, content = $2.to_i, $3, $4 |
|
850 | 852 | item = strip_tags(content).strip |
|
851 | 853 | anchor = sanitize_anchor_name(item) |
|
852 | 854 | # used for single-file wiki export |
|
853 | 855 | anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) |
|
854 | 856 | @heading_anchors[anchor] ||= 0 |
|
855 | 857 | idx = (@heading_anchors[anchor] += 1) |
|
856 | 858 | if idx > 1 |
|
857 | 859 | anchor = "#{anchor}-#{idx}" |
|
858 | 860 | end |
|
859 | 861 | @parsed_headings << [level, anchor, item] |
|
860 | 862 | "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">¶</a></h#{level}>" |
|
861 | 863 | end |
|
862 | 864 | end |
|
863 | 865 | |
|
864 | 866 | MACROS_RE = /( |
|
865 | 867 | (!)? # escaping |
|
866 | 868 | ( |
|
867 | 869 | \{\{ # opening tag |
|
868 | 870 | ([\w]+) # macro name |
|
869 | 871 | (\(([^\n\r]*?)\))? # optional arguments |
|
870 | 872 | ([\n\r].*?[\n\r])? # optional block of text |
|
871 | 873 | \}\} # closing tag |
|
872 | 874 | ) |
|
873 | 875 | )/mx unless const_defined?(:MACROS_RE) |
|
874 | 876 | |
|
875 | 877 | MACRO_SUB_RE = /( |
|
876 | 878 | \{\{ |
|
877 | 879 | macro\((\d+)\) |
|
878 | 880 | \}\} |
|
879 | 881 | )/x unless const_defined?(:MACRO_SUB_RE) |
|
880 | 882 | |
|
881 | 883 | # Extracts macros from text |
|
882 | 884 | def catch_macros(text) |
|
883 | 885 | macros = {} |
|
884 | 886 | text.gsub!(MACROS_RE) do |
|
885 | 887 | all, macro = $1, $4.downcase |
|
886 | 888 | if macro_exists?(macro) || all =~ MACRO_SUB_RE |
|
887 | 889 | index = macros.size |
|
888 | 890 | macros[index] = all |
|
889 | 891 | "{{macro(#{index})}}" |
|
890 | 892 | else |
|
891 | 893 | all |
|
892 | 894 | end |
|
893 | 895 | end |
|
894 | 896 | macros |
|
895 | 897 | end |
|
896 | 898 | |
|
897 | 899 | # Executes and replaces macros in text |
|
898 | 900 | def inject_macros(text, obj, macros, execute=true) |
|
899 | 901 | text.gsub!(MACRO_SUB_RE) do |
|
900 | 902 | all, index = $1, $2.to_i |
|
901 | 903 | orig = macros.delete(index) |
|
902 | 904 | if execute && orig && orig =~ MACROS_RE |
|
903 | 905 | esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip) |
|
904 | 906 | if esc.nil? |
|
905 | 907 | h(exec_macro(macro, obj, args, block) || all) |
|
906 | 908 | else |
|
907 | 909 | h(all) |
|
908 | 910 | end |
|
909 | 911 | elsif orig |
|
910 | 912 | h(orig) |
|
911 | 913 | else |
|
912 | 914 | h(all) |
|
913 | 915 | end |
|
914 | 916 | end |
|
915 | 917 | end |
|
916 | 918 | |
|
917 | 919 | TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE) |
|
918 | 920 | |
|
919 | 921 | # Renders the TOC with given headings |
|
920 | 922 | def replace_toc(text, headings) |
|
921 | 923 | text.gsub!(TOC_RE) do |
|
922 | 924 | # Keep only the 4 first levels |
|
923 | 925 | headings = headings.select{|level, anchor, item| level <= 4} |
|
924 | 926 | if headings.empty? |
|
925 | 927 | '' |
|
926 | 928 | else |
|
927 | 929 | div_class = 'toc' |
|
928 | 930 | div_class << ' right' if $1 == '>' |
|
929 | 931 | div_class << ' left' if $1 == '<' |
|
930 | 932 | out = "<ul class=\"#{div_class}\"><li>" |
|
931 | 933 | root = headings.map(&:first).min |
|
932 | 934 | current = root |
|
933 | 935 | started = false |
|
934 | 936 | headings.each do |level, anchor, item| |
|
935 | 937 | if level > current |
|
936 | 938 | out << '<ul><li>' * (level - current) |
|
937 | 939 | elsif level < current |
|
938 | 940 | out << "</li></ul>\n" * (current - level) + "</li><li>" |
|
939 | 941 | elsif started |
|
940 | 942 | out << '</li><li>' |
|
941 | 943 | end |
|
942 | 944 | out << "<a href=\"##{anchor}\">#{item}</a>" |
|
943 | 945 | current = level |
|
944 | 946 | started = true |
|
945 | 947 | end |
|
946 | 948 | out << '</li></ul>' * (current - root) |
|
947 | 949 | out << '</li></ul>' |
|
948 | 950 | end |
|
949 | 951 | end |
|
950 | 952 | end |
|
951 | 953 | |
|
952 | 954 | # Same as Rails' simple_format helper without using paragraphs |
|
953 | 955 | def simple_format_without_paragraph(text) |
|
954 | 956 | text.to_s. |
|
955 | 957 | gsub(/\r\n?/, "\n"). # \r\n and \r -> \n |
|
956 | 958 | gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br |
|
957 | 959 | gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br |
|
958 | 960 | html_safe |
|
959 | 961 | end |
|
960 | 962 | |
|
961 | 963 | def lang_options_for_select(blank=true) |
|
962 | 964 | (blank ? [["(auto)", ""]] : []) + |
|
963 | 965 | valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last } |
|
964 | 966 | end |
|
965 | 967 | |
|
966 | 968 | def label_tag_for(name, option_tags = nil, options = {}) |
|
967 | 969 | label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "") |
|
968 | 970 | content_tag("label", label_text) |
|
969 | 971 | end |
|
970 | 972 | |
|
971 | 973 | def labelled_form_for(*args, &proc) |
|
972 | 974 | args << {} unless args.last.is_a?(Hash) |
|
973 | 975 | options = args.last |
|
974 | 976 | if args.first.is_a?(Symbol) |
|
975 | 977 | options.merge!(:as => args.shift) |
|
976 | 978 | end |
|
977 | 979 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) |
|
978 | 980 | form_for(*args, &proc) |
|
979 | 981 | end |
|
980 | 982 | |
|
981 | 983 | def labelled_fields_for(*args, &proc) |
|
982 | 984 | args << {} unless args.last.is_a?(Hash) |
|
983 | 985 | options = args.last |
|
984 | 986 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder}) |
|
985 | 987 | fields_for(*args, &proc) |
|
986 | 988 | end |
|
987 | 989 | |
|
988 | 990 | def labelled_remote_form_for(*args, &proc) |
|
989 | 991 | ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2." |
|
990 | 992 | args << {} unless args.last.is_a?(Hash) |
|
991 | 993 | options = args.last |
|
992 | 994 | options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true}) |
|
993 | 995 | form_for(*args, &proc) |
|
994 | 996 | end |
|
995 | 997 | |
|
996 | 998 | def error_messages_for(*objects) |
|
997 | 999 | html = "" |
|
998 | 1000 | objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact |
|
999 | 1001 | errors = objects.map {|o| o.errors.full_messages}.flatten |
|
1000 | 1002 | if errors.any? |
|
1001 | 1003 | html << "<div id='errorExplanation'><ul>\n" |
|
1002 | 1004 | errors.each do |error| |
|
1003 | 1005 | html << "<li>#{h error}</li>\n" |
|
1004 | 1006 | end |
|
1005 | 1007 | html << "</ul></div>\n" |
|
1006 | 1008 | end |
|
1007 | 1009 | html.html_safe |
|
1008 | 1010 | end |
|
1009 | 1011 | |
|
1010 | 1012 | def delete_link(url, options={}) |
|
1011 | 1013 | options = { |
|
1012 | 1014 | :method => :delete, |
|
1013 | 1015 | :data => {:confirm => l(:text_are_you_sure)}, |
|
1014 | 1016 | :class => 'icon icon-del' |
|
1015 | 1017 | }.merge(options) |
|
1016 | 1018 | |
|
1017 | 1019 | link_to l(:button_delete), url, options |
|
1018 | 1020 | end |
|
1019 | 1021 | |
|
1020 | 1022 | def preview_link(url, form, target='preview', options={}) |
|
1021 | 1023 | content_tag 'a', l(:label_preview), { |
|
1022 | 1024 | :href => "#", |
|
1023 | 1025 | :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|, |
|
1024 | 1026 | :accesskey => accesskey(:preview) |
|
1025 | 1027 | }.merge(options) |
|
1026 | 1028 | end |
|
1027 | 1029 | |
|
1028 | 1030 | def link_to_function(name, function, html_options={}) |
|
1029 | 1031 | content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options)) |
|
1030 | 1032 | end |
|
1031 | 1033 | |
|
1032 | 1034 | # Helper to render JSON in views |
|
1033 | 1035 | def raw_json(arg) |
|
1034 | 1036 | arg.to_json.to_s.gsub('/', '\/').html_safe |
|
1035 | 1037 | end |
|
1036 | 1038 | |
|
1037 | 1039 | def back_url |
|
1038 | 1040 | url = params[:back_url] |
|
1039 | 1041 | if url.nil? && referer = request.env['HTTP_REFERER'] |
|
1040 | 1042 | url = CGI.unescape(referer.to_s) |
|
1041 | 1043 | end |
|
1042 | 1044 | url |
|
1043 | 1045 | end |
|
1044 | 1046 | |
|
1045 | 1047 | def back_url_hidden_field_tag |
|
1046 | 1048 | url = back_url |
|
1047 | 1049 | hidden_field_tag('back_url', url, :id => nil) unless url.blank? |
|
1048 | 1050 | end |
|
1049 | 1051 | |
|
1050 | 1052 | def check_all_links(form_name) |
|
1051 | 1053 | link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") + |
|
1052 | 1054 | " | ".html_safe + |
|
1053 | 1055 | link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)") |
|
1054 | 1056 | end |
|
1055 | 1057 | |
|
1056 | 1058 | def progress_bar(pcts, options={}) |
|
1057 | 1059 | pcts = [pcts, pcts] unless pcts.is_a?(Array) |
|
1058 | 1060 | pcts = pcts.collect(&:round) |
|
1059 | 1061 | pcts[1] = pcts[1] - pcts[0] |
|
1060 | 1062 | pcts << (100 - pcts[1] - pcts[0]) |
|
1061 | 1063 | width = options[:width] || '100px;' |
|
1062 | 1064 | legend = options[:legend] || '' |
|
1063 | 1065 | content_tag('table', |
|
1064 | 1066 | content_tag('tr', |
|
1065 | 1067 | (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) + |
|
1066 | 1068 | (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) + |
|
1067 | 1069 | (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe) |
|
1068 | 1070 | ), :class => 'progress', :style => "width: #{width};").html_safe + |
|
1069 | 1071 | content_tag('p', legend, :class => 'pourcent').html_safe |
|
1070 | 1072 | end |
|
1071 | 1073 | |
|
1072 | 1074 | def checked_image(checked=true) |
|
1073 | 1075 | if checked |
|
1074 | 1076 | image_tag 'toggle_check.png' |
|
1075 | 1077 | end |
|
1076 | 1078 | end |
|
1077 | 1079 | |
|
1078 | 1080 | def context_menu(url) |
|
1079 | 1081 | unless @context_menu_included |
|
1080 | 1082 | content_for :header_tags do |
|
1081 | 1083 | javascript_include_tag('context_menu') + |
|
1082 | 1084 | stylesheet_link_tag('context_menu') |
|
1083 | 1085 | end |
|
1084 | 1086 | if l(:direction) == 'rtl' |
|
1085 | 1087 | content_for :header_tags do |
|
1086 | 1088 | stylesheet_link_tag('context_menu_rtl') |
|
1087 | 1089 | end |
|
1088 | 1090 | end |
|
1089 | 1091 | @context_menu_included = true |
|
1090 | 1092 | end |
|
1091 | 1093 | javascript_tag "contextMenuInit('#{ url_for(url) }')" |
|
1092 | 1094 | end |
|
1093 | 1095 | |
|
1094 | 1096 | def calendar_for(field_id) |
|
1095 | 1097 | include_calendar_headers_tags |
|
1096 | 1098 | javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });") |
|
1097 | 1099 | end |
|
1098 | 1100 | |
|
1099 | 1101 | def include_calendar_headers_tags |
|
1100 | 1102 | unless @calendar_headers_tags_included |
|
1101 | 1103 | @calendar_headers_tags_included = true |
|
1102 | 1104 | content_for :header_tags do |
|
1103 | 1105 | start_of_week = Setting.start_of_week |
|
1104 | 1106 | start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank? |
|
1105 | 1107 | # Redmine uses 1..7 (monday..sunday) in settings and locales |
|
1106 | 1108 | # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0 |
|
1107 | 1109 | start_of_week = start_of_week.to_i % 7 |
|
1108 | 1110 | |
|
1109 | 1111 | tags = javascript_tag( |
|
1110 | 1112 | "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " + |
|
1111 | 1113 | "showOn: 'button', buttonImageOnly: true, buttonImage: '" + |
|
1112 | 1114 | path_to_image('/images/calendar.png') + |
|
1113 | 1115 | "', showButtonPanel: true};") |
|
1114 | 1116 | jquery_locale = l('jquery.locale', :default => current_language.to_s) |
|
1115 | 1117 | unless jquery_locale == 'en' |
|
1116 | 1118 | tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js") |
|
1117 | 1119 | end |
|
1118 | 1120 | tags |
|
1119 | 1121 | end |
|
1120 | 1122 | end |
|
1121 | 1123 | end |
|
1122 | 1124 | |
|
1123 | 1125 | # Overrides Rails' stylesheet_link_tag with themes and plugins support. |
|
1124 | 1126 | # Examples: |
|
1125 | 1127 | # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults |
|
1126 | 1128 | # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets |
|
1127 | 1129 | # |
|
1128 | 1130 | def stylesheet_link_tag(*sources) |
|
1129 | 1131 | options = sources.last.is_a?(Hash) ? sources.pop : {} |
|
1130 | 1132 | plugin = options.delete(:plugin) |
|
1131 | 1133 | sources = sources.map do |source| |
|
1132 | 1134 | if plugin |
|
1133 | 1135 | "/plugin_assets/#{plugin}/stylesheets/#{source}" |
|
1134 | 1136 | elsif current_theme && current_theme.stylesheets.include?(source) |
|
1135 | 1137 | current_theme.stylesheet_path(source) |
|
1136 | 1138 | else |
|
1137 | 1139 | source |
|
1138 | 1140 | end |
|
1139 | 1141 | end |
|
1140 | 1142 | super sources, options |
|
1141 | 1143 | end |
|
1142 | 1144 | |
|
1143 | 1145 | # Overrides Rails' image_tag with themes and plugins support. |
|
1144 | 1146 | # Examples: |
|
1145 | 1147 | # image_tag('image.png') # => picks image.png from the current theme or defaults |
|
1146 | 1148 | # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets |
|
1147 | 1149 | # |
|
1148 | 1150 | def image_tag(source, options={}) |
|
1149 | 1151 | if plugin = options.delete(:plugin) |
|
1150 | 1152 | source = "/plugin_assets/#{plugin}/images/#{source}" |
|
1151 | 1153 | elsif current_theme && current_theme.images.include?(source) |
|
1152 | 1154 | source = current_theme.image_path(source) |
|
1153 | 1155 | end |
|
1154 | 1156 | super source, options |
|
1155 | 1157 | end |
|
1156 | 1158 | |
|
1157 | 1159 | # Overrides Rails' javascript_include_tag with plugins support |
|
1158 | 1160 | # Examples: |
|
1159 | 1161 | # javascript_include_tag('scripts') # => picks scripts.js from defaults |
|
1160 | 1162 | # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets |
|
1161 | 1163 | # |
|
1162 | 1164 | def javascript_include_tag(*sources) |
|
1163 | 1165 | options = sources.last.is_a?(Hash) ? sources.pop : {} |
|
1164 | 1166 | if plugin = options.delete(:plugin) |
|
1165 | 1167 | sources = sources.map do |source| |
|
1166 | 1168 | if plugin |
|
1167 | 1169 | "/plugin_assets/#{plugin}/javascripts/#{source}" |
|
1168 | 1170 | else |
|
1169 | 1171 | source |
|
1170 | 1172 | end |
|
1171 | 1173 | end |
|
1172 | 1174 | end |
|
1173 | 1175 | super sources, options |
|
1174 | 1176 | end |
|
1175 | 1177 | |
|
1176 | 1178 | def content_for(name, content = nil, &block) |
|
1177 | 1179 | @has_content ||= {} |
|
1178 | 1180 | @has_content[name] = true |
|
1179 | 1181 | super(name, content, &block) |
|
1180 | 1182 | end |
|
1181 | 1183 | |
|
1182 | 1184 | def has_content?(name) |
|
1183 | 1185 | (@has_content && @has_content[name]) || false |
|
1184 | 1186 | end |
|
1185 | 1187 | |
|
1186 | 1188 | def sidebar_content? |
|
1187 | 1189 | has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present? |
|
1188 | 1190 | end |
|
1189 | 1191 | |
|
1190 | 1192 | def view_layouts_base_sidebar_hook_response |
|
1191 | 1193 | @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar) |
|
1192 | 1194 | end |
|
1193 | 1195 | |
|
1194 | 1196 | def email_delivery_enabled? |
|
1195 | 1197 | !!ActionMailer::Base.perform_deliveries |
|
1196 | 1198 | end |
|
1197 | 1199 | |
|
1198 | 1200 | # Returns the avatar image tag for the given +user+ if avatars are enabled |
|
1199 | 1201 | # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>') |
|
1200 | 1202 | def avatar(user, options = { }) |
|
1201 | 1203 | if Setting.gravatar_enabled? |
|
1202 | 1204 | options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default}) |
|
1203 | 1205 | email = nil |
|
1204 | 1206 | if user.respond_to?(:mail) |
|
1205 | 1207 | email = user.mail |
|
1206 | 1208 | elsif user.to_s =~ %r{<(.+?)>} |
|
1207 | 1209 | email = $1 |
|
1208 | 1210 | end |
|
1209 | 1211 | return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil |
|
1210 | 1212 | else |
|
1211 | 1213 | '' |
|
1212 | 1214 | end |
|
1213 | 1215 | end |
|
1214 | 1216 | |
|
1215 | 1217 | def sanitize_anchor_name(anchor) |
|
1216 | 1218 | if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java' |
|
1217 | 1219 | anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
|
1218 | 1220 | else |
|
1219 | 1221 | # TODO: remove when ruby1.8 is no longer supported |
|
1220 | 1222 | anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-') |
|
1221 | 1223 | end |
|
1222 | 1224 | end |
|
1223 | 1225 | |
|
1224 | 1226 | # Returns the javascript tags that are included in the html layout head |
|
1225 | 1227 | def javascript_heads |
|
1226 | 1228 | tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application') |
|
1227 | 1229 | unless User.current.pref.warn_on_leaving_unsaved == '0' |
|
1228 | 1230 | tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });") |
|
1229 | 1231 | end |
|
1230 | 1232 | tags |
|
1231 | 1233 | end |
|
1232 | 1234 | |
|
1233 | 1235 | def favicon |
|
1234 | 1236 | "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe |
|
1235 | 1237 | end |
|
1236 | 1238 | |
|
1237 | 1239 | def robot_exclusion_tag |
|
1238 | 1240 | '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe |
|
1239 | 1241 | end |
|
1240 | 1242 | |
|
1241 | 1243 | # Returns true if arg is expected in the API response |
|
1242 | 1244 | def include_in_api_response?(arg) |
|
1243 | 1245 | unless @included_in_api_response |
|
1244 | 1246 | param = params[:include] |
|
1245 | 1247 | @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',') |
|
1246 | 1248 | @included_in_api_response.collect!(&:strip) |
|
1247 | 1249 | end |
|
1248 | 1250 | @included_in_api_response.include?(arg.to_s) |
|
1249 | 1251 | end |
|
1250 | 1252 | |
|
1251 | 1253 | # Returns options or nil if nometa param or X-Redmine-Nometa header |
|
1252 | 1254 | # was set in the request |
|
1253 | 1255 | def api_meta(options) |
|
1254 | 1256 | if params[:nometa].present? || request.headers['X-Redmine-Nometa'] |
|
1255 | 1257 | # compatibility mode for activeresource clients that raise |
|
1256 | 1258 | # an error when unserializing an array with attributes |
|
1257 | 1259 | nil |
|
1258 | 1260 | else |
|
1259 | 1261 | options |
|
1260 | 1262 | end |
|
1261 | 1263 | end |
|
1262 | 1264 | |
|
1263 | 1265 | private |
|
1264 | 1266 | |
|
1265 | 1267 | def wiki_helper |
|
1266 | 1268 | helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting) |
|
1267 | 1269 | extend helper |
|
1268 | 1270 | return self |
|
1269 | 1271 | end |
|
1270 | 1272 | |
|
1271 | 1273 | def link_to_content_update(text, url_params = {}, html_options = {}) |
|
1272 | 1274 | link_to(text, url_params, html_options) |
|
1273 | 1275 | end |
|
1274 | 1276 | end |
@@ -1,134 +1,139 | |||
|
1 | 1 | # encoding: utf-8 |
|
2 | 2 | # |
|
3 | 3 | # Redmine - project management software |
|
4 | 4 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
5 | 5 | # |
|
6 | 6 | # This program is free software; you can redistribute it and/or |
|
7 | 7 | # modify it under the terms of the GNU General Public License |
|
8 | 8 | # as published by the Free Software Foundation; either version 2 |
|
9 | 9 | # of the License, or (at your option) any later version. |
|
10 | 10 | # |
|
11 | 11 | # This program is distributed in the hope that it will be useful, |
|
12 | 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 | 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 | 14 | # GNU General Public License for more details. |
|
15 | 15 | # |
|
16 | 16 | # You should have received a copy of the GNU General Public License |
|
17 | 17 | # along with this program; if not, write to the Free Software |
|
18 | 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
19 | 19 | |
|
20 | 20 | module QueriesHelper |
|
21 | 21 | def filters_options_for_select(query) |
|
22 | 22 | options = [[]] |
|
23 | 23 | options += query.available_filters.sort {|a,b| a[1][:order] <=> b[1][:order]}.map do |field, field_options| |
|
24 | 24 | [field_options[:name], field] |
|
25 | 25 | end |
|
26 | 26 | options_for_select(options) |
|
27 | 27 | end |
|
28 | 28 | |
|
29 | 29 | def column_header(column) |
|
30 | 30 | column.sortable ? sort_header_tag(column.name.to_s, :caption => column.caption, |
|
31 | 31 | :default_order => column.default_order) : |
|
32 | 32 | content_tag('th', h(column.caption)) |
|
33 | 33 | end |
|
34 | 34 | |
|
35 | 35 | def column_content(column, issue) |
|
36 | 36 | value = column.value(issue) |
|
37 | 37 | if value.is_a?(Array) |
|
38 |
value.collect {|v| column_value(column, issue, v)}.compact. |
|
|
38 | value.collect {|v| column_value(column, issue, v)}.compact.join(', ').html_safe | |
|
39 | 39 | else |
|
40 | 40 | column_value(column, issue, value) |
|
41 | 41 | end |
|
42 | 42 | end |
|
43 | 43 | |
|
44 | 44 | def column_value(column, issue, value) |
|
45 | 45 | case value.class.name |
|
46 | 46 | when 'String' |
|
47 | 47 | if column.name == :subject |
|
48 | 48 | link_to(h(value), :controller => 'issues', :action => 'show', :id => issue) |
|
49 | 49 | else |
|
50 | 50 | h(value) |
|
51 | 51 | end |
|
52 | 52 | when 'Time' |
|
53 | 53 | format_time(value) |
|
54 | 54 | when 'Date' |
|
55 | 55 | format_date(value) |
|
56 | 56 | when 'Fixnum', 'Float' |
|
57 | 57 | if column.name == :done_ratio |
|
58 | 58 | progress_bar(value, :width => '80px') |
|
59 | 59 | elsif column.name == :spent_hours |
|
60 | 60 | sprintf "%.2f", value |
|
61 | 61 | else |
|
62 | 62 | h(value.to_s) |
|
63 | 63 | end |
|
64 | 64 | when 'User' |
|
65 | 65 | link_to_user value |
|
66 | 66 | when 'Project' |
|
67 | 67 | link_to_project value |
|
68 | 68 | when 'Version' |
|
69 | 69 | link_to(h(value), :controller => 'versions', :action => 'show', :id => value) |
|
70 | 70 | when 'TrueClass' |
|
71 | 71 | l(:general_text_Yes) |
|
72 | 72 | when 'FalseClass' |
|
73 | 73 | l(:general_text_No) |
|
74 | 74 | when 'Issue' |
|
75 | 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 | 81 | else |
|
77 | 82 | h(value) |
|
78 | 83 | end |
|
79 | 84 | end |
|
80 | 85 | |
|
81 | 86 | # Retrieve query from session or build a new query |
|
82 | 87 | def retrieve_query |
|
83 | 88 | if !params[:query_id].blank? |
|
84 | 89 | cond = "project_id IS NULL" |
|
85 | 90 | cond << " OR project_id = #{@project.id}" if @project |
|
86 | 91 | @query = Query.find(params[:query_id], :conditions => cond) |
|
87 | 92 | raise ::Unauthorized unless @query.visible? |
|
88 | 93 | @query.project = @project |
|
89 | 94 | session[:query] = {:id => @query.id, :project_id => @query.project_id} |
|
90 | 95 | sort_clear |
|
91 | 96 | elsif api_request? || params[:set_filter] || session[:query].nil? || session[:query][:project_id] != (@project ? @project.id : nil) |
|
92 | 97 | # Give it a name, required to be valid |
|
93 | 98 | @query = Query.new(:name => "_") |
|
94 | 99 | @query.project = @project |
|
95 | 100 | build_query_from_params |
|
96 | 101 | session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names} |
|
97 | 102 | else |
|
98 | 103 | # retrieve from session |
|
99 | 104 | @query = Query.find_by_id(session[:query][:id]) if session[:query][:id] |
|
100 | 105 | @query ||= Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names]) |
|
101 | 106 | @query.project = @project |
|
102 | 107 | end |
|
103 | 108 | end |
|
104 | 109 | |
|
105 | 110 | def retrieve_query_from_session |
|
106 | 111 | if session[:query] |
|
107 | 112 | if session[:query][:id] |
|
108 | 113 | @query = Query.find_by_id(session[:query][:id]) |
|
109 | 114 | return unless @query |
|
110 | 115 | else |
|
111 | 116 | @query = Query.new(:name => "_", :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names]) |
|
112 | 117 | end |
|
113 | 118 | if session[:query].has_key?(:project_id) |
|
114 | 119 | @query.project_id = session[:query][:project_id] |
|
115 | 120 | else |
|
116 | 121 | @query.project = @project |
|
117 | 122 | end |
|
118 | 123 | @query |
|
119 | 124 | end |
|
120 | 125 | end |
|
121 | 126 | |
|
122 | 127 | def build_query_from_params |
|
123 | 128 | if params[:fields] || params[:f] |
|
124 | 129 | @query.filters = {} |
|
125 | 130 | @query.add_filters(params[:fields] || params[:f], params[:operators] || params[:op], params[:values] || params[:v]) |
|
126 | 131 | else |
|
127 | 132 | @query.available_filters.keys.each do |field| |
|
128 | 133 | @query.add_short_filter(field, params[field]) if params[field] |
|
129 | 134 | end |
|
130 | 135 | end |
|
131 | 136 | @query.group_by = params[:group_by] || (params[:query] && params[:query][:group_by]) |
|
132 | 137 | @query.column_names = params[:c] || (params[:query] && params[:query][:column_names]) |
|
133 | 138 | end |
|
134 | 139 | end |
@@ -1,1292 +1,1311 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class Issue < ActiveRecord::Base |
|
19 | 19 | include Redmine::SafeAttributes |
|
20 | 20 | |
|
21 | 21 | belongs_to :project |
|
22 | 22 | belongs_to :tracker |
|
23 | 23 | belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id' |
|
24 | 24 | belongs_to :author, :class_name => 'User', :foreign_key => 'author_id' |
|
25 | 25 | belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id' |
|
26 | 26 | belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id' |
|
27 | 27 | belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id' |
|
28 | 28 | belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id' |
|
29 | 29 | |
|
30 | 30 | has_many :journals, :as => :journalized, :dependent => :destroy |
|
31 | 31 | has_many :time_entries, :dependent => :delete_all |
|
32 | 32 | has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC" |
|
33 | 33 | |
|
34 | 34 | has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all |
|
35 | 35 | has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all |
|
36 | 36 | |
|
37 | 37 | acts_as_nested_set :scope => 'root_id', :dependent => :destroy |
|
38 | 38 | acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed |
|
39 | 39 | acts_as_customizable |
|
40 | 40 | acts_as_watchable |
|
41 | 41 | acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"], |
|
42 | 42 | :include => [:project, :journals], |
|
43 | 43 | # sort by id so that limited eager loading doesn't break with postgresql |
|
44 | 44 | :order_column => "#{table_name}.id" |
|
45 | 45 | acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"}, |
|
46 | 46 | :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}}, |
|
47 | 47 | :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') } |
|
48 | 48 | |
|
49 | 49 | acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}, |
|
50 | 50 | :author_key => :author_id |
|
51 | 51 | |
|
52 | 52 | DONE_RATIO_OPTIONS = %w(issue_field issue_status) |
|
53 | 53 | |
|
54 | 54 | attr_reader :current_journal |
|
55 | 55 | |
|
56 | 56 | validates_presence_of :subject, :priority, :project, :tracker, :author, :status |
|
57 | 57 | |
|
58 | 58 | validates_length_of :subject, :maximum => 255 |
|
59 | 59 | validates_inclusion_of :done_ratio, :in => 0..100 |
|
60 | 60 | validates_numericality_of :estimated_hours, :allow_nil => true |
|
61 | 61 | validate :validate_issue, :validate_required_fields |
|
62 | 62 | |
|
63 | 63 | scope :visible, |
|
64 | 64 | lambda {|*args| { :include => :project, |
|
65 | 65 | :conditions => Issue.visible_condition(args.shift || User.current, *args) } } |
|
66 | 66 | |
|
67 | 67 | scope :open, lambda {|*args| |
|
68 | 68 | is_closed = args.size > 0 ? !args.first : false |
|
69 | 69 | {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status} |
|
70 | 70 | } |
|
71 | 71 | |
|
72 | 72 | scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC" |
|
73 | 73 | scope :on_active_project, :include => [:status, :project, :tracker], |
|
74 | 74 | :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"] |
|
75 | 75 | |
|
76 | 76 | before_create :default_assign |
|
77 | 77 | before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change |
|
78 | 78 | after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?} |
|
79 | 79 | after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal |
|
80 | 80 | # Should be after_create but would be called before previous after_save callbacks |
|
81 | 81 | after_save :after_create_from_copy |
|
82 | 82 | after_destroy :update_parent_attributes |
|
83 | 83 | |
|
84 | 84 | # Returns a SQL conditions string used to find all issues visible by the specified user |
|
85 | 85 | def self.visible_condition(user, options={}) |
|
86 | 86 | Project.allowed_to_condition(user, :view_issues, options) do |role, user| |
|
87 | 87 | if user.logged? |
|
88 | 88 | case role.issues_visibility |
|
89 | 89 | when 'all' |
|
90 | 90 | nil |
|
91 | 91 | when 'default' |
|
92 | 92 | user_ids = [user.id] + user.groups.map(&:id) |
|
93 | 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 | 94 | when 'own' |
|
95 | 95 | user_ids = [user.id] + user.groups.map(&:id) |
|
96 | 96 | "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))" |
|
97 | 97 | else |
|
98 | 98 | '1=0' |
|
99 | 99 | end |
|
100 | 100 | else |
|
101 | 101 | "(#{table_name}.is_private = #{connection.quoted_false})" |
|
102 | 102 | end |
|
103 | 103 | end |
|
104 | 104 | end |
|
105 | 105 | |
|
106 | 106 | # Returns true if usr or current user is allowed to view the issue |
|
107 | 107 | def visible?(usr=nil) |
|
108 | 108 | (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user| |
|
109 | 109 | if user.logged? |
|
110 | 110 | case role.issues_visibility |
|
111 | 111 | when 'all' |
|
112 | 112 | true |
|
113 | 113 | when 'default' |
|
114 | 114 | !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to)) |
|
115 | 115 | when 'own' |
|
116 | 116 | self.author == user || user.is_or_belongs_to?(assigned_to) |
|
117 | 117 | else |
|
118 | 118 | false |
|
119 | 119 | end |
|
120 | 120 | else |
|
121 | 121 | !self.is_private? |
|
122 | 122 | end |
|
123 | 123 | end |
|
124 | 124 | end |
|
125 | 125 | |
|
126 | 126 | def initialize(attributes=nil, *args) |
|
127 | 127 | super |
|
128 | 128 | if new_record? |
|
129 | 129 | # set default values for new records only |
|
130 | 130 | self.status ||= IssueStatus.default |
|
131 | 131 | self.priority ||= IssuePriority.default |
|
132 | 132 | self.watcher_user_ids = [] |
|
133 | 133 | end |
|
134 | 134 | end |
|
135 | 135 | |
|
136 | 136 | # AR#Persistence#destroy would raise and RecordNotFound exception |
|
137 | 137 | # if the issue was already deleted or updated (non matching lock_version). |
|
138 | 138 | # This is a problem when bulk deleting issues or deleting a project |
|
139 | 139 | # (because an issue may already be deleted if its parent was deleted |
|
140 | 140 | # first). |
|
141 | 141 | # The issue is reloaded by the nested_set before being deleted so |
|
142 | 142 | # the lock_version condition should not be an issue but we handle it. |
|
143 | 143 | def destroy |
|
144 | 144 | super |
|
145 | 145 | rescue ActiveRecord::RecordNotFound |
|
146 | 146 | # Stale or already deleted |
|
147 | 147 | begin |
|
148 | 148 | reload |
|
149 | 149 | rescue ActiveRecord::RecordNotFound |
|
150 | 150 | # The issue was actually already deleted |
|
151 | 151 | @destroyed = true |
|
152 | 152 | return freeze |
|
153 | 153 | end |
|
154 | 154 | # The issue was stale, retry to destroy |
|
155 | 155 | super |
|
156 | 156 | end |
|
157 | 157 | |
|
158 | 158 | def reload(*args) |
|
159 | 159 | @workflow_rule_by_attribute = nil |
|
160 | 160 | @assignable_versions = nil |
|
161 | 161 | super |
|
162 | 162 | end |
|
163 | 163 | |
|
164 | 164 | # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields |
|
165 | 165 | def available_custom_fields |
|
166 | 166 | (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : [] |
|
167 | 167 | end |
|
168 | 168 | |
|
169 | 169 | # Copies attributes from another issue, arg can be an id or an Issue |
|
170 | 170 | def copy_from(arg, options={}) |
|
171 | 171 | issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg) |
|
172 | 172 | self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on") |
|
173 | 173 | self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h} |
|
174 | 174 | self.status = issue.status |
|
175 | 175 | self.author = User.current |
|
176 | 176 | unless options[:attachments] == false |
|
177 | 177 | self.attachments = issue.attachments.map do |attachement| |
|
178 | 178 | attachement.copy(:container => self) |
|
179 | 179 | end |
|
180 | 180 | end |
|
181 | 181 | @copied_from = issue |
|
182 | 182 | @copy_options = options |
|
183 | 183 | self |
|
184 | 184 | end |
|
185 | 185 | |
|
186 | 186 | # Returns an unsaved copy of the issue |
|
187 | 187 | def copy(attributes=nil, copy_options={}) |
|
188 | 188 | copy = self.class.new.copy_from(self, copy_options) |
|
189 | 189 | copy.attributes = attributes if attributes |
|
190 | 190 | copy |
|
191 | 191 | end |
|
192 | 192 | |
|
193 | 193 | # Returns true if the issue is a copy |
|
194 | 194 | def copy? |
|
195 | 195 | @copied_from.present? |
|
196 | 196 | end |
|
197 | 197 | |
|
198 | 198 | # Moves/copies an issue to a new project and tracker |
|
199 | 199 | # Returns the moved/copied issue on success, false on failure |
|
200 | 200 | def move_to_project(new_project, new_tracker=nil, options={}) |
|
201 | 201 | ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead." |
|
202 | 202 | |
|
203 | 203 | if options[:copy] |
|
204 | 204 | issue = self.copy |
|
205 | 205 | else |
|
206 | 206 | issue = self |
|
207 | 207 | end |
|
208 | 208 | |
|
209 | 209 | issue.init_journal(User.current, options[:notes]) |
|
210 | 210 | |
|
211 | 211 | # Preserve previous behaviour |
|
212 | 212 | # #move_to_project doesn't change tracker automatically |
|
213 | 213 | issue.send :project=, new_project, true |
|
214 | 214 | if new_tracker |
|
215 | 215 | issue.tracker = new_tracker |
|
216 | 216 | end |
|
217 | 217 | # Allow bulk setting of attributes on the issue |
|
218 | 218 | if options[:attributes] |
|
219 | 219 | issue.attributes = options[:attributes] |
|
220 | 220 | end |
|
221 | 221 | |
|
222 | 222 | issue.save ? issue : false |
|
223 | 223 | end |
|
224 | 224 | |
|
225 | 225 | def status_id=(sid) |
|
226 | 226 | self.status = nil |
|
227 | 227 | result = write_attribute(:status_id, sid) |
|
228 | 228 | @workflow_rule_by_attribute = nil |
|
229 | 229 | result |
|
230 | 230 | end |
|
231 | 231 | |
|
232 | 232 | def priority_id=(pid) |
|
233 | 233 | self.priority = nil |
|
234 | 234 | write_attribute(:priority_id, pid) |
|
235 | 235 | end |
|
236 | 236 | |
|
237 | 237 | def category_id=(cid) |
|
238 | 238 | self.category = nil |
|
239 | 239 | write_attribute(:category_id, cid) |
|
240 | 240 | end |
|
241 | 241 | |
|
242 | 242 | def fixed_version_id=(vid) |
|
243 | 243 | self.fixed_version = nil |
|
244 | 244 | write_attribute(:fixed_version_id, vid) |
|
245 | 245 | end |
|
246 | 246 | |
|
247 | 247 | def tracker_id=(tid) |
|
248 | 248 | self.tracker = nil |
|
249 | 249 | result = write_attribute(:tracker_id, tid) |
|
250 | 250 | @custom_field_values = nil |
|
251 | 251 | @workflow_rule_by_attribute = nil |
|
252 | 252 | result |
|
253 | 253 | end |
|
254 | 254 | |
|
255 | 255 | def project_id=(project_id) |
|
256 | 256 | if project_id.to_s != self.project_id.to_s |
|
257 | 257 | self.project = (project_id.present? ? Project.find_by_id(project_id) : nil) |
|
258 | 258 | end |
|
259 | 259 | end |
|
260 | 260 | |
|
261 | 261 | def project=(project, keep_tracker=false) |
|
262 | 262 | project_was = self.project |
|
263 | 263 | write_attribute(:project_id, project ? project.id : nil) |
|
264 | 264 | association_instance_set('project', project) |
|
265 | 265 | if project_was && project && project_was != project |
|
266 | 266 | @assignable_versions = nil |
|
267 | 267 | |
|
268 | 268 | unless keep_tracker || project.trackers.include?(tracker) |
|
269 | 269 | self.tracker = project.trackers.first |
|
270 | 270 | end |
|
271 | 271 | # Reassign to the category with same name if any |
|
272 | 272 | if category |
|
273 | 273 | self.category = project.issue_categories.find_by_name(category.name) |
|
274 | 274 | end |
|
275 | 275 | # Keep the fixed_version if it's still valid in the new_project |
|
276 | 276 | if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version) |
|
277 | 277 | self.fixed_version = nil |
|
278 | 278 | end |
|
279 | 279 | if parent && parent.project_id != project_id |
|
280 | 280 | self.parent_issue_id = nil |
|
281 | 281 | end |
|
282 | 282 | @custom_field_values = nil |
|
283 | 283 | end |
|
284 | 284 | end |
|
285 | 285 | |
|
286 | 286 | def description=(arg) |
|
287 | 287 | if arg.is_a?(String) |
|
288 | 288 | arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n") |
|
289 | 289 | end |
|
290 | 290 | write_attribute(:description, arg) |
|
291 | 291 | end |
|
292 | 292 | |
|
293 | 293 | # Overrides assign_attributes so that project and tracker get assigned first |
|
294 | 294 | def assign_attributes_with_project_and_tracker_first(new_attributes, *args) |
|
295 | 295 | return if new_attributes.nil? |
|
296 | 296 | attrs = new_attributes.dup |
|
297 | 297 | attrs.stringify_keys! |
|
298 | 298 | |
|
299 | 299 | %w(project project_id tracker tracker_id).each do |attr| |
|
300 | 300 | if attrs.has_key?(attr) |
|
301 | 301 | send "#{attr}=", attrs.delete(attr) |
|
302 | 302 | end |
|
303 | 303 | end |
|
304 | 304 | send :assign_attributes_without_project_and_tracker_first, attrs, *args |
|
305 | 305 | end |
|
306 | 306 | # Do not redefine alias chain on reload (see #4838) |
|
307 | 307 | alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first) |
|
308 | 308 | |
|
309 | 309 | def estimated_hours=(h) |
|
310 | 310 | write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) |
|
311 | 311 | end |
|
312 | 312 | |
|
313 | 313 | safe_attributes 'project_id', |
|
314 | 314 | :if => lambda {|issue, user| |
|
315 | 315 | if issue.new_record? |
|
316 | 316 | issue.copy? |
|
317 | 317 | elsif user.allowed_to?(:move_issues, issue.project) |
|
318 | 318 | projects = Issue.allowed_target_projects_on_move(user) |
|
319 | 319 | projects.include?(issue.project) && projects.size > 1 |
|
320 | 320 | end |
|
321 | 321 | } |
|
322 | 322 | |
|
323 | 323 | safe_attributes 'tracker_id', |
|
324 | 324 | 'status_id', |
|
325 | 325 | 'category_id', |
|
326 | 326 | 'assigned_to_id', |
|
327 | 327 | 'priority_id', |
|
328 | 328 | 'fixed_version_id', |
|
329 | 329 | 'subject', |
|
330 | 330 | 'description', |
|
331 | 331 | 'start_date', |
|
332 | 332 | 'due_date', |
|
333 | 333 | 'done_ratio', |
|
334 | 334 | 'estimated_hours', |
|
335 | 335 | 'custom_field_values', |
|
336 | 336 | 'custom_fields', |
|
337 | 337 | 'lock_version', |
|
338 | 338 | :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) } |
|
339 | 339 | |
|
340 | 340 | safe_attributes 'status_id', |
|
341 | 341 | 'assigned_to_id', |
|
342 | 342 | 'fixed_version_id', |
|
343 | 343 | 'done_ratio', |
|
344 | 344 | 'lock_version', |
|
345 | 345 | :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? } |
|
346 | 346 | |
|
347 | 347 | safe_attributes 'watcher_user_ids', |
|
348 | 348 | :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)} |
|
349 | 349 | |
|
350 | 350 | safe_attributes 'is_private', |
|
351 | 351 | :if => lambda {|issue, user| |
|
352 | 352 | user.allowed_to?(:set_issues_private, issue.project) || |
|
353 | 353 | (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project)) |
|
354 | 354 | } |
|
355 | 355 | |
|
356 | 356 | safe_attributes 'parent_issue_id', |
|
357 | 357 | :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) && |
|
358 | 358 | user.allowed_to?(:manage_subtasks, issue.project)} |
|
359 | 359 | |
|
360 | 360 | def safe_attribute_names(user=nil) |
|
361 | 361 | names = super |
|
362 | 362 | names -= disabled_core_fields |
|
363 | 363 | names -= read_only_attribute_names(user) |
|
364 | 364 | names |
|
365 | 365 | end |
|
366 | 366 | |
|
367 | 367 | # Safely sets attributes |
|
368 | 368 | # Should be called from controllers instead of #attributes= |
|
369 | 369 | # attr_accessible is too rough because we still want things like |
|
370 | 370 | # Issue.new(:project => foo) to work |
|
371 | 371 | def safe_attributes=(attrs, user=User.current) |
|
372 | 372 | return unless attrs.is_a?(Hash) |
|
373 | 373 | |
|
374 | 374 | attrs = attrs.dup |
|
375 | 375 | |
|
376 | 376 | # Project and Tracker must be set before since new_statuses_allowed_to depends on it. |
|
377 | 377 | if (p = attrs.delete('project_id')) && safe_attribute?('project_id') |
|
378 | 378 | if allowed_target_projects(user).collect(&:id).include?(p.to_i) |
|
379 | 379 | self.project_id = p |
|
380 | 380 | end |
|
381 | 381 | end |
|
382 | 382 | |
|
383 | 383 | if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id') |
|
384 | 384 | self.tracker_id = t |
|
385 | 385 | end |
|
386 | 386 | |
|
387 | 387 | if (s = attrs.delete('status_id')) && safe_attribute?('status_id') |
|
388 | 388 | if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i) |
|
389 | 389 | self.status_id = s |
|
390 | 390 | end |
|
391 | 391 | end |
|
392 | 392 | |
|
393 | 393 | attrs = delete_unsafe_attributes(attrs, user) |
|
394 | 394 | return if attrs.empty? |
|
395 | 395 | |
|
396 | 396 | unless leaf? |
|
397 | 397 | attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)} |
|
398 | 398 | end |
|
399 | 399 | |
|
400 | 400 | if attrs['parent_issue_id'].present? |
|
401 | 401 | attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i) |
|
402 | 402 | end |
|
403 | 403 | |
|
404 | 404 | if attrs['custom_field_values'].present? |
|
405 | 405 | attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s} |
|
406 | 406 | end |
|
407 | 407 | |
|
408 | 408 | if attrs['custom_fields'].present? |
|
409 | 409 | attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s} |
|
410 | 410 | end |
|
411 | 411 | |
|
412 | 412 | # mass-assignment security bypass |
|
413 | 413 | assign_attributes attrs, :without_protection => true |
|
414 | 414 | end |
|
415 | 415 | |
|
416 | 416 | def disabled_core_fields |
|
417 | 417 | tracker ? tracker.disabled_core_fields : [] |
|
418 | 418 | end |
|
419 | 419 | |
|
420 | 420 | # Returns the custom_field_values that can be edited by the given user |
|
421 | 421 | def editable_custom_field_values(user=nil) |
|
422 | 422 | custom_field_values.reject do |value| |
|
423 | 423 | read_only_attribute_names(user).include?(value.custom_field_id.to_s) |
|
424 | 424 | end |
|
425 | 425 | end |
|
426 | 426 | |
|
427 | 427 | # Returns the names of attributes that are read-only for user or the current user |
|
428 | 428 | # For users with multiple roles, the read-only fields are the intersection of |
|
429 | 429 | # read-only fields of each role |
|
430 | 430 | # The result is an array of strings where sustom fields are represented with their ids |
|
431 | 431 | # |
|
432 | 432 | # Examples: |
|
433 | 433 | # issue.read_only_attribute_names # => ['due_date', '2'] |
|
434 | 434 | # issue.read_only_attribute_names(user) # => [] |
|
435 | 435 | def read_only_attribute_names(user=nil) |
|
436 | 436 | workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys |
|
437 | 437 | end |
|
438 | 438 | |
|
439 | 439 | # Returns the names of required attributes for user or the current user |
|
440 | 440 | # For users with multiple roles, the required fields are the intersection of |
|
441 | 441 | # required fields of each role |
|
442 | 442 | # The result is an array of strings where sustom fields are represented with their ids |
|
443 | 443 | # |
|
444 | 444 | # Examples: |
|
445 | 445 | # issue.required_attribute_names # => ['due_date', '2'] |
|
446 | 446 | # issue.required_attribute_names(user) # => [] |
|
447 | 447 | def required_attribute_names(user=nil) |
|
448 | 448 | workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys |
|
449 | 449 | end |
|
450 | 450 | |
|
451 | 451 | # Returns true if the attribute is required for user |
|
452 | 452 | def required_attribute?(name, user=nil) |
|
453 | 453 | required_attribute_names(user).include?(name.to_s) |
|
454 | 454 | end |
|
455 | 455 | |
|
456 | 456 | # Returns a hash of the workflow rule by attribute for the given user |
|
457 | 457 | # |
|
458 | 458 | # Examples: |
|
459 | 459 | # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'} |
|
460 | 460 | def workflow_rule_by_attribute(user=nil) |
|
461 | 461 | return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil? |
|
462 | 462 | |
|
463 | 463 | user_real = user || User.current |
|
464 | 464 | roles = user_real.admin ? Role.all : user_real.roles_for_project(project) |
|
465 | 465 | return {} if roles.empty? |
|
466 | 466 | |
|
467 | 467 | result = {} |
|
468 | 468 | workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all |
|
469 | 469 | if workflow_permissions.any? |
|
470 | 470 | workflow_rules = workflow_permissions.inject({}) do |h, wp| |
|
471 | 471 | h[wp.field_name] ||= [] |
|
472 | 472 | h[wp.field_name] << wp.rule |
|
473 | 473 | h |
|
474 | 474 | end |
|
475 | 475 | workflow_rules.each do |attr, rules| |
|
476 | 476 | next if rules.size < roles.size |
|
477 | 477 | uniq_rules = rules.uniq |
|
478 | 478 | if uniq_rules.size == 1 |
|
479 | 479 | result[attr] = uniq_rules.first |
|
480 | 480 | else |
|
481 | 481 | result[attr] = 'required' |
|
482 | 482 | end |
|
483 | 483 | end |
|
484 | 484 | end |
|
485 | 485 | @workflow_rule_by_attribute = result if user.nil? |
|
486 | 486 | result |
|
487 | 487 | end |
|
488 | 488 | private :workflow_rule_by_attribute |
|
489 | 489 | |
|
490 | 490 | def done_ratio |
|
491 | 491 | if Issue.use_status_for_done_ratio? && status && status.default_done_ratio |
|
492 | 492 | status.default_done_ratio |
|
493 | 493 | else |
|
494 | 494 | read_attribute(:done_ratio) |
|
495 | 495 | end |
|
496 | 496 | end |
|
497 | 497 | |
|
498 | 498 | def self.use_status_for_done_ratio? |
|
499 | 499 | Setting.issue_done_ratio == 'issue_status' |
|
500 | 500 | end |
|
501 | 501 | |
|
502 | 502 | def self.use_field_for_done_ratio? |
|
503 | 503 | Setting.issue_done_ratio == 'issue_field' |
|
504 | 504 | end |
|
505 | 505 | |
|
506 | 506 | def validate_issue |
|
507 | 507 | if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? |
|
508 | 508 | errors.add :due_date, :not_a_date |
|
509 | 509 | end |
|
510 | 510 | |
|
511 | 511 | if self.due_date and self.start_date and self.due_date < self.start_date |
|
512 | 512 | errors.add :due_date, :greater_than_start_date |
|
513 | 513 | end |
|
514 | 514 | |
|
515 | 515 | if start_date && soonest_start && start_date < soonest_start |
|
516 | 516 | errors.add :start_date, :invalid |
|
517 | 517 | end |
|
518 | 518 | |
|
519 | 519 | if fixed_version |
|
520 | 520 | if !assignable_versions.include?(fixed_version) |
|
521 | 521 | errors.add :fixed_version_id, :inclusion |
|
522 | 522 | elsif reopened? && fixed_version.closed? |
|
523 | 523 | errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version) |
|
524 | 524 | end |
|
525 | 525 | end |
|
526 | 526 | |
|
527 | 527 | # Checks that the issue can not be added/moved to a disabled tracker |
|
528 | 528 | if project && (tracker_id_changed? || project_id_changed?) |
|
529 | 529 | unless project.trackers.include?(tracker) |
|
530 | 530 | errors.add :tracker_id, :inclusion |
|
531 | 531 | end |
|
532 | 532 | end |
|
533 | 533 | |
|
534 | 534 | # Checks parent issue assignment |
|
535 | 535 | if @parent_issue |
|
536 | 536 | if @parent_issue.project_id != project_id |
|
537 | 537 | errors.add :parent_issue_id, :not_same_project |
|
538 | 538 | elsif !new_record? |
|
539 | 539 | # moving an existing issue |
|
540 | 540 | if @parent_issue.root_id != root_id |
|
541 | 541 | # we can always move to another tree |
|
542 | 542 | elsif move_possible?(@parent_issue) |
|
543 | 543 | # move accepted inside tree |
|
544 | 544 | else |
|
545 | 545 | errors.add :parent_issue_id, :not_a_valid_parent |
|
546 | 546 | end |
|
547 | 547 | end |
|
548 | 548 | end |
|
549 | 549 | end |
|
550 | 550 | |
|
551 | 551 | # Validates the issue against additional workflow requirements |
|
552 | 552 | def validate_required_fields |
|
553 | 553 | user = new_record? ? author : current_journal.try(:user) |
|
554 | 554 | |
|
555 | 555 | required_attribute_names(user).each do |attribute| |
|
556 | 556 | if attribute =~ /^\d+$/ |
|
557 | 557 | attribute = attribute.to_i |
|
558 | 558 | v = custom_field_values.detect {|v| v.custom_field_id == attribute } |
|
559 | 559 | if v && v.value.blank? |
|
560 | 560 | errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank') |
|
561 | 561 | end |
|
562 | 562 | else |
|
563 | 563 | if respond_to?(attribute) && send(attribute).blank? |
|
564 | 564 | errors.add attribute, :blank |
|
565 | 565 | end |
|
566 | 566 | end |
|
567 | 567 | end |
|
568 | 568 | end |
|
569 | 569 | |
|
570 | 570 | # Set the done_ratio using the status if that setting is set. This will keep the done_ratios |
|
571 | 571 | # even if the user turns off the setting later |
|
572 | 572 | def update_done_ratio_from_issue_status |
|
573 | 573 | if Issue.use_status_for_done_ratio? && status && status.default_done_ratio |
|
574 | 574 | self.done_ratio = status.default_done_ratio |
|
575 | 575 | end |
|
576 | 576 | end |
|
577 | 577 | |
|
578 | 578 | def init_journal(user, notes = "") |
|
579 | 579 | @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes) |
|
580 | 580 | if new_record? |
|
581 | 581 | @current_journal.notify = false |
|
582 | 582 | else |
|
583 | 583 | @attributes_before_change = attributes.dup |
|
584 | 584 | @custom_values_before_change = {} |
|
585 | 585 | self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value } |
|
586 | 586 | end |
|
587 | 587 | @current_journal |
|
588 | 588 | end |
|
589 | 589 | |
|
590 | 590 | # Returns the id of the last journal or nil |
|
591 | 591 | def last_journal_id |
|
592 | 592 | if new_record? |
|
593 | 593 | nil |
|
594 | 594 | else |
|
595 | 595 | journals.maximum(:id) |
|
596 | 596 | end |
|
597 | 597 | end |
|
598 | 598 | |
|
599 | 599 | # Returns a scope for journals that have an id greater than journal_id |
|
600 | 600 | def journals_after(journal_id) |
|
601 | 601 | scope = journals.reorder("#{Journal.table_name}.id ASC") |
|
602 | 602 | if journal_id.present? |
|
603 | 603 | scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i) |
|
604 | 604 | end |
|
605 | 605 | scope |
|
606 | 606 | end |
|
607 | 607 | |
|
608 | 608 | # Return true if the issue is closed, otherwise false |
|
609 | 609 | def closed? |
|
610 | 610 | self.status.is_closed? |
|
611 | 611 | end |
|
612 | 612 | |
|
613 | 613 | # Return true if the issue is being reopened |
|
614 | 614 | def reopened? |
|
615 | 615 | if !new_record? && status_id_changed? |
|
616 | 616 | status_was = IssueStatus.find_by_id(status_id_was) |
|
617 | 617 | status_new = IssueStatus.find_by_id(status_id) |
|
618 | 618 | if status_was && status_new && status_was.is_closed? && !status_new.is_closed? |
|
619 | 619 | return true |
|
620 | 620 | end |
|
621 | 621 | end |
|
622 | 622 | false |
|
623 | 623 | end |
|
624 | 624 | |
|
625 | 625 | # Return true if the issue is being closed |
|
626 | 626 | def closing? |
|
627 | 627 | if !new_record? && status_id_changed? |
|
628 | 628 | status_was = IssueStatus.find_by_id(status_id_was) |
|
629 | 629 | status_new = IssueStatus.find_by_id(status_id) |
|
630 | 630 | if status_was && status_new && !status_was.is_closed? && status_new.is_closed? |
|
631 | 631 | return true |
|
632 | 632 | end |
|
633 | 633 | end |
|
634 | 634 | false |
|
635 | 635 | end |
|
636 | 636 | |
|
637 | 637 | # Returns true if the issue is overdue |
|
638 | 638 | def overdue? |
|
639 | 639 | !due_date.nil? && (due_date < Date.today) && !status.is_closed? |
|
640 | 640 | end |
|
641 | 641 | |
|
642 | 642 | # Is the amount of work done less than it should for the due date |
|
643 | 643 | def behind_schedule? |
|
644 | 644 | return false if start_date.nil? || due_date.nil? |
|
645 | 645 | done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor |
|
646 | 646 | return done_date <= Date.today |
|
647 | 647 | end |
|
648 | 648 | |
|
649 | 649 | # Does this issue have children? |
|
650 | 650 | def children? |
|
651 | 651 | !leaf? |
|
652 | 652 | end |
|
653 | 653 | |
|
654 | 654 | # Users the issue can be assigned to |
|
655 | 655 | def assignable_users |
|
656 | 656 | users = project.assignable_users |
|
657 | 657 | users << author if author |
|
658 | 658 | users << assigned_to if assigned_to |
|
659 | 659 | users.uniq.sort |
|
660 | 660 | end |
|
661 | 661 | |
|
662 | 662 | # Versions that the issue can be assigned to |
|
663 | 663 | def assignable_versions |
|
664 | 664 | return @assignable_versions if @assignable_versions |
|
665 | 665 | |
|
666 | 666 | versions = project.shared_versions.open.all |
|
667 | 667 | if fixed_version |
|
668 | 668 | if fixed_version_id_changed? |
|
669 | 669 | # nothing to do |
|
670 | 670 | elsif project_id_changed? |
|
671 | 671 | if project.shared_versions.include?(fixed_version) |
|
672 | 672 | versions << fixed_version |
|
673 | 673 | end |
|
674 | 674 | else |
|
675 | 675 | versions << fixed_version |
|
676 | 676 | end |
|
677 | 677 | end |
|
678 | 678 | @assignable_versions = versions.uniq.sort |
|
679 | 679 | end |
|
680 | 680 | |
|
681 | 681 | # Returns true if this issue is blocked by another issue that is still open |
|
682 | 682 | def blocked? |
|
683 | 683 | !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil? |
|
684 | 684 | end |
|
685 | 685 | |
|
686 | 686 | # Returns an array of statuses that user is able to apply |
|
687 | 687 | def new_statuses_allowed_to(user=User.current, include_default=false) |
|
688 | 688 | if new_record? && @copied_from |
|
689 | 689 | [IssueStatus.default, @copied_from.status].compact.uniq.sort |
|
690 | 690 | else |
|
691 | 691 | initial_status = nil |
|
692 | 692 | if new_record? |
|
693 | 693 | initial_status = IssueStatus.default |
|
694 | 694 | elsif status_id_was |
|
695 | 695 | initial_status = IssueStatus.find_by_id(status_id_was) |
|
696 | 696 | end |
|
697 | 697 | initial_status ||= status |
|
698 | 698 | |
|
699 | 699 | statuses = initial_status.find_new_statuses_allowed_to( |
|
700 | 700 | user.admin ? Role.all : user.roles_for_project(project), |
|
701 | 701 | tracker, |
|
702 | 702 | author == user, |
|
703 | 703 | assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id |
|
704 | 704 | ) |
|
705 | 705 | statuses << initial_status unless statuses.empty? |
|
706 | 706 | statuses << IssueStatus.default if include_default |
|
707 | 707 | statuses = statuses.compact.uniq.sort |
|
708 | 708 | blocked? ? statuses.reject {|s| s.is_closed?} : statuses |
|
709 | 709 | end |
|
710 | 710 | end |
|
711 | 711 | |
|
712 | 712 | def assigned_to_was |
|
713 | 713 | if assigned_to_id_changed? && assigned_to_id_was.present? |
|
714 | 714 | @assigned_to_was ||= User.find_by_id(assigned_to_id_was) |
|
715 | 715 | end |
|
716 | 716 | end |
|
717 | 717 | |
|
718 | 718 | # Returns the mail adresses of users that should be notified |
|
719 | 719 | def recipients |
|
720 | 720 | notified = [] |
|
721 | 721 | # Author and assignee are always notified unless they have been |
|
722 | 722 | # locked or don't want to be notified |
|
723 | 723 | notified << author if author |
|
724 | 724 | if assigned_to |
|
725 | 725 | notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to]) |
|
726 | 726 | end |
|
727 | 727 | if assigned_to_was |
|
728 | 728 | notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was]) |
|
729 | 729 | end |
|
730 | 730 | notified = notified.select {|u| u.active? && u.notify_about?(self)} |
|
731 | 731 | |
|
732 | 732 | notified += project.notified_users |
|
733 | 733 | notified.uniq! |
|
734 | 734 | # Remove users that can not view the issue |
|
735 | 735 | notified.reject! {|user| !visible?(user)} |
|
736 | 736 | notified.collect(&:mail) |
|
737 | 737 | end |
|
738 | 738 | |
|
739 | 739 | # Returns the number of hours spent on this issue |
|
740 | 740 | def spent_hours |
|
741 | 741 | @spent_hours ||= time_entries.sum(:hours) || 0 |
|
742 | 742 | end |
|
743 | 743 | |
|
744 | 744 | # Returns the total number of hours spent on this issue and its descendants |
|
745 | 745 | # |
|
746 | 746 | # Example: |
|
747 | 747 | # spent_hours => 0.0 |
|
748 | 748 | # spent_hours => 50.2 |
|
749 | 749 | def total_spent_hours |
|
750 | 750 | @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", |
|
751 | 751 | :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0 |
|
752 | 752 | end |
|
753 | 753 | |
|
754 | 754 | def relations |
|
755 | @relations ||= (relations_from + relations_to).sort | |
|
755 | @relations ||= IssueRelations.new(self, (relations_from + relations_to).sort) | |
|
756 | 756 | end |
|
757 | 757 | |
|
758 | 758 | # Preloads relations for a collection of issues |
|
759 | 759 | def self.load_relations(issues) |
|
760 | 760 | if issues.any? |
|
761 | 761 | relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}]) |
|
762 | 762 | issues.each do |issue| |
|
763 | 763 | issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id} |
|
764 | 764 | end |
|
765 | 765 | end |
|
766 | 766 | end |
|
767 | 767 | |
|
768 | 768 | # Preloads visible spent time for a collection of issues |
|
769 | 769 | def self.load_visible_spent_hours(issues, user=User.current) |
|
770 | 770 | if issues.any? |
|
771 | 771 | hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id) |
|
772 | 772 | issues.each do |issue| |
|
773 | 773 | issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0) |
|
774 | 774 | end |
|
775 | 775 | end |
|
776 | 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 | 797 | # Finds an issue relation given its id. |
|
779 | 798 | def find_relation(relation_id) |
|
780 | 799 | IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id]) |
|
781 | 800 | end |
|
782 | 801 | |
|
783 | 802 | def all_dependent_issues(except=[]) |
|
784 | 803 | except << self |
|
785 | 804 | dependencies = [] |
|
786 | 805 | relations_from.each do |relation| |
|
787 | 806 | if relation.issue_to && !except.include?(relation.issue_to) |
|
788 | 807 | dependencies << relation.issue_to |
|
789 | 808 | dependencies += relation.issue_to.all_dependent_issues(except) |
|
790 | 809 | end |
|
791 | 810 | end |
|
792 | 811 | dependencies |
|
793 | 812 | end |
|
794 | 813 | |
|
795 | 814 | # Returns an array of issues that duplicate this one |
|
796 | 815 | def duplicates |
|
797 | 816 | relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from} |
|
798 | 817 | end |
|
799 | 818 | |
|
800 | 819 | # Returns the due date or the target due date if any |
|
801 | 820 | # Used on gantt chart |
|
802 | 821 | def due_before |
|
803 | 822 | due_date || (fixed_version ? fixed_version.effective_date : nil) |
|
804 | 823 | end |
|
805 | 824 | |
|
806 | 825 | # Returns the time scheduled for this issue. |
|
807 | 826 | # |
|
808 | 827 | # Example: |
|
809 | 828 | # Start Date: 2/26/09, End Date: 3/04/09 |
|
810 | 829 | # duration => 6 |
|
811 | 830 | def duration |
|
812 | 831 | (start_date && due_date) ? due_date - start_date : 0 |
|
813 | 832 | end |
|
814 | 833 | |
|
815 | 834 | def soonest_start |
|
816 | 835 | @soonest_start ||= ( |
|
817 | 836 | relations_to.collect{|relation| relation.successor_soonest_start} + |
|
818 | 837 | ancestors.collect(&:soonest_start) |
|
819 | 838 | ).compact.max |
|
820 | 839 | end |
|
821 | 840 | |
|
822 | 841 | def reschedule_after(date) |
|
823 | 842 | return if date.nil? |
|
824 | 843 | if leaf? |
|
825 | 844 | if start_date.nil? || start_date < date |
|
826 | 845 | self.start_date, self.due_date = date, date + duration |
|
827 | 846 | begin |
|
828 | 847 | save |
|
829 | 848 | rescue ActiveRecord::StaleObjectError |
|
830 | 849 | reload |
|
831 | 850 | self.start_date, self.due_date = date, date + duration |
|
832 | 851 | save |
|
833 | 852 | end |
|
834 | 853 | end |
|
835 | 854 | else |
|
836 | 855 | leaves.each do |leaf| |
|
837 | 856 | leaf.reschedule_after(date) |
|
838 | 857 | end |
|
839 | 858 | end |
|
840 | 859 | end |
|
841 | 860 | |
|
842 | 861 | def <=>(issue) |
|
843 | 862 | if issue.nil? |
|
844 | 863 | -1 |
|
845 | 864 | elsif root_id != issue.root_id |
|
846 | 865 | (root_id || 0) <=> (issue.root_id || 0) |
|
847 | 866 | else |
|
848 | 867 | (lft || 0) <=> (issue.lft || 0) |
|
849 | 868 | end |
|
850 | 869 | end |
|
851 | 870 | |
|
852 | 871 | def to_s |
|
853 | 872 | "#{tracker} ##{id}: #{subject}" |
|
854 | 873 | end |
|
855 | 874 | |
|
856 | 875 | # Returns a string of css classes that apply to the issue |
|
857 | 876 | def css_classes |
|
858 | 877 | s = "issue status-#{status_id} priority-#{priority_id}" |
|
859 | 878 | s << ' closed' if closed? |
|
860 | 879 | s << ' overdue' if overdue? |
|
861 | 880 | s << ' child' if child? |
|
862 | 881 | s << ' parent' unless leaf? |
|
863 | 882 | s << ' private' if is_private? |
|
864 | 883 | s << ' created-by-me' if User.current.logged? && author_id == User.current.id |
|
865 | 884 | s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id |
|
866 | 885 | s |
|
867 | 886 | end |
|
868 | 887 | |
|
869 | 888 | # Saves an issue and a time_entry from the parameters |
|
870 | 889 | def save_issue_with_child_records(params, existing_time_entry=nil) |
|
871 | 890 | Issue.transaction do |
|
872 | 891 | if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project) |
|
873 | 892 | @time_entry = existing_time_entry || TimeEntry.new |
|
874 | 893 | @time_entry.project = project |
|
875 | 894 | @time_entry.issue = self |
|
876 | 895 | @time_entry.user = User.current |
|
877 | 896 | @time_entry.spent_on = User.current.today |
|
878 | 897 | @time_entry.attributes = params[:time_entry] |
|
879 | 898 | self.time_entries << @time_entry |
|
880 | 899 | end |
|
881 | 900 | |
|
882 | 901 | # TODO: Rename hook |
|
883 | 902 | Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) |
|
884 | 903 | if save |
|
885 | 904 | # TODO: Rename hook |
|
886 | 905 | Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal}) |
|
887 | 906 | else |
|
888 | 907 | raise ActiveRecord::Rollback |
|
889 | 908 | end |
|
890 | 909 | end |
|
891 | 910 | end |
|
892 | 911 | |
|
893 | 912 | # Unassigns issues from +version+ if it's no longer shared with issue's project |
|
894 | 913 | def self.update_versions_from_sharing_change(version) |
|
895 | 914 | # Update issues assigned to the version |
|
896 | 915 | update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id]) |
|
897 | 916 | end |
|
898 | 917 | |
|
899 | 918 | # Unassigns issues from versions that are no longer shared |
|
900 | 919 | # after +project+ was moved |
|
901 | 920 | def self.update_versions_from_hierarchy_change(project) |
|
902 | 921 | moved_project_ids = project.self_and_descendants.reload.collect(&:id) |
|
903 | 922 | # Update issues of the moved projects and issues assigned to a version of a moved project |
|
904 | 923 | Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids]) |
|
905 | 924 | end |
|
906 | 925 | |
|
907 | 926 | def parent_issue_id=(arg) |
|
908 | 927 | parent_issue_id = arg.blank? ? nil : arg.to_i |
|
909 | 928 | if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id) |
|
910 | 929 | @parent_issue.id |
|
911 | 930 | else |
|
912 | 931 | @parent_issue = nil |
|
913 | 932 | nil |
|
914 | 933 | end |
|
915 | 934 | end |
|
916 | 935 | |
|
917 | 936 | def parent_issue_id |
|
918 | 937 | if instance_variable_defined? :@parent_issue |
|
919 | 938 | @parent_issue.nil? ? nil : @parent_issue.id |
|
920 | 939 | else |
|
921 | 940 | parent_id |
|
922 | 941 | end |
|
923 | 942 | end |
|
924 | 943 | |
|
925 | 944 | # Extracted from the ReportsController. |
|
926 | 945 | def self.by_tracker(project) |
|
927 | 946 | count_and_group_by(:project => project, |
|
928 | 947 | :field => 'tracker_id', |
|
929 | 948 | :joins => Tracker.table_name) |
|
930 | 949 | end |
|
931 | 950 | |
|
932 | 951 | def self.by_version(project) |
|
933 | 952 | count_and_group_by(:project => project, |
|
934 | 953 | :field => 'fixed_version_id', |
|
935 | 954 | :joins => Version.table_name) |
|
936 | 955 | end |
|
937 | 956 | |
|
938 | 957 | def self.by_priority(project) |
|
939 | 958 | count_and_group_by(:project => project, |
|
940 | 959 | :field => 'priority_id', |
|
941 | 960 | :joins => IssuePriority.table_name) |
|
942 | 961 | end |
|
943 | 962 | |
|
944 | 963 | def self.by_category(project) |
|
945 | 964 | count_and_group_by(:project => project, |
|
946 | 965 | :field => 'category_id', |
|
947 | 966 | :joins => IssueCategory.table_name) |
|
948 | 967 | end |
|
949 | 968 | |
|
950 | 969 | def self.by_assigned_to(project) |
|
951 | 970 | count_and_group_by(:project => project, |
|
952 | 971 | :field => 'assigned_to_id', |
|
953 | 972 | :joins => User.table_name) |
|
954 | 973 | end |
|
955 | 974 | |
|
956 | 975 | def self.by_author(project) |
|
957 | 976 | count_and_group_by(:project => project, |
|
958 | 977 | :field => 'author_id', |
|
959 | 978 | :joins => User.table_name) |
|
960 | 979 | end |
|
961 | 980 | |
|
962 | 981 | def self.by_subproject(project) |
|
963 | 982 | ActiveRecord::Base.connection.select_all("select s.id as status_id, |
|
964 | 983 | s.is_closed as closed, |
|
965 | 984 | #{Issue.table_name}.project_id as project_id, |
|
966 | 985 | count(#{Issue.table_name}.id) as total |
|
967 | 986 | from |
|
968 | 987 | #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s |
|
969 | 988 | where |
|
970 | 989 | #{Issue.table_name}.status_id=s.id |
|
971 | 990 | and #{Issue.table_name}.project_id = #{Project.table_name}.id |
|
972 | 991 | and #{visible_condition(User.current, :project => project, :with_subprojects => true)} |
|
973 | 992 | and #{Issue.table_name}.project_id <> #{project.id} |
|
974 | 993 | group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any? |
|
975 | 994 | end |
|
976 | 995 | # End ReportsController extraction |
|
977 | 996 | |
|
978 | 997 | # Returns an array of projects that user can assign the issue to |
|
979 | 998 | def allowed_target_projects(user=User.current) |
|
980 | 999 | if new_record? |
|
981 | 1000 | Project.all(:conditions => Project.allowed_to_condition(user, :add_issues)) |
|
982 | 1001 | else |
|
983 | 1002 | self.class.allowed_target_projects_on_move(user) |
|
984 | 1003 | end |
|
985 | 1004 | end |
|
986 | 1005 | |
|
987 | 1006 | # Returns an array of projects that user can move issues to |
|
988 | 1007 | def self.allowed_target_projects_on_move(user=User.current) |
|
989 | 1008 | Project.all(:conditions => Project.allowed_to_condition(user, :move_issues)) |
|
990 | 1009 | end |
|
991 | 1010 | |
|
992 | 1011 | private |
|
993 | 1012 | |
|
994 | 1013 | def after_project_change |
|
995 | 1014 | # Update project_id on related time entries |
|
996 | 1015 | TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id}) |
|
997 | 1016 | |
|
998 | 1017 | # Delete issue relations |
|
999 | 1018 | unless Setting.cross_project_issue_relations? |
|
1000 | 1019 | relations_from.clear |
|
1001 | 1020 | relations_to.clear |
|
1002 | 1021 | end |
|
1003 | 1022 | |
|
1004 | 1023 | # Move subtasks |
|
1005 | 1024 | children.each do |child| |
|
1006 | 1025 | # Change project and keep project |
|
1007 | 1026 | child.send :project=, project, true |
|
1008 | 1027 | unless child.save |
|
1009 | 1028 | raise ActiveRecord::Rollback |
|
1010 | 1029 | end |
|
1011 | 1030 | end |
|
1012 | 1031 | end |
|
1013 | 1032 | |
|
1014 | 1033 | # Callback for after the creation of an issue by copy |
|
1015 | 1034 | # * adds a "copied to" relation with the copied issue |
|
1016 | 1035 | # * copies subtasks from the copied issue |
|
1017 | 1036 | def after_create_from_copy |
|
1018 | 1037 | return unless copy? && !@after_create_from_copy_handled |
|
1019 | 1038 | |
|
1020 | 1039 | if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false |
|
1021 | 1040 | relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO) |
|
1022 | 1041 | unless relation.save |
|
1023 | 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 | 1043 | end |
|
1025 | 1044 | end |
|
1026 | 1045 | |
|
1027 | 1046 | unless @copied_from.leaf? || @copy_options[:subtasks] == false |
|
1028 | 1047 | @copied_from.children.each do |child| |
|
1029 | 1048 | unless child.visible? |
|
1030 | 1049 | # Do not copy subtasks that are not visible to avoid potential disclosure of private data |
|
1031 | 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 | 1051 | next |
|
1033 | 1052 | end |
|
1034 | 1053 | copy = Issue.new.copy_from(child, @copy_options) |
|
1035 | 1054 | copy.author = author |
|
1036 | 1055 | copy.project = project |
|
1037 | 1056 | copy.parent_issue_id = id |
|
1038 | 1057 | # Children subtasks are copied recursively |
|
1039 | 1058 | unless copy.save |
|
1040 | 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 | 1060 | end |
|
1042 | 1061 | end |
|
1043 | 1062 | end |
|
1044 | 1063 | @after_create_from_copy_handled = true |
|
1045 | 1064 | end |
|
1046 | 1065 | |
|
1047 | 1066 | def update_nested_set_attributes |
|
1048 | 1067 | if root_id.nil? |
|
1049 | 1068 | # issue was just created |
|
1050 | 1069 | self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id) |
|
1051 | 1070 | set_default_left_and_right |
|
1052 | 1071 | Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id]) |
|
1053 | 1072 | if @parent_issue |
|
1054 | 1073 | move_to_child_of(@parent_issue) |
|
1055 | 1074 | end |
|
1056 | 1075 | reload |
|
1057 | 1076 | elsif parent_issue_id != parent_id |
|
1058 | 1077 | former_parent_id = parent_id |
|
1059 | 1078 | # moving an existing issue |
|
1060 | 1079 | if @parent_issue && @parent_issue.root_id == root_id |
|
1061 | 1080 | # inside the same tree |
|
1062 | 1081 | move_to_child_of(@parent_issue) |
|
1063 | 1082 | else |
|
1064 | 1083 | # to another tree |
|
1065 | 1084 | unless root? |
|
1066 | 1085 | move_to_right_of(root) |
|
1067 | 1086 | reload |
|
1068 | 1087 | end |
|
1069 | 1088 | old_root_id = root_id |
|
1070 | 1089 | self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id ) |
|
1071 | 1090 | target_maxright = nested_set_scope.maximum(right_column_name) || 0 |
|
1072 | 1091 | offset = target_maxright + 1 - lft |
|
1073 | 1092 | Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}", |
|
1074 | 1093 | ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]) |
|
1075 | 1094 | self[left_column_name] = lft + offset |
|
1076 | 1095 | self[right_column_name] = rgt + offset |
|
1077 | 1096 | if @parent_issue |
|
1078 | 1097 | move_to_child_of(@parent_issue) |
|
1079 | 1098 | end |
|
1080 | 1099 | end |
|
1081 | 1100 | reload |
|
1082 | 1101 | # delete invalid relations of all descendants |
|
1083 | 1102 | self_and_descendants.each do |issue| |
|
1084 | 1103 | issue.relations.each do |relation| |
|
1085 | 1104 | relation.destroy unless relation.valid? |
|
1086 | 1105 | end |
|
1087 | 1106 | end |
|
1088 | 1107 | # update former parent |
|
1089 | 1108 | recalculate_attributes_for(former_parent_id) if former_parent_id |
|
1090 | 1109 | end |
|
1091 | 1110 | remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue) |
|
1092 | 1111 | end |
|
1093 | 1112 | |
|
1094 | 1113 | def update_parent_attributes |
|
1095 | 1114 | recalculate_attributes_for(parent_id) if parent_id |
|
1096 | 1115 | end |
|
1097 | 1116 | |
|
1098 | 1117 | def recalculate_attributes_for(issue_id) |
|
1099 | 1118 | if issue_id && p = Issue.find_by_id(issue_id) |
|
1100 | 1119 | # priority = highest priority of children |
|
1101 | 1120 | if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority) |
|
1102 | 1121 | p.priority = IssuePriority.find_by_position(priority_position) |
|
1103 | 1122 | end |
|
1104 | 1123 | |
|
1105 | 1124 | # start/due dates = lowest/highest dates of children |
|
1106 | 1125 | p.start_date = p.children.minimum(:start_date) |
|
1107 | 1126 | p.due_date = p.children.maximum(:due_date) |
|
1108 | 1127 | if p.start_date && p.due_date && p.due_date < p.start_date |
|
1109 | 1128 | p.start_date, p.due_date = p.due_date, p.start_date |
|
1110 | 1129 | end |
|
1111 | 1130 | |
|
1112 | 1131 | # done ratio = weighted average ratio of leaves |
|
1113 | 1132 | unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio |
|
1114 | 1133 | leaves_count = p.leaves.count |
|
1115 | 1134 | if leaves_count > 0 |
|
1116 | 1135 | average = p.leaves.average(:estimated_hours).to_f |
|
1117 | 1136 | if average == 0 |
|
1118 | 1137 | average = 1 |
|
1119 | 1138 | end |
|
1120 | 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 | 1140 | progress = done / (average * leaves_count) |
|
1122 | 1141 | p.done_ratio = progress.round |
|
1123 | 1142 | end |
|
1124 | 1143 | end |
|
1125 | 1144 | |
|
1126 | 1145 | # estimate = sum of leaves estimates |
|
1127 | 1146 | p.estimated_hours = p.leaves.sum(:estimated_hours).to_f |
|
1128 | 1147 | p.estimated_hours = nil if p.estimated_hours == 0.0 |
|
1129 | 1148 | |
|
1130 | 1149 | # ancestors will be recursively updated |
|
1131 | 1150 | p.save(:validate => false) |
|
1132 | 1151 | end |
|
1133 | 1152 | end |
|
1134 | 1153 | |
|
1135 | 1154 | # Update issues so their versions are not pointing to a |
|
1136 | 1155 | # fixed_version that is not shared with the issue's project |
|
1137 | 1156 | def self.update_versions(conditions=nil) |
|
1138 | 1157 | # Only need to update issues with a fixed_version from |
|
1139 | 1158 | # a different project and that is not systemwide shared |
|
1140 | 1159 | Issue.scoped(:conditions => conditions).all( |
|
1141 | 1160 | :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" + |
|
1142 | 1161 | " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" + |
|
1143 | 1162 | " AND #{Version.table_name}.sharing <> 'system'", |
|
1144 | 1163 | :include => [:project, :fixed_version] |
|
1145 | 1164 | ).each do |issue| |
|
1146 | 1165 | next if issue.project.nil? || issue.fixed_version.nil? |
|
1147 | 1166 | unless issue.project.shared_versions.include?(issue.fixed_version) |
|
1148 | 1167 | issue.init_journal(User.current) |
|
1149 | 1168 | issue.fixed_version = nil |
|
1150 | 1169 | issue.save |
|
1151 | 1170 | end |
|
1152 | 1171 | end |
|
1153 | 1172 | end |
|
1154 | 1173 | |
|
1155 | 1174 | # Callback on file attachment |
|
1156 | 1175 | def attachment_added(obj) |
|
1157 | 1176 | if @current_journal && !obj.new_record? |
|
1158 | 1177 | @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename) |
|
1159 | 1178 | end |
|
1160 | 1179 | end |
|
1161 | 1180 | |
|
1162 | 1181 | # Callback on attachment deletion |
|
1163 | 1182 | def attachment_removed(obj) |
|
1164 | 1183 | if @current_journal && !obj.new_record? |
|
1165 | 1184 | @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename) |
|
1166 | 1185 | @current_journal.save |
|
1167 | 1186 | end |
|
1168 | 1187 | end |
|
1169 | 1188 | |
|
1170 | 1189 | # Default assignment based on category |
|
1171 | 1190 | def default_assign |
|
1172 | 1191 | if assigned_to.nil? && category && category.assigned_to |
|
1173 | 1192 | self.assigned_to = category.assigned_to |
|
1174 | 1193 | end |
|
1175 | 1194 | end |
|
1176 | 1195 | |
|
1177 | 1196 | # Updates start/due dates of following issues |
|
1178 | 1197 | def reschedule_following_issues |
|
1179 | 1198 | if start_date_changed? || due_date_changed? |
|
1180 | 1199 | relations_from.each do |relation| |
|
1181 | 1200 | relation.set_issue_to_dates |
|
1182 | 1201 | end |
|
1183 | 1202 | end |
|
1184 | 1203 | end |
|
1185 | 1204 | |
|
1186 | 1205 | # Closes duplicates if the issue is being closed |
|
1187 | 1206 | def close_duplicates |
|
1188 | 1207 | if closing? |
|
1189 | 1208 | duplicates.each do |duplicate| |
|
1190 | 1209 | # Reload is need in case the duplicate was updated by a previous duplicate |
|
1191 | 1210 | duplicate.reload |
|
1192 | 1211 | # Don't re-close it if it's already closed |
|
1193 | 1212 | next if duplicate.closed? |
|
1194 | 1213 | # Same user and notes |
|
1195 | 1214 | if @current_journal |
|
1196 | 1215 | duplicate.init_journal(@current_journal.user, @current_journal.notes) |
|
1197 | 1216 | end |
|
1198 | 1217 | duplicate.update_attribute :status, self.status |
|
1199 | 1218 | end |
|
1200 | 1219 | end |
|
1201 | 1220 | end |
|
1202 | 1221 | |
|
1203 | 1222 | # Make sure updated_on is updated when adding a note |
|
1204 | 1223 | def force_updated_on_change |
|
1205 | 1224 | if @current_journal |
|
1206 | 1225 | self.updated_on = current_time_from_proper_timezone |
|
1207 | 1226 | end |
|
1208 | 1227 | end |
|
1209 | 1228 | |
|
1210 | 1229 | # Saves the changes in a Journal |
|
1211 | 1230 | # Called after_save |
|
1212 | 1231 | def create_journal |
|
1213 | 1232 | if @current_journal |
|
1214 | 1233 | # attributes changes |
|
1215 | 1234 | if @attributes_before_change |
|
1216 | 1235 | (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c| |
|
1217 | 1236 | before = @attributes_before_change[c] |
|
1218 | 1237 | after = send(c) |
|
1219 | 1238 | next if before == after || (before.blank? && after.blank?) |
|
1220 | 1239 | @current_journal.details << JournalDetail.new(:property => 'attr', |
|
1221 | 1240 | :prop_key => c, |
|
1222 | 1241 | :old_value => before, |
|
1223 | 1242 | :value => after) |
|
1224 | 1243 | } |
|
1225 | 1244 | end |
|
1226 | 1245 | if @custom_values_before_change |
|
1227 | 1246 | # custom fields changes |
|
1228 | 1247 | custom_field_values.each {|c| |
|
1229 | 1248 | before = @custom_values_before_change[c.custom_field_id] |
|
1230 | 1249 | after = c.value |
|
1231 | 1250 | next if before == after || (before.blank? && after.blank?) |
|
1232 | 1251 | |
|
1233 | 1252 | if before.is_a?(Array) || after.is_a?(Array) |
|
1234 | 1253 | before = [before] unless before.is_a?(Array) |
|
1235 | 1254 | after = [after] unless after.is_a?(Array) |
|
1236 | 1255 | |
|
1237 | 1256 | # values removed |
|
1238 | 1257 | (before - after).reject(&:blank?).each do |value| |
|
1239 | 1258 | @current_journal.details << JournalDetail.new(:property => 'cf', |
|
1240 | 1259 | :prop_key => c.custom_field_id, |
|
1241 | 1260 | :old_value => value, |
|
1242 | 1261 | :value => nil) |
|
1243 | 1262 | end |
|
1244 | 1263 | # values added |
|
1245 | 1264 | (after - before).reject(&:blank?).each do |value| |
|
1246 | 1265 | @current_journal.details << JournalDetail.new(:property => 'cf', |
|
1247 | 1266 | :prop_key => c.custom_field_id, |
|
1248 | 1267 | :old_value => nil, |
|
1249 | 1268 | :value => value) |
|
1250 | 1269 | end |
|
1251 | 1270 | else |
|
1252 | 1271 | @current_journal.details << JournalDetail.new(:property => 'cf', |
|
1253 | 1272 | :prop_key => c.custom_field_id, |
|
1254 | 1273 | :old_value => before, |
|
1255 | 1274 | :value => after) |
|
1256 | 1275 | end |
|
1257 | 1276 | } |
|
1258 | 1277 | end |
|
1259 | 1278 | @current_journal.save |
|
1260 | 1279 | # reset current journal |
|
1261 | 1280 | init_journal @current_journal.user, @current_journal.notes |
|
1262 | 1281 | end |
|
1263 | 1282 | end |
|
1264 | 1283 | |
|
1265 | 1284 | # Query generator for selecting groups of issue counts for a project |
|
1266 | 1285 | # based on specific criteria |
|
1267 | 1286 | # |
|
1268 | 1287 | # Options |
|
1269 | 1288 | # * project - Project to search in. |
|
1270 | 1289 | # * field - String. Issue field to key off of in the grouping. |
|
1271 | 1290 | # * joins - String. The table name to join against. |
|
1272 | 1291 | def self.count_and_group_by(options) |
|
1273 | 1292 | project = options.delete(:project) |
|
1274 | 1293 | select_field = options.delete(:field) |
|
1275 | 1294 | joins = options.delete(:joins) |
|
1276 | 1295 | |
|
1277 | 1296 | where = "#{Issue.table_name}.#{select_field}=j.id" |
|
1278 | 1297 | |
|
1279 | 1298 | ActiveRecord::Base.connection.select_all("select s.id as status_id, |
|
1280 | 1299 | s.is_closed as closed, |
|
1281 | 1300 | j.id as #{select_field}, |
|
1282 | 1301 | count(#{Issue.table_name}.id) as total |
|
1283 | 1302 | from |
|
1284 | 1303 | #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j |
|
1285 | 1304 | where |
|
1286 | 1305 | #{Issue.table_name}.status_id=s.id |
|
1287 | 1306 | and #{where} |
|
1288 | 1307 | and #{Issue.table_name}.project_id=#{Project.table_name}.id |
|
1289 | 1308 | and #{visible_condition(User.current, :project => project)} |
|
1290 | 1309 | group by s.id, s.is_closed, j.id") |
|
1291 | 1310 | end |
|
1292 | 1311 | end |
@@ -1,147 +1,166 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 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 | 32 | class IssueRelation < ActiveRecord::Base |
|
19 | 33 | belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id' |
|
20 | 34 | belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id' |
|
21 | 35 | |
|
22 | 36 | TYPE_RELATES = "relates" |
|
23 | 37 | TYPE_DUPLICATES = "duplicates" |
|
24 | 38 | TYPE_DUPLICATED = "duplicated" |
|
25 | 39 | TYPE_BLOCKS = "blocks" |
|
26 | 40 | TYPE_BLOCKED = "blocked" |
|
27 | 41 | TYPE_PRECEDES = "precedes" |
|
28 | 42 | TYPE_FOLLOWS = "follows" |
|
29 | 43 | TYPE_COPIED_TO = "copied_to" |
|
30 | 44 | TYPE_COPIED_FROM = "copied_from" |
|
31 | 45 | |
|
32 | 46 | TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1, :sym => TYPE_RELATES }, |
|
33 | 47 | TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2, :sym => TYPE_DUPLICATED }, |
|
34 | 48 | TYPE_DUPLICATED => { :name => :label_duplicated_by, :sym_name => :label_duplicates, :order => 3, :sym => TYPE_DUPLICATES, :reverse => TYPE_DUPLICATES }, |
|
35 | 49 | TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 4, :sym => TYPE_BLOCKED }, |
|
36 | 50 | TYPE_BLOCKED => { :name => :label_blocked_by, :sym_name => :label_blocks, :order => 5, :sym => TYPE_BLOCKS, :reverse => TYPE_BLOCKS }, |
|
37 | 51 | TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 6, :sym => TYPE_FOLLOWS }, |
|
38 | 52 | TYPE_FOLLOWS => { :name => :label_follows, :sym_name => :label_precedes, :order => 7, :sym => TYPE_PRECEDES, :reverse => TYPE_PRECEDES }, |
|
39 | 53 | TYPE_COPIED_TO => { :name => :label_copied_to, :sym_name => :label_copied_from, :order => 8, :sym => TYPE_COPIED_FROM }, |
|
40 | 54 | TYPE_COPIED_FROM => { :name => :label_copied_from, :sym_name => :label_copied_to, :order => 9, :sym => TYPE_COPIED_TO, :reverse => TYPE_COPIED_TO } |
|
41 | 55 | }.freeze |
|
42 | 56 | |
|
43 | 57 | validates_presence_of :issue_from, :issue_to, :relation_type |
|
44 | 58 | validates_inclusion_of :relation_type, :in => TYPES.keys |
|
45 | 59 | validates_numericality_of :delay, :allow_nil => true |
|
46 | 60 | validates_uniqueness_of :issue_to_id, :scope => :issue_from_id |
|
47 | 61 | |
|
48 | 62 | validate :validate_issue_relation |
|
49 | 63 | |
|
50 | 64 | attr_protected :issue_from_id, :issue_to_id |
|
51 | 65 | |
|
52 | 66 | before_save :handle_issue_order |
|
53 | 67 | |
|
54 | 68 | def visible?(user=User.current) |
|
55 | 69 | (issue_from.nil? || issue_from.visible?(user)) && (issue_to.nil? || issue_to.visible?(user)) |
|
56 | 70 | end |
|
57 | 71 | |
|
58 | 72 | def deletable?(user=User.current) |
|
59 | 73 | visible?(user) && |
|
60 | 74 | ((issue_from.nil? || user.allowed_to?(:manage_issue_relations, issue_from.project)) || |
|
61 | 75 | (issue_to.nil? || user.allowed_to?(:manage_issue_relations, issue_to.project))) |
|
62 | 76 | end |
|
63 | 77 | |
|
64 | 78 | def initialize(attributes=nil, *args) |
|
65 | 79 | super |
|
66 | 80 | if new_record? |
|
67 | 81 | if relation_type.blank? |
|
68 | 82 | self.relation_type = IssueRelation::TYPE_RELATES |
|
69 | 83 | end |
|
70 | 84 | end |
|
71 | 85 | end |
|
72 | 86 | |
|
73 | 87 | def validate_issue_relation |
|
74 | 88 | if issue_from && issue_to |
|
75 | 89 | errors.add :issue_to_id, :invalid if issue_from_id == issue_to_id |
|
76 | 90 | errors.add :issue_to_id, :not_same_project unless issue_from.project_id == issue_to.project_id || Setting.cross_project_issue_relations? |
|
77 | 91 | #detect circular dependencies depending wether the relation should be reversed |
|
78 | 92 | if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] |
|
79 | 93 | errors.add :base, :circular_dependency if issue_from.all_dependent_issues.include? issue_to |
|
80 | 94 | else |
|
81 | 95 | errors.add :base, :circular_dependency if issue_to.all_dependent_issues.include? issue_from |
|
82 | 96 | end |
|
83 | 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 | 98 | end |
|
85 | 99 | end |
|
86 | 100 | |
|
87 | 101 | def other_issue(issue) |
|
88 | 102 | (self.issue_from_id == issue.id) ? issue_to : issue_from |
|
89 | 103 | end |
|
90 | 104 | |
|
91 | 105 | # Returns the relation type for +issue+ |
|
92 | 106 | def relation_type_for(issue) |
|
93 | 107 | if TYPES[relation_type] |
|
94 | 108 | if self.issue_from_id == issue.id |
|
95 | 109 | relation_type |
|
96 | 110 | else |
|
97 | 111 | TYPES[relation_type][:sym] |
|
98 | 112 | end |
|
99 | 113 | end |
|
100 | 114 | end |
|
101 | 115 | |
|
102 | 116 | def label_for(issue) |
|
103 | 117 | TYPES[relation_type] ? TYPES[relation_type][(self.issue_from_id == issue.id) ? :name : :sym_name] : :unknow |
|
104 | 118 | end |
|
105 | 119 | |
|
120 | def css_classes_for(issue) | |
|
121 | "rel-#{relation_type_for(issue)}" | |
|
122 | end | |
|
123 | ||
|
106 | 124 | def handle_issue_order |
|
107 | 125 | reverse_if_needed |
|
108 | 126 | |
|
109 | 127 | if TYPE_PRECEDES == relation_type |
|
110 | 128 | self.delay ||= 0 |
|
111 | 129 | else |
|
112 | 130 | self.delay = nil |
|
113 | 131 | end |
|
114 | 132 | set_issue_to_dates |
|
115 | 133 | end |
|
116 | 134 | |
|
117 | 135 | def set_issue_to_dates |
|
118 | 136 | soonest_start = self.successor_soonest_start |
|
119 | 137 | if soonest_start && issue_to |
|
120 | 138 | issue_to.reschedule_after(soonest_start) |
|
121 | 139 | end |
|
122 | 140 | end |
|
123 | 141 | |
|
124 | 142 | def successor_soonest_start |
|
125 | 143 | if (TYPE_PRECEDES == self.relation_type) && delay && issue_from && (issue_from.start_date || issue_from.due_date) |
|
126 | 144 | (issue_from.due_date || issue_from.start_date) + 1 + delay |
|
127 | 145 | end |
|
128 | 146 | end |
|
129 | 147 | |
|
130 | 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 | 151 | end |
|
133 | 152 | |
|
134 | 153 | private |
|
135 | 154 | |
|
136 | 155 | # Reverses the relation if needed so that it gets stored in the proper way |
|
137 | 156 | # Should not be reversed before validation so that it can be displayed back |
|
138 | 157 | # as entered on new relation form |
|
139 | 158 | def reverse_if_needed |
|
140 | 159 | if TYPES.has_key?(relation_type) && TYPES[relation_type][:reverse] |
|
141 | 160 | issue_tmp = issue_to |
|
142 | 161 | self.issue_to = issue_from |
|
143 | 162 | self.issue_from = issue_tmp |
|
144 | 163 | self.relation_type = TYPES[relation_type][:reverse] |
|
145 | 164 | end |
|
146 | 165 | end |
|
147 | 166 | end |
@@ -1,962 +1,1019 | |||
|
1 | 1 | # Redmine - project management software |
|
2 | 2 | # Copyright (C) 2006-2012 Jean-Philippe Lang |
|
3 | 3 | # |
|
4 | 4 | # This program is free software; you can redistribute it and/or |
|
5 | 5 | # modify it under the terms of the GNU General Public License |
|
6 | 6 | # as published by the Free Software Foundation; either version 2 |
|
7 | 7 | # of the License, or (at your option) any later version. |
|
8 | 8 | # |
|
9 | 9 | # This program is distributed in the hope that it will be useful, |
|
10 | 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
11 | 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
12 | 12 | # GNU General Public License for more details. |
|
13 | 13 | # |
|
14 | 14 | # You should have received a copy of the GNU General Public License |
|
15 | 15 | # along with this program; if not, write to the Free Software |
|
16 | 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|
17 | 17 | |
|
18 | 18 | class QueryColumn |
|
19 | 19 | attr_accessor :name, :sortable, :groupable, :default_order |
|
20 | 20 | include Redmine::I18n |
|
21 | 21 | |
|
22 | 22 | def initialize(name, options={}) |
|
23 | 23 | self.name = name |
|
24 | 24 | self.sortable = options[:sortable] |
|
25 | 25 | self.groupable = options[:groupable] || false |
|
26 | 26 | if groupable == true |
|
27 | 27 | self.groupable = name.to_s |
|
28 | 28 | end |
|
29 | 29 | self.default_order = options[:default_order] |
|
30 | 30 | @caption_key = options[:caption] || "field_#{name}" |
|
31 | 31 | end |
|
32 | 32 | |
|
33 | 33 | def caption |
|
34 | 34 | l(@caption_key) |
|
35 | 35 | end |
|
36 | 36 | |
|
37 | 37 | # Returns true if the column is sortable, otherwise false |
|
38 | 38 | def sortable? |
|
39 | 39 | !@sortable.nil? |
|
40 | 40 | end |
|
41 | 41 | |
|
42 | 42 | def sortable |
|
43 | 43 | @sortable.is_a?(Proc) ? @sortable.call : @sortable |
|
44 | 44 | end |
|
45 | 45 | |
|
46 | 46 | def value(issue) |
|
47 | 47 | issue.send name |
|
48 | 48 | end |
|
49 | 49 | |
|
50 | 50 | def css_classes |
|
51 | 51 | name |
|
52 | 52 | end |
|
53 | 53 | end |
|
54 | 54 | |
|
55 | 55 | class QueryCustomFieldColumn < QueryColumn |
|
56 | 56 | |
|
57 | 57 | def initialize(custom_field) |
|
58 | 58 | self.name = "cf_#{custom_field.id}".to_sym |
|
59 | 59 | self.sortable = custom_field.order_statement || false |
|
60 | 60 | self.groupable = custom_field.group_statement || false |
|
61 | 61 | @cf = custom_field |
|
62 | 62 | end |
|
63 | 63 | |
|
64 | 64 | def caption |
|
65 | 65 | @cf.name |
|
66 | 66 | end |
|
67 | 67 | |
|
68 | 68 | def custom_field |
|
69 | 69 | @cf |
|
70 | 70 | end |
|
71 | 71 | |
|
72 | 72 | def value(issue) |
|
73 | 73 | cv = issue.custom_values.select {|v| v.custom_field_id == @cf.id}.collect {|v| @cf.cast_value(v.value)} |
|
74 | 74 | cv.size > 1 ? cv : cv.first |
|
75 | 75 | end |
|
76 | 76 | |
|
77 | 77 | def css_classes |
|
78 | 78 | @css_classes ||= "#{name} #{@cf.field_format}" |
|
79 | 79 | end |
|
80 | 80 | end |
|
81 | 81 | |
|
82 | 82 | class Query < ActiveRecord::Base |
|
83 | 83 | class StatementInvalid < ::ActiveRecord::StatementInvalid |
|
84 | 84 | end |
|
85 | 85 | |
|
86 | 86 | belongs_to :project |
|
87 | 87 | belongs_to :user |
|
88 | 88 | serialize :filters |
|
89 | 89 | serialize :column_names |
|
90 | 90 | serialize :sort_criteria, Array |
|
91 | 91 | |
|
92 | 92 | attr_protected :project_id, :user_id |
|
93 | 93 | |
|
94 | 94 | validates_presence_of :name |
|
95 | 95 | validates_length_of :name, :maximum => 255 |
|
96 | 96 | validate :validate_query_filters |
|
97 | 97 | |
|
98 | 98 | @@operators = { "=" => :label_equals, |
|
99 | 99 | "!" => :label_not_equals, |
|
100 | 100 | "o" => :label_open_issues, |
|
101 | 101 | "c" => :label_closed_issues, |
|
102 | 102 | "!*" => :label_none, |
|
103 | 103 | "*" => :label_all, |
|
104 | 104 | ">=" => :label_greater_or_equal, |
|
105 | 105 | "<=" => :label_less_or_equal, |
|
106 | 106 | "><" => :label_between, |
|
107 | 107 | "<t+" => :label_in_less_than, |
|
108 | 108 | ">t+" => :label_in_more_than, |
|
109 | 109 | "t+" => :label_in, |
|
110 | 110 | "t" => :label_today, |
|
111 | 111 | "w" => :label_this_week, |
|
112 | 112 | ">t-" => :label_less_than_ago, |
|
113 | 113 | "<t-" => :label_more_than_ago, |
|
114 | 114 | "t-" => :label_ago, |
|
115 | 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 | 120 | cattr_reader :operators |
|
119 | 121 | |
|
120 | 122 | @@operators_by_filter_type = { :list => [ "=", "!" ], |
|
121 | 123 | :list_status => [ "o", "=", "!", "c", "*" ], |
|
122 | 124 | :list_optional => [ "=", "!", "!*", "*" ], |
|
123 | 125 | :list_subprojects => [ "*", "!*", "=" ], |
|
124 | 126 | :date => [ "=", ">=", "<=", "><", "<t+", ">t+", "t+", "t", "w", ">t-", "<t-", "t-", "!*", "*" ], |
|
125 | 127 | :date_past => [ "=", ">=", "<=", "><", ">t-", "<t-", "t-", "t", "w", "!*", "*" ], |
|
126 | 128 | :string => [ "=", "~", "!", "!~", "!*", "*" ], |
|
127 | 129 | :text => [ "~", "!~", "!*", "*" ], |
|
128 | 130 | :integer => [ "=", ">=", "<=", "><", "!*", "*" ], |
|
129 |
:float => [ "=", ">=", "<=", "><", "!*", "*" ] |
|
|
131 | :float => [ "=", ">=", "<=", "><", "!*", "*" ], | |
|
132 | :relation => ["=", "=p", "=!p", "!*", "*"]} | |
|
130 | 133 | |
|
131 | 134 | cattr_reader :operators_by_filter_type |
|
132 | 135 | |
|
133 | 136 | @@available_columns = [ |
|
134 | 137 | QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true), |
|
135 | 138 | QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true), |
|
136 | 139 | QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue), |
|
137 | 140 | QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true), |
|
138 | 141 | QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true), |
|
139 | 142 | QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"), |
|
140 | 143 | QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true), |
|
141 | 144 | QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true), |
|
142 | 145 | QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'), |
|
143 | 146 | QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true), |
|
144 | 147 | QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true), |
|
145 | 148 | QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"), |
|
146 | 149 | QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"), |
|
147 | 150 | QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"), |
|
148 | 151 | QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true), |
|
149 | 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 | 155 | cattr_reader :available_columns |
|
152 | 156 | |
|
153 | 157 | scope :visible, lambda {|*args| |
|
154 | 158 | user = args.shift || User.current |
|
155 | 159 | base = Project.allowed_to_condition(user, :view_issues, *args) |
|
156 | 160 | user_id = user.logged? ? user.id : 0 |
|
157 | 161 | { |
|
158 | 162 | :conditions => ["(#{table_name}.project_id IS NULL OR (#{base})) AND (#{table_name}.is_public = ? OR #{table_name}.user_id = ?)", true, user_id], |
|
159 | 163 | :include => :project |
|
160 | 164 | } |
|
161 | 165 | } |
|
162 | 166 | |
|
163 | 167 | def initialize(attributes=nil, *args) |
|
164 | 168 | super attributes |
|
165 | 169 | self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} } |
|
166 | 170 | @is_for_all = project.nil? |
|
167 | 171 | end |
|
168 | 172 | |
|
169 | 173 | def validate_query_filters |
|
170 | 174 | filters.each_key do |field| |
|
171 | 175 | if values_for(field) |
|
172 | 176 | case type_for(field) |
|
173 | 177 | when :integer |
|
174 | 178 | add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+$/) } |
|
175 | 179 | when :float |
|
176 | 180 | add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^[+-]?\d+(\.\d*)?$/) } |
|
177 | 181 | when :date, :date_past |
|
178 | 182 | case operator_for(field) |
|
179 | 183 | when "=", ">=", "<=", "><" |
|
180 | 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 | 185 | when ">t-", "<t-", "t-" |
|
182 | 186 | add_filter_error(field, :invalid) if values_for(field).detect {|v| v.present? && !v.match(/^\d+$/) } |
|
183 | 187 | end |
|
184 | 188 | end |
|
185 | 189 | end |
|
186 | 190 | |
|
187 | 191 | add_filter_error(field, :blank) unless |
|
188 | 192 | # filter requires one or more values |
|
189 | 193 | (values_for(field) and !values_for(field).first.blank?) or |
|
190 | 194 | # filter doesn't require any value |
|
191 | 195 | ["o", "c", "!*", "*", "t", "w"].include? operator_for(field) |
|
192 | 196 | end if filters |
|
193 | 197 | end |
|
194 | 198 | |
|
195 | 199 | def add_filter_error(field, message) |
|
196 | 200 | m = label_for(field) + " " + l(message, :scope => 'activerecord.errors.messages') |
|
197 | 201 | errors.add(:base, m) |
|
198 | 202 | end |
|
199 | 203 | |
|
200 | 204 | # Returns true if the query is visible to +user+ or the current user. |
|
201 | 205 | def visible?(user=User.current) |
|
202 | 206 | (project.nil? || user.allowed_to?(:view_issues, project)) && (self.is_public? || self.user_id == user.id) |
|
203 | 207 | end |
|
204 | 208 | |
|
205 | 209 | def editable_by?(user) |
|
206 | 210 | return false unless user |
|
207 | 211 | # Admin can edit them all and regular users can edit their private queries |
|
208 | 212 | return true if user.admin? || (!is_public && self.user_id == user.id) |
|
209 | 213 | # Members can not edit public queries that are for all project (only admin is allowed to) |
|
210 | 214 | is_public && !@is_for_all && user.allowed_to?(:manage_public_queries, project) |
|
211 | 215 | end |
|
212 | 216 | |
|
213 | 217 | def trackers |
|
214 | 218 | @trackers ||= project.nil? ? Tracker.find(:all, :order => 'position') : project.rolled_up_trackers |
|
215 | 219 | end |
|
216 | 220 | |
|
217 | 221 | # Returns a hash of localized labels for all filter operators |
|
218 | 222 | def self.operators_labels |
|
219 | 223 | operators.inject({}) {|h, operator| h[operator.first] = l(operator.last); h} |
|
220 | 224 | end |
|
221 | 225 | |
|
222 | 226 | def available_filters |
|
223 | 227 | return @available_filters if @available_filters |
|
224 | 228 | |
|
225 | 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 | 230 | "tracker_id" => { :type => :list, :order => 2, :values => trackers.collect{|s| [s.name, s.id.to_s] } }, |
|
227 | 231 | "priority_id" => { :type => :list, :order => 3, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] } }, |
|
228 | 232 | "subject" => { :type => :text, :order => 8 }, |
|
229 | 233 | "created_on" => { :type => :date_past, :order => 9 }, |
|
230 | 234 | "updated_on" => { :type => :date_past, :order => 10 }, |
|
231 | 235 | "start_date" => { :type => :date, :order => 11 }, |
|
232 | 236 | "due_date" => { :type => :date, :order => 12 }, |
|
233 | 237 | "estimated_hours" => { :type => :float, :order => 13 }, |
|
234 | 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 | 244 | principals = [] |
|
237 | 245 | if project |
|
238 | 246 | principals += project.principals.sort |
|
239 | 247 | unless project.leaf? |
|
240 | 248 | subprojects = project.descendants.visible.all |
|
241 | 249 | if subprojects.any? |
|
242 | 250 | @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => subprojects.collect{|s| [s.name, s.id.to_s] } } |
|
243 | 251 | principals += Principal.member_of(subprojects) |
|
244 | 252 | end |
|
245 | 253 | end |
|
246 | 254 | else |
|
247 | all_projects = Project.visible.all | |
|
248 | 255 | if all_projects.any? |
|
249 | 256 | # members of visible projects |
|
250 | 257 | principals += Principal.member_of(all_projects) |
|
251 | 258 | |
|
252 | 259 | # project filter |
|
253 | 260 | project_values = [] |
|
254 | 261 | if User.current.logged? && User.current.memberships.any? |
|
255 | 262 | project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"] |
|
256 | 263 | end |
|
257 |
|
|
|
258 | prefix = (level > 0 ? ('--' * level + ' ') : '') | |
|
259 | project_values << ["#{prefix}#{p.name}", p.id.to_s] | |
|
260 | end | |
|
264 | project_values += all_projects_values | |
|
261 | 265 | @available_filters["project_id"] = { :type => :list, :order => 1, :values => project_values} unless project_values.empty? |
|
262 | 266 | end |
|
263 | 267 | end |
|
264 | 268 | principals.uniq! |
|
265 | 269 | principals.sort! |
|
266 | 270 | users = principals.select {|p| p.is_a?(User)} |
|
267 | 271 | |
|
268 | 272 | assigned_to_values = [] |
|
269 | 273 | assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? |
|
270 | 274 | assigned_to_values += (Setting.issue_group_assignment? ? principals : users).collect{|s| [s.name, s.id.to_s] } |
|
271 | 275 | @available_filters["assigned_to_id"] = { :type => :list_optional, :order => 4, :values => assigned_to_values } unless assigned_to_values.empty? |
|
272 | 276 | |
|
273 | 277 | author_values = [] |
|
274 | 278 | author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged? |
|
275 | 279 | author_values += users.collect{|s| [s.name, s.id.to_s] } |
|
276 | 280 | @available_filters["author_id"] = { :type => :list, :order => 5, :values => author_values } unless author_values.empty? |
|
277 | 281 | |
|
278 | 282 | group_values = Group.all.collect {|g| [g.name, g.id.to_s] } |
|
279 | 283 | @available_filters["member_of_group"] = { :type => :list_optional, :order => 6, :values => group_values } unless group_values.empty? |
|
280 | 284 | |
|
281 | 285 | role_values = Role.givable.collect {|r| [r.name, r.id.to_s] } |
|
282 | 286 | @available_filters["assigned_to_role"] = { :type => :list_optional, :order => 7, :values => role_values } unless role_values.empty? |
|
283 | 287 | |
|
284 | 288 | if User.current.logged? |
|
285 | 289 | @available_filters["watcher_id"] = { :type => :list, :order => 15, :values => [["<< #{l(:label_me)} >>", "me"]] } |
|
286 | 290 | end |
|
287 | 291 | |
|
288 | 292 | if project |
|
289 | 293 | # project specific filters |
|
290 | 294 | categories = project.issue_categories.all |
|
291 | 295 | unless categories.empty? |
|
292 | 296 | @available_filters["category_id"] = { :type => :list_optional, :order => 6, :values => categories.collect{|s| [s.name, s.id.to_s] } } |
|
293 | 297 | end |
|
294 | 298 | versions = project.shared_versions.all |
|
295 | 299 | unless versions.empty? |
|
296 | 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 | 301 | end |
|
298 | 302 | add_custom_fields_filters(project.all_issue_custom_fields) |
|
299 | 303 | else |
|
300 | 304 | # global filters for cross project issue list |
|
301 | 305 | system_shared_versions = Version.visible.find_all_by_sharing('system') |
|
302 | 306 | unless system_shared_versions.empty? |
|
303 | 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 | 308 | end |
|
305 | 309 | add_custom_fields_filters(IssueCustomField.find(:all, :conditions => {:is_filter => true, :is_for_all => true})) |
|
306 | 310 | end |
|
307 | 311 | |
|
308 | 312 | add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version |
|
309 | 313 | |
|
310 | 314 | if User.current.allowed_to?(:set_issues_private, nil, :global => true) || |
|
311 | 315 | User.current.allowed_to?(:set_own_issues_private, nil, :global => true) |
|
312 | 316 | @available_filters["is_private"] = { :type => :list, :order => 15, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]] } |
|
313 | 317 | end |
|
314 | 318 | |
|
315 | 319 | Tracker.disabled_core_fields(trackers).each {|field| |
|
316 | 320 | @available_filters.delete field |
|
317 | 321 | } |
|
318 | 322 | |
|
319 | 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 | 325 | end |
|
322 | 326 | |
|
323 | 327 | @available_filters |
|
324 | 328 | end |
|
325 | 329 | |
|
326 | 330 | # Returns a representation of the available filters for JSON serialization |
|
327 | 331 | def available_filters_as_json |
|
328 | 332 | json = {} |
|
329 | 333 | available_filters.each do |field, options| |
|
330 | 334 | json[field] = options.slice(:type, :name, :values).stringify_keys |
|
331 | 335 | end |
|
332 | 336 | json |
|
333 | 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 | 354 | def add_filter(field, operator, values) |
|
336 | 355 | # values must be an array |
|
337 | 356 | return unless values.nil? || values.is_a?(Array) |
|
338 | 357 | # check if field is defined as an available filter |
|
339 | 358 | if available_filters.has_key? field |
|
340 | 359 | filter_options = available_filters[field] |
|
341 | 360 | # check if operator is allowed for that filter |
|
342 | 361 | #if @@operators_by_filter_type[filter_options[:type]].include? operator |
|
343 | 362 | # allowed_values = values & ([""] + (filter_options[:values] || []).collect {|val| val[1]}) |
|
344 | 363 | # filters[field] = {:operator => operator, :values => allowed_values } if (allowed_values.first and !allowed_values.first.empty?) or ["o", "c", "!*", "*", "t"].include? operator |
|
345 | 364 | #end |
|
346 | 365 | filters[field] = {:operator => operator, :values => (values || [''])} |
|
347 | 366 | end |
|
348 | 367 | end |
|
349 | 368 | |
|
350 | 369 | def add_short_filter(field, expression) |
|
351 | 370 | return unless expression && available_filters.has_key?(field) |
|
352 | 371 | field_type = available_filters[field][:type] |
|
353 | 372 | @@operators_by_filter_type[field_type].sort.reverse.detect do |operator| |
|
354 | 373 | next unless expression =~ /^#{Regexp.escape(operator)}(.*)$/ |
|
355 | 374 | add_filter field, operator, $1.present? ? $1.split('|') : [''] |
|
356 | 375 | end || add_filter(field, '=', expression.split('|')) |
|
357 | 376 | end |
|
358 | 377 | |
|
359 | 378 | # Add multiple filters using +add_filter+ |
|
360 | 379 | def add_filters(fields, operators, values) |
|
361 | 380 | if fields.is_a?(Array) && operators.is_a?(Hash) && (values.nil? || values.is_a?(Hash)) |
|
362 | 381 | fields.each do |field| |
|
363 | 382 | add_filter(field, operators[field], values && values[field]) |
|
364 | 383 | end |
|
365 | 384 | end |
|
366 | 385 | end |
|
367 | 386 | |
|
368 | 387 | def has_filter?(field) |
|
369 | 388 | filters and filters[field] |
|
370 | 389 | end |
|
371 | 390 | |
|
372 | 391 | def type_for(field) |
|
373 | 392 | available_filters[field][:type] if available_filters.has_key?(field) |
|
374 | 393 | end |
|
375 | 394 | |
|
376 | 395 | def operator_for(field) |
|
377 | 396 | has_filter?(field) ? filters[field][:operator] : nil |
|
378 | 397 | end |
|
379 | 398 | |
|
380 | 399 | def values_for(field) |
|
381 | 400 | has_filter?(field) ? filters[field][:values] : nil |
|
382 | 401 | end |
|
383 | 402 | |
|
384 | 403 | def value_for(field, index=0) |
|
385 | 404 | (values_for(field) || [])[index] |
|
386 | 405 | end |
|
387 | 406 | |
|
388 | 407 | def label_for(field) |
|
389 | 408 | label = available_filters[field][:name] if available_filters.has_key?(field) |
|
390 | 409 | label ||= l("field_#{field.to_s.gsub(/_id$/, '')}", :default => field) |
|
391 | 410 | end |
|
392 | 411 | |
|
393 | 412 | def available_columns |
|
394 | 413 | return @available_columns if @available_columns |
|
395 | 414 | @available_columns = ::Query.available_columns.dup |
|
396 | 415 | @available_columns += (project ? |
|
397 | 416 | project.all_issue_custom_fields : |
|
398 | 417 | IssueCustomField.find(:all) |
|
399 | 418 | ).collect {|cf| QueryCustomFieldColumn.new(cf) } |
|
400 | 419 | |
|
401 | 420 | if User.current.allowed_to?(:view_time_entries, project, :global => true) |
|
402 | 421 | index = nil |
|
403 | 422 | @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours} |
|
404 | 423 | index = (index ? index + 1 : -1) |
|
405 | 424 | # insert the column after estimated_hours or at the end |
|
406 | 425 | @available_columns.insert index, QueryColumn.new(:spent_hours, |
|
407 | 426 | :sortable => "(SELECT COALESCE(SUM(hours), 0) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id)", |
|
408 | 427 | :default_order => 'desc', |
|
409 | 428 | :caption => :label_spent_time |
|
410 | 429 | ) |
|
411 | 430 | end |
|
412 | 431 | |
|
413 | 432 | if User.current.allowed_to?(:set_issues_private, nil, :global => true) || |
|
414 | 433 | User.current.allowed_to?(:set_own_issues_private, nil, :global => true) |
|
415 | 434 | @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private") |
|
416 | 435 | end |
|
417 | 436 | |
|
418 | 437 | disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')} |
|
419 | 438 | @available_columns.reject! {|column| |
|
420 | 439 | disabled_fields.include?(column.name.to_s) |
|
421 | 440 | } |
|
422 | 441 | |
|
423 | 442 | @available_columns |
|
424 | 443 | end |
|
425 | 444 | |
|
426 | 445 | def self.available_columns=(v) |
|
427 | 446 | self.available_columns = (v) |
|
428 | 447 | end |
|
429 | 448 | |
|
430 | 449 | def self.add_available_column(column) |
|
431 | 450 | self.available_columns << (column) if column.is_a?(QueryColumn) |
|
432 | 451 | end |
|
433 | 452 | |
|
434 | 453 | # Returns an array of columns that can be used to group the results |
|
435 | 454 | def groupable_columns |
|
436 | 455 | available_columns.select {|c| c.groupable} |
|
437 | 456 | end |
|
438 | 457 | |
|
439 | 458 | # Returns a Hash of columns and the key for sorting |
|
440 | 459 | def sortable_columns |
|
441 | 460 | {'id' => "#{Issue.table_name}.id"}.merge(available_columns.inject({}) {|h, column| |
|
442 | 461 | h[column.name.to_s] = column.sortable |
|
443 | 462 | h |
|
444 | 463 | }) |
|
445 | 464 | end |
|
446 | 465 | |
|
447 | 466 | def columns |
|
448 | 467 | # preserve the column_names order |
|
449 | 468 | (has_default_columns? ? default_columns_names : column_names).collect do |name| |
|
450 | 469 | available_columns.find { |col| col.name == name } |
|
451 | 470 | end.compact |
|
452 | 471 | end |
|
453 | 472 | |
|
454 | 473 | def default_columns_names |
|
455 | 474 | @default_columns_names ||= begin |
|
456 | 475 | default_columns = Setting.issue_list_default_columns.map(&:to_sym) |
|
457 | 476 | |
|
458 | 477 | project.present? ? default_columns : [:project] | default_columns |
|
459 | 478 | end |
|
460 | 479 | end |
|
461 | 480 | |
|
462 | 481 | def column_names=(names) |
|
463 | 482 | if names |
|
464 | 483 | names = names.select {|n| n.is_a?(Symbol) || !n.blank? } |
|
465 | 484 | names = names.collect {|n| n.is_a?(Symbol) ? n : n.to_sym } |
|
466 | 485 | # Set column_names to nil if default columns |
|
467 | 486 | if names == default_columns_names |
|
468 | 487 | names = nil |
|
469 | 488 | end |
|
470 | 489 | end |
|
471 | 490 | write_attribute(:column_names, names) |
|
472 | 491 | end |
|
473 | 492 | |
|
474 | 493 | def has_column?(column) |
|
475 | 494 | column_names && column_names.include?(column.is_a?(QueryColumn) ? column.name : column) |
|
476 | 495 | end |
|
477 | 496 | |
|
478 | 497 | def has_default_columns? |
|
479 | 498 | column_names.nil? || column_names.empty? |
|
480 | 499 | end |
|
481 | 500 | |
|
482 | 501 | def sort_criteria=(arg) |
|
483 | 502 | c = [] |
|
484 | 503 | if arg.is_a?(Hash) |
|
485 | 504 | arg = arg.keys.sort.collect {|k| arg[k]} |
|
486 | 505 | end |
|
487 | 506 | c = arg.select {|k,o| !k.to_s.blank?}.slice(0,3).collect {|k,o| [k.to_s, o == 'desc' ? o : 'asc']} |
|
488 | 507 | write_attribute(:sort_criteria, c) |
|
489 | 508 | end |
|
490 | 509 | |
|
491 | 510 | def sort_criteria |
|
492 | 511 | read_attribute(:sort_criteria) || [] |
|
493 | 512 | end |
|
494 | 513 | |
|
495 | 514 | def sort_criteria_key(arg) |
|
496 | 515 | sort_criteria && sort_criteria[arg] && sort_criteria[arg].first |
|
497 | 516 | end |
|
498 | 517 | |
|
499 | 518 | def sort_criteria_order(arg) |
|
500 | 519 | sort_criteria && sort_criteria[arg] && sort_criteria[arg].last |
|
501 | 520 | end |
|
502 | 521 | |
|
503 | 522 | # Returns the SQL sort order that should be prepended for grouping |
|
504 | 523 | def group_by_sort_order |
|
505 | 524 | if grouped? && (column = group_by_column) |
|
506 | 525 | column.sortable.is_a?(Array) ? |
|
507 | 526 | column.sortable.collect {|s| "#{s} #{column.default_order}"}.join(',') : |
|
508 | 527 | "#{column.sortable} #{column.default_order}" |
|
509 | 528 | end |
|
510 | 529 | end |
|
511 | 530 | |
|
512 | 531 | # Returns true if the query is a grouped query |
|
513 | 532 | def grouped? |
|
514 | 533 | !group_by_column.nil? |
|
515 | 534 | end |
|
516 | 535 | |
|
517 | 536 | def group_by_column |
|
518 | 537 | groupable_columns.detect {|c| c.groupable && c.name.to_s == group_by} |
|
519 | 538 | end |
|
520 | 539 | |
|
521 | 540 | def group_by_statement |
|
522 | 541 | group_by_column.try(:groupable) |
|
523 | 542 | end |
|
524 | 543 | |
|
525 | 544 | def project_statement |
|
526 | 545 | project_clauses = [] |
|
527 | 546 | if project && !project.descendants.active.empty? |
|
528 | 547 | ids = [project.id] |
|
529 | 548 | if has_filter?("subproject_id") |
|
530 | 549 | case operator_for("subproject_id") |
|
531 | 550 | when '=' |
|
532 | 551 | # include the selected subprojects |
|
533 | 552 | ids += values_for("subproject_id").each(&:to_i) |
|
534 | 553 | when '!*' |
|
535 | 554 | # main project only |
|
536 | 555 | else |
|
537 | 556 | # all subprojects |
|
538 | 557 | ids += project.descendants.collect(&:id) |
|
539 | 558 | end |
|
540 | 559 | elsif Setting.display_subprojects_issues? |
|
541 | 560 | ids += project.descendants.collect(&:id) |
|
542 | 561 | end |
|
543 | 562 | project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') |
|
544 | 563 | elsif project |
|
545 | 564 | project_clauses << "#{Project.table_name}.id = %d" % project.id |
|
546 | 565 | end |
|
547 | 566 | project_clauses.any? ? project_clauses.join(' AND ') : nil |
|
548 | 567 | end |
|
549 | 568 | |
|
550 | 569 | def statement |
|
551 | 570 | # filters clauses |
|
552 | 571 | filters_clauses = [] |
|
553 | 572 | filters.each_key do |field| |
|
554 | 573 | next if field == "subproject_id" |
|
555 | 574 | v = values_for(field).clone |
|
556 | 575 | next unless v and !v.empty? |
|
557 | 576 | operator = operator_for(field) |
|
558 | 577 | |
|
559 | 578 | # "me" value subsitution |
|
560 | 579 | if %w(assigned_to_id author_id watcher_id).include?(field) |
|
561 | 580 | if v.delete("me") |
|
562 | 581 | if User.current.logged? |
|
563 | 582 | v.push(User.current.id.to_s) |
|
564 | 583 | v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id' |
|
565 | 584 | else |
|
566 | 585 | v.push("0") |
|
567 | 586 | end |
|
568 | 587 | end |
|
569 | 588 | end |
|
570 | 589 | |
|
571 | 590 | if field == 'project_id' |
|
572 | 591 | if v.delete('mine') |
|
573 | 592 | v += User.current.memberships.map(&:project_id).map(&:to_s) |
|
574 | 593 | end |
|
575 | 594 | end |
|
576 | 595 | |
|
577 | 596 | if field =~ /cf_(\d+)$/ |
|
578 | 597 | # custom field |
|
579 | 598 | filters_clauses << sql_for_custom_field(field, operator, v, $1) |
|
580 | 599 | elsif respond_to?("sql_for_#{field}_field") |
|
581 | 600 | # specific statement |
|
582 | 601 | filters_clauses << send("sql_for_#{field}_field", field, operator, v) |
|
583 | 602 | else |
|
584 | 603 | # regular field |
|
585 | 604 | filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')' |
|
586 | 605 | end |
|
587 | 606 | end if filters and valid? |
|
588 | 607 | |
|
589 | 608 | filters_clauses << project_statement |
|
590 | 609 | filters_clauses.reject!(&:blank?) |
|
591 | 610 | |
|
592 | 611 | filters_clauses.any? ? filters_clauses.join(' AND ') : nil |
|
593 | 612 | end |
|
594 | 613 | |
|
595 | 614 | # Returns the issue count |
|
596 | 615 | def issue_count |
|
597 | 616 | Issue.visible.count(:include => [:status, :project], :conditions => statement) |
|
598 | 617 | rescue ::ActiveRecord::StatementInvalid => e |
|
599 | 618 | raise StatementInvalid.new(e.message) |
|
600 | 619 | end |
|
601 | 620 | |
|
602 | 621 | # Returns the issue count by group or nil if query is not grouped |
|
603 | 622 | def issue_count_by_group |
|
604 | 623 | r = nil |
|
605 | 624 | if grouped? |
|
606 | 625 | begin |
|
607 | 626 | # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value |
|
608 | 627 | r = Issue.visible.count(:group => group_by_statement, :include => [:status, :project], :conditions => statement) |
|
609 | 628 | rescue ActiveRecord::RecordNotFound |
|
610 | 629 | r = {nil => issue_count} |
|
611 | 630 | end |
|
612 | 631 | c = group_by_column |
|
613 | 632 | if c.is_a?(QueryCustomFieldColumn) |
|
614 | 633 | r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h} |
|
615 | 634 | end |
|
616 | 635 | end |
|
617 | 636 | r |
|
618 | 637 | rescue ::ActiveRecord::StatementInvalid => e |
|
619 | 638 | raise StatementInvalid.new(e.message) |
|
620 | 639 | end |
|
621 | 640 | |
|
622 | 641 | # Returns the issues |
|
623 | 642 | # Valid options are :order, :offset, :limit, :include, :conditions |
|
624 | 643 | def issues(options={}) |
|
625 | 644 | order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') |
|
626 | 645 | order_option = nil if order_option.blank? |
|
627 | 646 | |
|
628 | 647 | issues = Issue.visible.scoped(:conditions => options[:conditions]).find :all, :include => ([:status, :project] + (options[:include] || [])).uniq, |
|
629 | 648 | :conditions => statement, |
|
630 | 649 | :order => order_option, |
|
631 | 650 | :joins => joins_for_order_statement(order_option), |
|
632 | 651 | :limit => options[:limit], |
|
633 | 652 | :offset => options[:offset] |
|
634 | 653 | |
|
635 | 654 | if has_column?(:spent_hours) |
|
636 | 655 | Issue.load_visible_spent_hours(issues) |
|
637 | 656 | end |
|
657 | if has_column?(:relations) | |
|
658 | Issue.load_visible_relations(issues) | |
|
659 | end | |
|
638 | 660 | issues |
|
639 | 661 | rescue ::ActiveRecord::StatementInvalid => e |
|
640 | 662 | raise StatementInvalid.new(e.message) |
|
641 | 663 | end |
|
642 | 664 | |
|
643 | 665 | # Returns the issues ids |
|
644 | 666 | def issue_ids(options={}) |
|
645 | 667 | order_option = [group_by_sort_order, options[:order]].reject {|s| s.blank?}.join(',') |
|
646 | 668 | order_option = nil if order_option.blank? |
|
647 | 669 | |
|
648 | 670 | Issue.visible.scoped(:conditions => options[:conditions]).scoped(:include => ([:status, :project] + (options[:include] || [])).uniq, |
|
649 | 671 | :conditions => statement, |
|
650 | 672 | :order => order_option, |
|
651 | 673 | :joins => joins_for_order_statement(order_option), |
|
652 | 674 | :limit => options[:limit], |
|
653 | 675 | :offset => options[:offset]).find_ids |
|
654 | 676 | rescue ::ActiveRecord::StatementInvalid => e |
|
655 | 677 | raise StatementInvalid.new(e.message) |
|
656 | 678 | end |
|
657 | 679 | |
|
658 | 680 | # Returns the journals |
|
659 | 681 | # Valid options are :order, :offset, :limit |
|
660 | 682 | def journals(options={}) |
|
661 | 683 | Journal.visible.find :all, :include => [:details, :user, {:issue => [:project, :author, :tracker, :status]}], |
|
662 | 684 | :conditions => statement, |
|
663 | 685 | :order => options[:order], |
|
664 | 686 | :limit => options[:limit], |
|
665 | 687 | :offset => options[:offset] |
|
666 | 688 | rescue ::ActiveRecord::StatementInvalid => e |
|
667 | 689 | raise StatementInvalid.new(e.message) |
|
668 | 690 | end |
|
669 | 691 | |
|
670 | 692 | # Returns the versions |
|
671 | 693 | # Valid options are :conditions |
|
672 | 694 | def versions(options={}) |
|
673 | 695 | Version.visible.scoped(:conditions => options[:conditions]).find :all, :include => :project, :conditions => project_statement |
|
674 | 696 | rescue ::ActiveRecord::StatementInvalid => e |
|
675 | 697 | raise StatementInvalid.new(e.message) |
|
676 | 698 | end |
|
677 | 699 | |
|
678 | 700 | def sql_for_watcher_id_field(field, operator, value) |
|
679 | 701 | db_table = Watcher.table_name |
|
680 | 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 | 703 | sql_for_field(field, '=', value, db_table, 'user_id') + ')' |
|
682 | 704 | end |
|
683 | 705 | |
|
684 | 706 | def sql_for_member_of_group_field(field, operator, value) |
|
685 | 707 | if operator == '*' # Any group |
|
686 | 708 | groups = Group.all |
|
687 | 709 | operator = '=' # Override the operator since we want to find by assigned_to |
|
688 | 710 | elsif operator == "!*" |
|
689 | 711 | groups = Group.all |
|
690 | 712 | operator = '!' # Override the operator since we want to find by assigned_to |
|
691 | 713 | else |
|
692 | 714 | groups = Group.find_all_by_id(value) |
|
693 | 715 | end |
|
694 | 716 | groups ||= [] |
|
695 | 717 | |
|
696 | 718 | members_of_groups = groups.inject([]) {|user_ids, group| |
|
697 | 719 | if group && group.user_ids.present? |
|
698 | 720 | user_ids << group.user_ids |
|
699 | 721 | end |
|
700 | 722 | user_ids.flatten.uniq.compact |
|
701 | 723 | }.sort.collect(&:to_s) |
|
702 | 724 | |
|
703 | 725 | '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')' |
|
704 | 726 | end |
|
705 | 727 | |
|
706 | 728 | def sql_for_assigned_to_role_field(field, operator, value) |
|
707 | 729 | case operator |
|
708 | 730 | when "*", "!*" # Member / Not member |
|
709 | 731 | sw = operator == "!*" ? 'NOT' : '' |
|
710 | 732 | nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' |
|
711 | 733 | "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" + |
|
712 | 734 | " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))" |
|
713 | 735 | when "=", "!" |
|
714 | 736 | role_cond = value.any? ? |
|
715 | 737 | "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" : |
|
716 | 738 | "1=0" |
|
717 | 739 | |
|
718 | 740 | sw = operator == "!" ? 'NOT' : '' |
|
719 | 741 | nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : '' |
|
720 | 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 | 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 | 744 | end |
|
723 | 745 | end |
|
724 | 746 | |
|
725 | 747 | def sql_for_is_private_field(field, operator, value) |
|
726 | 748 | op = (operator == "=" ? 'IN' : 'NOT IN') |
|
727 | 749 | va = value.map {|v| v == '0' ? connection.quoted_false : connection.quoted_true}.uniq.join(',') |
|
728 | 750 | |
|
729 | 751 | "#{Issue.table_name}.is_private #{op} (#{va})" |
|
730 | 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 | 789 | private |
|
733 | 790 | |
|
734 | 791 | def sql_for_custom_field(field, operator, value, custom_field_id) |
|
735 | 792 | db_table = CustomValue.table_name |
|
736 | 793 | db_field = 'value' |
|
737 | 794 | filter = @available_filters[field] |
|
738 | 795 | return nil unless filter |
|
739 | 796 | if filter[:format] == 'user' |
|
740 | 797 | if value.delete('me') |
|
741 | 798 | value.push User.current.id.to_s |
|
742 | 799 | end |
|
743 | 800 | end |
|
744 | 801 | not_in = nil |
|
745 | 802 | if operator == '!' |
|
746 | 803 | # Makes ! operator work for custom fields with multiple values |
|
747 | 804 | operator = '=' |
|
748 | 805 | not_in = 'NOT' |
|
749 | 806 | end |
|
750 | 807 | customized_key = "id" |
|
751 | 808 | customized_class = Issue |
|
752 | 809 | if field =~ /^(.+)\.cf_/ |
|
753 | 810 | assoc = $1 |
|
754 | 811 | customized_key = "#{assoc}_id" |
|
755 | 812 | customized_class = Issue.reflect_on_association(assoc.to_sym).klass.base_class rescue nil |
|
756 | 813 | raise "Unknown Issue association #{assoc}" unless customized_class |
|
757 | 814 | end |
|
758 | 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 | 816 | sql_for_field(field, operator, value, db_table, db_field, true) + ')' |
|
760 | 817 | end |
|
761 | 818 | |
|
762 | 819 | # Helper method to generate the WHERE sql for a +field+, +operator+ and a +value+ |
|
763 | 820 | def sql_for_field(field, operator, value, db_table, db_field, is_custom_filter=false) |
|
764 | 821 | sql = '' |
|
765 | 822 | case operator |
|
766 | 823 | when "=" |
|
767 | 824 | if value.any? |
|
768 | 825 | case type_for(field) |
|
769 | 826 | when :date, :date_past |
|
770 | 827 | sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), (Date.parse(value.first) rescue nil)) |
|
771 | 828 | when :integer |
|
772 | 829 | if is_custom_filter |
|
773 | 830 | sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) = #{value.first.to_i})" |
|
774 | 831 | else |
|
775 | 832 | sql = "#{db_table}.#{db_field} = #{value.first.to_i}" |
|
776 | 833 | end |
|
777 | 834 | when :float |
|
778 | 835 | if is_custom_filter |
|
779 | 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 | 837 | else |
|
781 | 838 | sql = "#{db_table}.#{db_field} BETWEEN #{value.first.to_f - 1e-5} AND #{value.first.to_f + 1e-5}" |
|
782 | 839 | end |
|
783 | 840 | else |
|
784 | 841 | sql = "#{db_table}.#{db_field} IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + ")" |
|
785 | 842 | end |
|
786 | 843 | else |
|
787 | 844 | # IN an empty set |
|
788 | 845 | sql = "1=0" |
|
789 | 846 | end |
|
790 | 847 | when "!" |
|
791 | 848 | if value.any? |
|
792 | 849 | sql = "(#{db_table}.#{db_field} IS NULL OR #{db_table}.#{db_field} NOT IN (" + value.collect{|val| "'#{connection.quote_string(val)}'"}.join(",") + "))" |
|
793 | 850 | else |
|
794 | 851 | # NOT IN an empty set |
|
795 | 852 | sql = "1=1" |
|
796 | 853 | end |
|
797 | 854 | when "!*" |
|
798 | 855 | sql = "#{db_table}.#{db_field} IS NULL" |
|
799 | 856 | sql << " OR #{db_table}.#{db_field} = ''" if is_custom_filter |
|
800 | 857 | when "*" |
|
801 | 858 | sql = "#{db_table}.#{db_field} IS NOT NULL" |
|
802 | 859 | sql << " AND #{db_table}.#{db_field} <> ''" if is_custom_filter |
|
803 | 860 | when ">=" |
|
804 | 861 | if [:date, :date_past].include?(type_for(field)) |
|
805 | 862 | sql = date_clause(db_table, db_field, (Date.parse(value.first) rescue nil), nil) |
|
806 | 863 | else |
|
807 | 864 | if is_custom_filter |
|
808 | 865 | sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) >= #{value.first.to_f})" |
|
809 | 866 | else |
|
810 | 867 | sql = "#{db_table}.#{db_field} >= #{value.first.to_f}" |
|
811 | 868 | end |
|
812 | 869 | end |
|
813 | 870 | when "<=" |
|
814 | 871 | if [:date, :date_past].include?(type_for(field)) |
|
815 | 872 | sql = date_clause(db_table, db_field, nil, (Date.parse(value.first) rescue nil)) |
|
816 | 873 | else |
|
817 | 874 | if is_custom_filter |
|
818 | 875 | sql = "(#{db_table}.#{db_field} <> '' AND CAST(#{db_table}.#{db_field} AS decimal(60,3)) <= #{value.first.to_f})" |
|
819 | 876 | else |
|
820 | 877 | sql = "#{db_table}.#{db_field} <= #{value.first.to_f}" |
|
821 | 878 | end |
|
822 | 879 | end |
|
823 | 880 | when "><" |
|
824 | 881 | if [:date, :date_past].include?(type_for(field)) |
|
825 | 882 | sql = date_clause(db_table, db_field, (Date.parse(value[0]) rescue nil), (Date.parse(value[1]) rescue nil)) |
|
826 | 883 | else |
|
827 | 884 | if is_custom_filter |
|
828 | 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 | 886 | else |
|
830 | 887 | sql = "#{db_table}.#{db_field} BETWEEN #{value[0].to_f} AND #{value[1].to_f}" |
|
831 | 888 | end |
|
832 | 889 | end |
|
833 | 890 | when "o" |
|
834 | 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 | 892 | when "c" |
|
836 | 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 | 894 | when ">t-" |
|
838 | 895 | sql = relative_date_clause(db_table, db_field, - value.first.to_i, 0) |
|
839 | 896 | when "<t-" |
|
840 | 897 | sql = relative_date_clause(db_table, db_field, nil, - value.first.to_i) |
|
841 | 898 | when "t-" |
|
842 | 899 | sql = relative_date_clause(db_table, db_field, - value.first.to_i, - value.first.to_i) |
|
843 | 900 | when ">t+" |
|
844 | 901 | sql = relative_date_clause(db_table, db_field, value.first.to_i, nil) |
|
845 | 902 | when "<t+" |
|
846 | 903 | sql = relative_date_clause(db_table, db_field, 0, value.first.to_i) |
|
847 | 904 | when "t+" |
|
848 | 905 | sql = relative_date_clause(db_table, db_field, value.first.to_i, value.first.to_i) |
|
849 | 906 | when "t" |
|
850 | 907 | sql = relative_date_clause(db_table, db_field, 0, 0) |
|
851 | 908 | when "w" |
|
852 | 909 | first_day_of_week = l(:general_first_day_of_week).to_i |
|
853 | 910 | day_of_week = Date.today.cwday |
|
854 | 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 | 912 | sql = relative_date_clause(db_table, db_field, - days_ago, - days_ago + 6) |
|
856 | 913 | when "~" |
|
857 | 914 | sql = "LOWER(#{db_table}.#{db_field}) LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" |
|
858 | 915 | when "!~" |
|
859 | 916 | sql = "LOWER(#{db_table}.#{db_field}) NOT LIKE '%#{connection.quote_string(value.first.to_s.downcase)}%'" |
|
860 | 917 | else |
|
861 | 918 | raise "Unknown query operator #{operator}" |
|
862 | 919 | end |
|
863 | 920 | |
|
864 | 921 | return sql |
|
865 | 922 | end |
|
866 | 923 | |
|
867 | 924 | def add_custom_fields_filters(custom_fields, assoc=nil) |
|
868 | 925 | return unless custom_fields.present? |
|
869 | 926 | @available_filters ||= {} |
|
870 | 927 | |
|
871 | 928 | custom_fields.select(&:is_filter?).each do |field| |
|
872 | 929 | case field.field_format |
|
873 | 930 | when "text" |
|
874 | 931 | options = { :type => :text, :order => 20 } |
|
875 | 932 | when "list" |
|
876 | 933 | options = { :type => :list_optional, :values => field.possible_values, :order => 20} |
|
877 | 934 | when "date" |
|
878 | 935 | options = { :type => :date, :order => 20 } |
|
879 | 936 | when "bool" |
|
880 | 937 | options = { :type => :list, :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]], :order => 20 } |
|
881 | 938 | when "int" |
|
882 | 939 | options = { :type => :integer, :order => 20 } |
|
883 | 940 | when "float" |
|
884 | 941 | options = { :type => :float, :order => 20 } |
|
885 | 942 | when "user", "version" |
|
886 | 943 | next unless project |
|
887 | 944 | values = field.possible_values_options(project) |
|
888 | 945 | if User.current.logged? && field.field_format == 'user' |
|
889 | 946 | values.unshift ["<< #{l(:label_me)} >>", "me"] |
|
890 | 947 | end |
|
891 | 948 | options = { :type => :list_optional, :values => values, :order => 20} |
|
892 | 949 | else |
|
893 | 950 | options = { :type => :string, :order => 20 } |
|
894 | 951 | end |
|
895 | 952 | filter_id = "cf_#{field.id}" |
|
896 | 953 | filter_name = field.name |
|
897 | 954 | if assoc.present? |
|
898 | 955 | filter_id = "#{assoc}.#{filter_id}" |
|
899 | 956 | filter_name = l("label_attribute_of_#{assoc}", :name => filter_name) |
|
900 | 957 | end |
|
901 | 958 | @available_filters[filter_id] = options.merge({ :name => filter_name, :format => field.field_format }) |
|
902 | 959 | end |
|
903 | 960 | end |
|
904 | 961 | |
|
905 | 962 | def add_associations_custom_fields_filters(*associations) |
|
906 | 963 | fields_by_class = CustomField.where(:is_filter => true).group_by(&:class) |
|
907 | 964 | associations.each do |assoc| |
|
908 | 965 | association_klass = Issue.reflect_on_association(assoc).klass |
|
909 | 966 | fields_by_class.each do |field_class, fields| |
|
910 | 967 | if field_class.customized_class <= association_klass |
|
911 | 968 | add_custom_fields_filters(fields, assoc) |
|
912 | 969 | end |
|
913 | 970 | end |
|
914 | 971 | end |
|
915 | 972 | end |
|
916 | 973 | |
|
917 | 974 | # Returns a SQL clause for a date or datetime field. |
|
918 | 975 | def date_clause(table, field, from, to) |
|
919 | 976 | s = [] |
|
920 | 977 | if from |
|
921 | 978 | from_yesterday = from - 1 |
|
922 | 979 | from_yesterday_time = Time.local(from_yesterday.year, from_yesterday.month, from_yesterday.day) |
|
923 | 980 | if self.class.default_timezone == :utc |
|
924 | 981 | from_yesterday_time = from_yesterday_time.utc |
|
925 | 982 | end |
|
926 | 983 | s << ("#{table}.#{field} > '%s'" % [connection.quoted_date(from_yesterday_time.end_of_day)]) |
|
927 | 984 | end |
|
928 | 985 | if to |
|
929 | 986 | to_time = Time.local(to.year, to.month, to.day) |
|
930 | 987 | if self.class.default_timezone == :utc |
|
931 | 988 | to_time = to_time.utc |
|
932 | 989 | end |
|
933 | 990 | s << ("#{table}.#{field} <= '%s'" % [connection.quoted_date(to_time.end_of_day)]) |
|
934 | 991 | end |
|
935 | 992 | s.join(' AND ') |
|
936 | 993 | end |
|
937 | 994 | |
|
938 | 995 | # Returns a SQL clause for a date or datetime field using relative dates. |
|
939 | 996 | def relative_date_clause(table, field, days_from, days_to) |
|
940 | 997 | date_clause(table, field, (days_from ? Date.today + days_from : nil), (days_to ? Date.today + days_to : nil)) |
|
941 | 998 | end |
|
942 | 999 | |
|
943 | 1000 | # Additional joins required for the given sort options |
|
944 | 1001 | def joins_for_order_statement(order_options) |
|
945 | 1002 | joins = [] |
|
946 | 1003 | |
|
947 | 1004 | if order_options |
|
948 | 1005 | if order_options.include?('authors') |
|
949 | 1006 | joins << "LEFT OUTER JOIN #{User.table_name} authors ON authors.id = #{Issue.table_name}.author_id" |
|
950 | 1007 | end |
|
951 | 1008 | order_options.scan(/cf_\d+/).uniq.each do |name| |
|
952 | 1009 | column = available_columns.detect {|c| c.name.to_s == name} |
|
953 | 1010 | join = column && column.custom_field.join_for_order_statement |
|
954 | 1011 | if join |
|
955 | 1012 | joins << join |
|
956 | 1013 | end |
|
957 | 1014 | end |
|
958 | 1015 | end |
|
959 | 1016 | |
|
960 | 1017 | joins.any? ? joins.join(' ') : nil |
|
961 | 1018 | end |
|
962 | 1019 | end |
@@ -1,27 +1,28 | |||
|
1 | 1 | <%= javascript_tag do %> |
|
2 | 2 | var operatorLabels = <%= raw_json Query.operators_labels %>; |
|
3 | 3 | var operatorByType = <%= raw_json Query.operators_by_filter_type %>; |
|
4 | 4 | var availableFilters = <%= raw_json query.available_filters_as_json %>; |
|
5 | 5 | var labelDayPlural = <%= raw_json l(:label_day_plural) %>; |
|
6 | var allProjects = <%= raw query.all_projects_values.to_json %>; | |
|
6 | 7 | $(document).ready(function(){ |
|
7 | 8 | initFilters(); |
|
8 | 9 | <% query.filters.each do |field, options| %> |
|
9 | 10 | addFilter("<%= field %>", <%= raw_json query.operator_for(field) %>, <%= raw_json query.values_for(field) %>); |
|
10 | 11 | <% end %> |
|
11 | 12 | }); |
|
12 | 13 | <% end %> |
|
13 | 14 | |
|
14 | 15 | <table style="width:100%"> |
|
15 | 16 | <tr> |
|
16 | 17 | <td> |
|
17 | 18 | <table id="filters-table"> |
|
18 | 19 | </table> |
|
19 | 20 | </td> |
|
20 | 21 | <td class="add-filter"> |
|
21 | 22 | <%= label_tag('add_filter_select', l(:label_filter_add)) %> |
|
22 | 23 | <%= select_tag 'add_filter_select', filters_options_for_select(query), :name => nil %> |
|
23 | 24 | </td> |
|
24 | 25 | </tr> |
|
25 | 26 | </table> |
|
26 | 27 | <%= hidden_field_tag 'f[]', '' %> |
|
27 | 28 | <% include_calendar_headers_tags %> |
@@ -1,1064 +1,1066 | |||
|
1 | 1 | en: |
|
2 | 2 | # Text direction: Left-to-Right (ltr) or Right-to-Left (rtl) |
|
3 | 3 | direction: ltr |
|
4 | 4 | date: |
|
5 | 5 | formats: |
|
6 | 6 | # Use the strftime parameters for formats. |
|
7 | 7 | # When no format has been given, it uses default. |
|
8 | 8 | # You can provide other formats here if you like! |
|
9 | 9 | default: "%m/%d/%Y" |
|
10 | 10 | short: "%b %d" |
|
11 | 11 | long: "%B %d, %Y" |
|
12 | 12 | |
|
13 | 13 | day_names: [Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday] |
|
14 | 14 | abbr_day_names: [Sun, Mon, Tue, Wed, Thu, Fri, Sat] |
|
15 | 15 | |
|
16 | 16 | # Don't forget the nil at the beginning; there's no such thing as a 0th month |
|
17 | 17 | month_names: [~, January, February, March, April, May, June, July, August, September, October, November, December] |
|
18 | 18 | abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec] |
|
19 | 19 | # Used in date_select and datime_select. |
|
20 | 20 | order: |
|
21 | 21 | - :year |
|
22 | 22 | - :month |
|
23 | 23 | - :day |
|
24 | 24 | |
|
25 | 25 | time: |
|
26 | 26 | formats: |
|
27 | 27 | default: "%m/%d/%Y %I:%M %p" |
|
28 | 28 | time: "%I:%M %p" |
|
29 | 29 | short: "%d %b %H:%M" |
|
30 | 30 | long: "%B %d, %Y %H:%M" |
|
31 | 31 | am: "am" |
|
32 | 32 | pm: "pm" |
|
33 | 33 | |
|
34 | 34 | datetime: |
|
35 | 35 | distance_in_words: |
|
36 | 36 | half_a_minute: "half a minute" |
|
37 | 37 | less_than_x_seconds: |
|
38 | 38 | one: "less than 1 second" |
|
39 | 39 | other: "less than %{count} seconds" |
|
40 | 40 | x_seconds: |
|
41 | 41 | one: "1 second" |
|
42 | 42 | other: "%{count} seconds" |
|
43 | 43 | less_than_x_minutes: |
|
44 | 44 | one: "less than a minute" |
|
45 | 45 | other: "less than %{count} minutes" |
|
46 | 46 | x_minutes: |
|
47 | 47 | one: "1 minute" |
|
48 | 48 | other: "%{count} minutes" |
|
49 | 49 | about_x_hours: |
|
50 | 50 | one: "about 1 hour" |
|
51 | 51 | other: "about %{count} hours" |
|
52 | 52 | x_hours: |
|
53 | 53 | one: "1 hour" |
|
54 | 54 | other: "%{count} hours" |
|
55 | 55 | x_days: |
|
56 | 56 | one: "1 day" |
|
57 | 57 | other: "%{count} days" |
|
58 | 58 | about_x_months: |
|
59 | 59 | one: "about 1 month" |
|
60 | 60 | other: "about %{count} months" |
|
61 | 61 | x_months: |
|
62 | 62 | one: "1 month" |
|
63 | 63 | other: "%{count} months" |
|
64 | 64 | about_x_years: |
|
65 | 65 | one: "about 1 year" |
|
66 | 66 | other: "about %{count} years" |
|
67 | 67 | over_x_years: |
|
68 | 68 | one: "over 1 year" |
|
69 | 69 | other: "over %{count} years" |
|
70 | 70 | almost_x_years: |
|
71 | 71 | one: "almost 1 year" |
|
72 | 72 | other: "almost %{count} years" |
|
73 | 73 | |
|
74 | 74 | number: |
|
75 | 75 | format: |
|
76 | 76 | separator: "." |
|
77 | 77 | delimiter: "" |
|
78 | 78 | precision: 3 |
|
79 | 79 | |
|
80 | 80 | human: |
|
81 | 81 | format: |
|
82 | 82 | delimiter: "" |
|
83 | 83 | precision: 3 |
|
84 | 84 | storage_units: |
|
85 | 85 | format: "%n %u" |
|
86 | 86 | units: |
|
87 | 87 | byte: |
|
88 | 88 | one: "Byte" |
|
89 | 89 | other: "Bytes" |
|
90 | 90 | kb: "KB" |
|
91 | 91 | mb: "MB" |
|
92 | 92 | gb: "GB" |
|
93 | 93 | tb: "TB" |
|
94 | 94 | |
|
95 | 95 | # Used in array.to_sentence. |
|
96 | 96 | support: |
|
97 | 97 | array: |
|
98 | 98 | sentence_connector: "and" |
|
99 | 99 | skip_last_comma: false |
|
100 | 100 | |
|
101 | 101 | activerecord: |
|
102 | 102 | errors: |
|
103 | 103 | template: |
|
104 | 104 | header: |
|
105 | 105 | one: "1 error prohibited this %{model} from being saved" |
|
106 | 106 | other: "%{count} errors prohibited this %{model} from being saved" |
|
107 | 107 | messages: |
|
108 | 108 | inclusion: "is not included in the list" |
|
109 | 109 | exclusion: "is reserved" |
|
110 | 110 | invalid: "is invalid" |
|
111 | 111 | confirmation: "doesn't match confirmation" |
|
112 | 112 | accepted: "must be accepted" |
|
113 | 113 | empty: "can't be empty" |
|
114 | 114 | blank: "can't be blank" |
|
115 | 115 | too_long: "is too long (maximum is %{count} characters)" |
|
116 | 116 | too_short: "is too short (minimum is %{count} characters)" |
|
117 | 117 | wrong_length: "is the wrong length (should be %{count} characters)" |
|
118 | 118 | taken: "has already been taken" |
|
119 | 119 | not_a_number: "is not a number" |
|
120 | 120 | not_a_date: "is not a valid date" |
|
121 | 121 | greater_than: "must be greater than %{count}" |
|
122 | 122 | greater_than_or_equal_to: "must be greater than or equal to %{count}" |
|
123 | 123 | equal_to: "must be equal to %{count}" |
|
124 | 124 | less_than: "must be less than %{count}" |
|
125 | 125 | less_than_or_equal_to: "must be less than or equal to %{count}" |
|
126 | 126 | odd: "must be odd" |
|
127 | 127 | even: "must be even" |
|
128 | 128 | greater_than_start_date: "must be greater than start date" |
|
129 | 129 | not_same_project: "doesn't belong to the same project" |
|
130 | 130 | circular_dependency: "This relation would create a circular dependency" |
|
131 | 131 | cant_link_an_issue_with_a_descendant: "An issue cannot be linked to one of its subtasks" |
|
132 | 132 | |
|
133 | 133 | actionview_instancetag_blank_option: Please select |
|
134 | 134 | |
|
135 | 135 | general_text_No: 'No' |
|
136 | 136 | general_text_Yes: 'Yes' |
|
137 | 137 | general_text_no: 'no' |
|
138 | 138 | general_text_yes: 'yes' |
|
139 | 139 | general_lang_name: 'English' |
|
140 | 140 | general_csv_separator: ',' |
|
141 | 141 | general_csv_decimal_separator: '.' |
|
142 | 142 | general_csv_encoding: ISO-8859-1 |
|
143 | 143 | general_pdf_encoding: UTF-8 |
|
144 | 144 | general_first_day_of_week: '7' |
|
145 | 145 | |
|
146 | 146 | notice_account_updated: Account was successfully updated. |
|
147 | 147 | notice_account_invalid_creditentials: Invalid user or password |
|
148 | 148 | notice_account_password_updated: Password was successfully updated. |
|
149 | 149 | notice_account_wrong_password: Wrong password |
|
150 | 150 | notice_account_register_done: Account was successfully created. To activate your account, click on the link that was emailed to you. |
|
151 | 151 | notice_account_unknown_email: Unknown user. |
|
152 | 152 | notice_can_t_change_password: This account uses an external authentication source. Impossible to change the password. |
|
153 | 153 | notice_account_lost_email_sent: An email with instructions to choose a new password has been sent to you. |
|
154 | 154 | notice_account_activated: Your account has been activated. You can now log in. |
|
155 | 155 | notice_successful_create: Successful creation. |
|
156 | 156 | notice_successful_update: Successful update. |
|
157 | 157 | notice_successful_delete: Successful deletion. |
|
158 | 158 | notice_successful_connection: Successful connection. |
|
159 | 159 | notice_file_not_found: The page you were trying to access doesn't exist or has been removed. |
|
160 | 160 | notice_locking_conflict: Data has been updated by another user. |
|
161 | 161 | notice_not_authorized: You are not authorized to access this page. |
|
162 | 162 | notice_not_authorized_archived_project: The project you're trying to access has been archived. |
|
163 | 163 | notice_email_sent: "An email was sent to %{value}" |
|
164 | 164 | notice_email_error: "An error occurred while sending mail (%{value})" |
|
165 | 165 | notice_feeds_access_key_reseted: Your RSS access key was reset. |
|
166 | 166 | notice_api_access_key_reseted: Your API access key was reset. |
|
167 | 167 | notice_failed_to_save_issues: "Failed to save %{count} issue(s) on %{total} selected: %{ids}." |
|
168 | 168 | notice_failed_to_save_time_entries: "Failed to save %{count} time entrie(s) on %{total} selected: %{ids}." |
|
169 | 169 | notice_failed_to_save_members: "Failed to save member(s): %{errors}." |
|
170 | 170 | notice_no_issue_selected: "No issue is selected! Please, check the issues you want to edit." |
|
171 | 171 | notice_account_pending: "Your account was created and is now pending administrator approval." |
|
172 | 172 | notice_default_data_loaded: Default configuration successfully loaded. |
|
173 | 173 | notice_unable_delete_version: Unable to delete version. |
|
174 | 174 | notice_unable_delete_time_entry: Unable to delete time log entry. |
|
175 | 175 | notice_issue_done_ratios_updated: Issue done ratios updated. |
|
176 | 176 | notice_gantt_chart_truncated: "The chart was truncated because it exceeds the maximum number of items that can be displayed (%{max})" |
|
177 | 177 | notice_issue_successful_create: "Issue %{id} created." |
|
178 | 178 | notice_issue_update_conflict: "The issue has been updated by an other user while you were editing it." |
|
179 | 179 | notice_account_deleted: "Your account has been permanently deleted." |
|
180 | 180 | notice_user_successful_create: "User %{id} created." |
|
181 | 181 | |
|
182 | 182 | error_can_t_load_default_data: "Default configuration could not be loaded: %{value}" |
|
183 | 183 | error_scm_not_found: "The entry or revision was not found in the repository." |
|
184 | 184 | error_scm_command_failed: "An error occurred when trying to access the repository: %{value}" |
|
185 | 185 | error_scm_annotate: "The entry does not exist or cannot be annotated." |
|
186 | 186 | error_scm_annotate_big_text_file: "The entry cannot be annotated, as it exceeds the maximum text file size." |
|
187 | 187 | error_issue_not_found_in_project: 'The issue was not found or does not belong to this project' |
|
188 | 188 | error_no_tracker_in_project: 'No tracker is associated to this project. Please check the Project settings.' |
|
189 | 189 | error_no_default_issue_status: 'No default issue status is defined. Please check your configuration (Go to "Administration -> Issue statuses").' |
|
190 | 190 | error_can_not_delete_custom_field: Unable to delete custom field |
|
191 | 191 | error_can_not_delete_tracker: "This tracker contains issues and cannot be deleted." |
|
192 | 192 | error_can_not_remove_role: "This role is in use and cannot be deleted." |
|
193 | 193 | error_can_not_reopen_issue_on_closed_version: 'An issue assigned to a closed version cannot be reopened' |
|
194 | 194 | error_can_not_archive_project: This project cannot be archived |
|
195 | 195 | error_issue_done_ratios_not_updated: "Issue done ratios not updated." |
|
196 | 196 | error_workflow_copy_source: 'Please select a source tracker or role' |
|
197 | 197 | error_workflow_copy_target: 'Please select target tracker(s) and role(s)' |
|
198 | 198 | error_unable_delete_issue_status: 'Unable to delete issue status' |
|
199 | 199 | error_unable_to_connect: "Unable to connect (%{value})" |
|
200 | 200 | error_attachment_too_big: "This file cannot be uploaded because it exceeds the maximum allowed file size (%{max_size})" |
|
201 | 201 | error_session_expired: "Your session has expired. Please login again." |
|
202 | 202 | warning_attachments_not_saved: "%{count} file(s) could not be saved." |
|
203 | 203 | |
|
204 | 204 | mail_subject_lost_password: "Your %{value} password" |
|
205 | 205 | mail_body_lost_password: 'To change your password, click on the following link:' |
|
206 | 206 | mail_subject_register: "Your %{value} account activation" |
|
207 | 207 | mail_body_register: 'To activate your account, click on the following link:' |
|
208 | 208 | mail_body_account_information_external: "You can use your %{value} account to log in." |
|
209 | 209 | mail_body_account_information: Your account information |
|
210 | 210 | mail_subject_account_activation_request: "%{value} account activation request" |
|
211 | 211 | mail_body_account_activation_request: "A new user (%{value}) has registered. The account is pending your approval:" |
|
212 | 212 | mail_subject_reminder: "%{count} issue(s) due in the next %{days} days" |
|
213 | 213 | mail_body_reminder: "%{count} issue(s) that are assigned to you are due in the next %{days} days:" |
|
214 | 214 | mail_subject_wiki_content_added: "'%{id}' wiki page has been added" |
|
215 | 215 | mail_body_wiki_content_added: "The '%{id}' wiki page has been added by %{author}." |
|
216 | 216 | mail_subject_wiki_content_updated: "'%{id}' wiki page has been updated" |
|
217 | 217 | mail_body_wiki_content_updated: "The '%{id}' wiki page has been updated by %{author}." |
|
218 | 218 | |
|
219 | 219 | gui_validation_error: 1 error |
|
220 | 220 | gui_validation_error_plural: "%{count} errors" |
|
221 | 221 | |
|
222 | 222 | field_name: Name |
|
223 | 223 | field_description: Description |
|
224 | 224 | field_summary: Summary |
|
225 | 225 | field_is_required: Required |
|
226 | 226 | field_firstname: First name |
|
227 | 227 | field_lastname: Last name |
|
228 | 228 | field_mail: Email |
|
229 | 229 | field_filename: File |
|
230 | 230 | field_filesize: Size |
|
231 | 231 | field_downloads: Downloads |
|
232 | 232 | field_author: Author |
|
233 | 233 | field_created_on: Created |
|
234 | 234 | field_updated_on: Updated |
|
235 | 235 | field_field_format: Format |
|
236 | 236 | field_is_for_all: For all projects |
|
237 | 237 | field_possible_values: Possible values |
|
238 | 238 | field_regexp: Regular expression |
|
239 | 239 | field_min_length: Minimum length |
|
240 | 240 | field_max_length: Maximum length |
|
241 | 241 | field_value: Value |
|
242 | 242 | field_category: Category |
|
243 | 243 | field_title: Title |
|
244 | 244 | field_project: Project |
|
245 | 245 | field_issue: Issue |
|
246 | 246 | field_status: Status |
|
247 | 247 | field_notes: Notes |
|
248 | 248 | field_is_closed: Issue closed |
|
249 | 249 | field_is_default: Default value |
|
250 | 250 | field_tracker: Tracker |
|
251 | 251 | field_subject: Subject |
|
252 | 252 | field_due_date: Due date |
|
253 | 253 | field_assigned_to: Assignee |
|
254 | 254 | field_priority: Priority |
|
255 | 255 | field_fixed_version: Target version |
|
256 | 256 | field_user: User |
|
257 | 257 | field_principal: Principal |
|
258 | 258 | field_role: Role |
|
259 | 259 | field_homepage: Homepage |
|
260 | 260 | field_is_public: Public |
|
261 | 261 | field_parent: Subproject of |
|
262 | 262 | field_is_in_roadmap: Issues displayed in roadmap |
|
263 | 263 | field_login: Login |
|
264 | 264 | field_mail_notification: Email notifications |
|
265 | 265 | field_admin: Administrator |
|
266 | 266 | field_last_login_on: Last connection |
|
267 | 267 | field_language: Language |
|
268 | 268 | field_effective_date: Date |
|
269 | 269 | field_password: Password |
|
270 | 270 | field_new_password: New password |
|
271 | 271 | field_password_confirmation: Confirmation |
|
272 | 272 | field_version: Version |
|
273 | 273 | field_type: Type |
|
274 | 274 | field_host: Host |
|
275 | 275 | field_port: Port |
|
276 | 276 | field_account: Account |
|
277 | 277 | field_base_dn: Base DN |
|
278 | 278 | field_attr_login: Login attribute |
|
279 | 279 | field_attr_firstname: Firstname attribute |
|
280 | 280 | field_attr_lastname: Lastname attribute |
|
281 | 281 | field_attr_mail: Email attribute |
|
282 | 282 | field_onthefly: On-the-fly user creation |
|
283 | 283 | field_start_date: Start date |
|
284 | 284 | field_done_ratio: "% Done" |
|
285 | 285 | field_auth_source: Authentication mode |
|
286 | 286 | field_hide_mail: Hide my email address |
|
287 | 287 | field_comments: Comment |
|
288 | 288 | field_url: URL |
|
289 | 289 | field_start_page: Start page |
|
290 | 290 | field_subproject: Subproject |
|
291 | 291 | field_hours: Hours |
|
292 | 292 | field_activity: Activity |
|
293 | 293 | field_spent_on: Date |
|
294 | 294 | field_identifier: Identifier |
|
295 | 295 | field_is_filter: Used as a filter |
|
296 | 296 | field_issue_to: Related issue |
|
297 | 297 | field_delay: Delay |
|
298 | 298 | field_assignable: Issues can be assigned to this role |
|
299 | 299 | field_redirect_existing_links: Redirect existing links |
|
300 | 300 | field_estimated_hours: Estimated time |
|
301 | 301 | field_column_names: Columns |
|
302 | 302 | field_time_entries: Log time |
|
303 | 303 | field_time_zone: Time zone |
|
304 | 304 | field_searchable: Searchable |
|
305 | 305 | field_default_value: Default value |
|
306 | 306 | field_comments_sorting: Display comments |
|
307 | 307 | field_parent_title: Parent page |
|
308 | 308 | field_editable: Editable |
|
309 | 309 | field_watcher: Watcher |
|
310 | 310 | field_identity_url: OpenID URL |
|
311 | 311 | field_content: Content |
|
312 | 312 | field_group_by: Group results by |
|
313 | 313 | field_sharing: Sharing |
|
314 | 314 | field_parent_issue: Parent task |
|
315 | 315 | field_member_of_group: "Assignee's group" |
|
316 | 316 | field_assigned_to_role: "Assignee's role" |
|
317 | 317 | field_text: Text field |
|
318 | 318 | field_visible: Visible |
|
319 | 319 | field_warn_on_leaving_unsaved: "Warn me when leaving a page with unsaved text" |
|
320 | 320 | field_issues_visibility: Issues visibility |
|
321 | 321 | field_is_private: Private |
|
322 | 322 | field_commit_logs_encoding: Commit messages encoding |
|
323 | 323 | field_scm_path_encoding: Path encoding |
|
324 | 324 | field_path_to_repository: Path to repository |
|
325 | 325 | field_root_directory: Root directory |
|
326 | 326 | field_cvsroot: CVSROOT |
|
327 | 327 | field_cvs_module: Module |
|
328 | 328 | field_repository_is_default: Main repository |
|
329 | 329 | field_multiple: Multiple values |
|
330 | 330 | field_auth_source_ldap_filter: LDAP filter |
|
331 | 331 | field_core_fields: Standard fields |
|
332 | 332 | field_timeout: "Timeout (in seconds)" |
|
333 | 333 | field_board_parent: Parent forum |
|
334 | 334 | |
|
335 | 335 | setting_app_title: Application title |
|
336 | 336 | setting_app_subtitle: Application subtitle |
|
337 | 337 | setting_welcome_text: Welcome text |
|
338 | 338 | setting_default_language: Default language |
|
339 | 339 | setting_login_required: Authentication required |
|
340 | 340 | setting_self_registration: Self-registration |
|
341 | 341 | setting_attachment_max_size: Maximum attachment size |
|
342 | 342 | setting_issues_export_limit: Issues export limit |
|
343 | 343 | setting_mail_from: Emission email address |
|
344 | 344 | setting_bcc_recipients: Blind carbon copy recipients (bcc) |
|
345 | 345 | setting_plain_text_mail: Plain text mail (no HTML) |
|
346 | 346 | setting_host_name: Host name and path |
|
347 | 347 | setting_text_formatting: Text formatting |
|
348 | 348 | setting_wiki_compression: Wiki history compression |
|
349 | 349 | setting_feeds_limit: Maximum number of items in Atom feeds |
|
350 | 350 | setting_default_projects_public: New projects are public by default |
|
351 | 351 | setting_autofetch_changesets: Fetch commits automatically |
|
352 | 352 | setting_sys_api_enabled: Enable WS for repository management |
|
353 | 353 | setting_commit_ref_keywords: Referencing keywords |
|
354 | 354 | setting_commit_fix_keywords: Fixing keywords |
|
355 | 355 | setting_autologin: Autologin |
|
356 | 356 | setting_date_format: Date format |
|
357 | 357 | setting_time_format: Time format |
|
358 | 358 | setting_cross_project_issue_relations: Allow cross-project issue relations |
|
359 | 359 | setting_issue_list_default_columns: Default columns displayed on the issue list |
|
360 | 360 | setting_repositories_encodings: Attachments and repositories encodings |
|
361 | 361 | setting_emails_header: Emails header |
|
362 | 362 | setting_emails_footer: Emails footer |
|
363 | 363 | setting_protocol: Protocol |
|
364 | 364 | setting_per_page_options: Objects per page options |
|
365 | 365 | setting_user_format: Users display format |
|
366 | 366 | setting_activity_days_default: Days displayed on project activity |
|
367 | 367 | setting_display_subprojects_issues: Display subprojects issues on main projects by default |
|
368 | 368 | setting_enabled_scm: Enabled SCM |
|
369 | 369 | setting_mail_handler_body_delimiters: "Truncate emails after one of these lines" |
|
370 | 370 | setting_mail_handler_api_enabled: Enable WS for incoming emails |
|
371 | 371 | setting_mail_handler_api_key: API key |
|
372 | 372 | setting_sequential_project_identifiers: Generate sequential project identifiers |
|
373 | 373 | setting_gravatar_enabled: Use Gravatar user icons |
|
374 | 374 | setting_gravatar_default: Default Gravatar image |
|
375 | 375 | setting_diff_max_lines_displayed: Maximum number of diff lines displayed |
|
376 | 376 | setting_file_max_size_displayed: Maximum size of text files displayed inline |
|
377 | 377 | setting_repository_log_display_limit: Maximum number of revisions displayed on file log |
|
378 | 378 | setting_openid: Allow OpenID login and registration |
|
379 | 379 | setting_password_min_length: Minimum password length |
|
380 | 380 | setting_new_project_user_role_id: Role given to a non-admin user who creates a project |
|
381 | 381 | setting_default_projects_modules: Default enabled modules for new projects |
|
382 | 382 | setting_issue_done_ratio: Calculate the issue done ratio with |
|
383 | 383 | setting_issue_done_ratio_issue_field: Use the issue field |
|
384 | 384 | setting_issue_done_ratio_issue_status: Use the issue status |
|
385 | 385 | setting_start_of_week: Start calendars on |
|
386 | 386 | setting_rest_api_enabled: Enable REST web service |
|
387 | 387 | setting_cache_formatted_text: Cache formatted text |
|
388 | 388 | setting_default_notification_option: Default notification option |
|
389 | 389 | setting_commit_logtime_enabled: Enable time logging |
|
390 | 390 | setting_commit_logtime_activity_id: Activity for logged time |
|
391 | 391 | setting_gantt_items_limit: Maximum number of items displayed on the gantt chart |
|
392 | 392 | setting_issue_group_assignment: Allow issue assignment to groups |
|
393 | 393 | setting_default_issue_start_date_to_creation_date: Use current date as start date for new issues |
|
394 | 394 | setting_commit_cross_project_ref: Allow issues of all the other projects to be referenced and fixed |
|
395 | 395 | setting_unsubscribe: Allow users to delete their own account |
|
396 | 396 | setting_session_lifetime: Session maximum lifetime |
|
397 | 397 | setting_session_timeout: Session inactivity timeout |
|
398 | 398 | setting_thumbnails_enabled: Display attachment thumbnails |
|
399 | 399 | setting_thumbnails_size: Thumbnails size (in pixels) |
|
400 | 400 | |
|
401 | 401 | permission_add_project: Create project |
|
402 | 402 | permission_add_subprojects: Create subprojects |
|
403 | 403 | permission_edit_project: Edit project |
|
404 | 404 | permission_close_project: Close / reopen the project |
|
405 | 405 | permission_select_project_modules: Select project modules |
|
406 | 406 | permission_manage_members: Manage members |
|
407 | 407 | permission_manage_project_activities: Manage project activities |
|
408 | 408 | permission_manage_versions: Manage versions |
|
409 | 409 | permission_manage_categories: Manage issue categories |
|
410 | 410 | permission_view_issues: View Issues |
|
411 | 411 | permission_add_issues: Add issues |
|
412 | 412 | permission_edit_issues: Edit issues |
|
413 | 413 | permission_manage_issue_relations: Manage issue relations |
|
414 | 414 | permission_set_issues_private: Set issues public or private |
|
415 | 415 | permission_set_own_issues_private: Set own issues public or private |
|
416 | 416 | permission_add_issue_notes: Add notes |
|
417 | 417 | permission_edit_issue_notes: Edit notes |
|
418 | 418 | permission_edit_own_issue_notes: Edit own notes |
|
419 | 419 | permission_move_issues: Move issues |
|
420 | 420 | permission_delete_issues: Delete issues |
|
421 | 421 | permission_manage_public_queries: Manage public queries |
|
422 | 422 | permission_save_queries: Save queries |
|
423 | 423 | permission_view_gantt: View gantt chart |
|
424 | 424 | permission_view_calendar: View calendar |
|
425 | 425 | permission_view_issue_watchers: View watchers list |
|
426 | 426 | permission_add_issue_watchers: Add watchers |
|
427 | 427 | permission_delete_issue_watchers: Delete watchers |
|
428 | 428 | permission_log_time: Log spent time |
|
429 | 429 | permission_view_time_entries: View spent time |
|
430 | 430 | permission_edit_time_entries: Edit time logs |
|
431 | 431 | permission_edit_own_time_entries: Edit own time logs |
|
432 | 432 | permission_manage_news: Manage news |
|
433 | 433 | permission_comment_news: Comment news |
|
434 | 434 | permission_manage_documents: Manage documents |
|
435 | 435 | permission_view_documents: View documents |
|
436 | 436 | permission_manage_files: Manage files |
|
437 | 437 | permission_view_files: View files |
|
438 | 438 | permission_manage_wiki: Manage wiki |
|
439 | 439 | permission_rename_wiki_pages: Rename wiki pages |
|
440 | 440 | permission_delete_wiki_pages: Delete wiki pages |
|
441 | 441 | permission_view_wiki_pages: View wiki |
|
442 | 442 | permission_view_wiki_edits: View wiki history |
|
443 | 443 | permission_edit_wiki_pages: Edit wiki pages |
|
444 | 444 | permission_delete_wiki_pages_attachments: Delete attachments |
|
445 | 445 | permission_protect_wiki_pages: Protect wiki pages |
|
446 | 446 | permission_manage_repository: Manage repository |
|
447 | 447 | permission_browse_repository: Browse repository |
|
448 | 448 | permission_view_changesets: View changesets |
|
449 | 449 | permission_commit_access: Commit access |
|
450 | 450 | permission_manage_boards: Manage forums |
|
451 | 451 | permission_view_messages: View messages |
|
452 | 452 | permission_add_messages: Post messages |
|
453 | 453 | permission_edit_messages: Edit messages |
|
454 | 454 | permission_edit_own_messages: Edit own messages |
|
455 | 455 | permission_delete_messages: Delete messages |
|
456 | 456 | permission_delete_own_messages: Delete own messages |
|
457 | 457 | permission_export_wiki_pages: Export wiki pages |
|
458 | 458 | permission_manage_subtasks: Manage subtasks |
|
459 | 459 | permission_manage_related_issues: Manage related issues |
|
460 | 460 | |
|
461 | 461 | project_module_issue_tracking: Issue tracking |
|
462 | 462 | project_module_time_tracking: Time tracking |
|
463 | 463 | project_module_news: News |
|
464 | 464 | project_module_documents: Documents |
|
465 | 465 | project_module_files: Files |
|
466 | 466 | project_module_wiki: Wiki |
|
467 | 467 | project_module_repository: Repository |
|
468 | 468 | project_module_boards: Forums |
|
469 | 469 | project_module_calendar: Calendar |
|
470 | 470 | project_module_gantt: Gantt |
|
471 | 471 | |
|
472 | 472 | label_user: User |
|
473 | 473 | label_user_plural: Users |
|
474 | 474 | label_user_new: New user |
|
475 | 475 | label_user_anonymous: Anonymous |
|
476 | 476 | label_project: Project |
|
477 | 477 | label_project_new: New project |
|
478 | 478 | label_project_plural: Projects |
|
479 | 479 | label_x_projects: |
|
480 | 480 | zero: no projects |
|
481 | 481 | one: 1 project |
|
482 | 482 | other: "%{count} projects" |
|
483 | 483 | label_project_all: All Projects |
|
484 | 484 | label_project_latest: Latest projects |
|
485 | 485 | label_issue: Issue |
|
486 | 486 | label_issue_new: New issue |
|
487 | 487 | label_issue_plural: Issues |
|
488 | 488 | label_issue_view_all: View all issues |
|
489 | 489 | label_issues_by: "Issues by %{value}" |
|
490 | 490 | label_issue_added: Issue added |
|
491 | 491 | label_issue_updated: Issue updated |
|
492 | 492 | label_issue_note_added: Note added |
|
493 | 493 | label_issue_status_updated: Status updated |
|
494 | 494 | label_issue_priority_updated: Priority updated |
|
495 | 495 | label_document: Document |
|
496 | 496 | label_document_new: New document |
|
497 | 497 | label_document_plural: Documents |
|
498 | 498 | label_document_added: Document added |
|
499 | 499 | label_role: Role |
|
500 | 500 | label_role_plural: Roles |
|
501 | 501 | label_role_new: New role |
|
502 | 502 | label_role_and_permissions: Roles and permissions |
|
503 | 503 | label_role_anonymous: Anonymous |
|
504 | 504 | label_role_non_member: Non member |
|
505 | 505 | label_member: Member |
|
506 | 506 | label_member_new: New member |
|
507 | 507 | label_member_plural: Members |
|
508 | 508 | label_tracker: Tracker |
|
509 | 509 | label_tracker_plural: Trackers |
|
510 | 510 | label_tracker_new: New tracker |
|
511 | 511 | label_workflow: Workflow |
|
512 | 512 | label_issue_status: Issue status |
|
513 | 513 | label_issue_status_plural: Issue statuses |
|
514 | 514 | label_issue_status_new: New status |
|
515 | 515 | label_issue_category: Issue category |
|
516 | 516 | label_issue_category_plural: Issue categories |
|
517 | 517 | label_issue_category_new: New category |
|
518 | 518 | label_custom_field: Custom field |
|
519 | 519 | label_custom_field_plural: Custom fields |
|
520 | 520 | label_custom_field_new: New custom field |
|
521 | 521 | label_enumerations: Enumerations |
|
522 | 522 | label_enumeration_new: New value |
|
523 | 523 | label_information: Information |
|
524 | 524 | label_information_plural: Information |
|
525 | 525 | label_please_login: Please log in |
|
526 | 526 | label_register: Register |
|
527 | 527 | label_login_with_open_id_option: or login with OpenID |
|
528 | 528 | label_password_lost: Lost password |
|
529 | 529 | label_home: Home |
|
530 | 530 | label_my_page: My page |
|
531 | 531 | label_my_account: My account |
|
532 | 532 | label_my_projects: My projects |
|
533 | 533 | label_my_page_block: My page block |
|
534 | 534 | label_administration: Administration |
|
535 | 535 | label_login: Sign in |
|
536 | 536 | label_logout: Sign out |
|
537 | 537 | label_help: Help |
|
538 | 538 | label_reported_issues: Reported issues |
|
539 | 539 | label_assigned_to_me_issues: Issues assigned to me |
|
540 | 540 | label_last_login: Last connection |
|
541 | 541 | label_registered_on: Registered on |
|
542 | 542 | label_activity: Activity |
|
543 | 543 | label_overall_activity: Overall activity |
|
544 | 544 | label_user_activity: "%{value}'s activity" |
|
545 | 545 | label_new: New |
|
546 | 546 | label_logged_as: Logged in as |
|
547 | 547 | label_environment: Environment |
|
548 | 548 | label_authentication: Authentication |
|
549 | 549 | label_auth_source: Authentication mode |
|
550 | 550 | label_auth_source_new: New authentication mode |
|
551 | 551 | label_auth_source_plural: Authentication modes |
|
552 | 552 | label_subproject_plural: Subprojects |
|
553 | 553 | label_subproject_new: New subproject |
|
554 | 554 | label_and_its_subprojects: "%{value} and its subprojects" |
|
555 | 555 | label_min_max_length: Min - Max length |
|
556 | 556 | label_list: List |
|
557 | 557 | label_date: Date |
|
558 | 558 | label_integer: Integer |
|
559 | 559 | label_float: Float |
|
560 | 560 | label_boolean: Boolean |
|
561 | 561 | label_string: Text |
|
562 | 562 | label_text: Long text |
|
563 | 563 | label_attribute: Attribute |
|
564 | 564 | label_attribute_plural: Attributes |
|
565 | 565 | label_download: "%{count} Download" |
|
566 | 566 | label_download_plural: "%{count} Downloads" |
|
567 | 567 | label_no_data: No data to display |
|
568 | 568 | label_change_status: Change status |
|
569 | 569 | label_history: History |
|
570 | 570 | label_attachment: File |
|
571 | 571 | label_attachment_new: New file |
|
572 | 572 | label_attachment_delete: Delete file |
|
573 | 573 | label_attachment_plural: Files |
|
574 | 574 | label_file_added: File added |
|
575 | 575 | label_report: Report |
|
576 | 576 | label_report_plural: Reports |
|
577 | 577 | label_news: News |
|
578 | 578 | label_news_new: Add news |
|
579 | 579 | label_news_plural: News |
|
580 | 580 | label_news_latest: Latest news |
|
581 | 581 | label_news_view_all: View all news |
|
582 | 582 | label_news_added: News added |
|
583 | 583 | label_news_comment_added: Comment added to a news |
|
584 | 584 | label_settings: Settings |
|
585 | 585 | label_overview: Overview |
|
586 | 586 | label_version: Version |
|
587 | 587 | label_version_new: New version |
|
588 | 588 | label_version_plural: Versions |
|
589 | 589 | label_close_versions: Close completed versions |
|
590 | 590 | label_confirmation: Confirmation |
|
591 | 591 | label_export_to: 'Also available in:' |
|
592 | 592 | label_read: Read... |
|
593 | 593 | label_public_projects: Public projects |
|
594 | 594 | label_open_issues: open |
|
595 | 595 | label_open_issues_plural: open |
|
596 | 596 | label_closed_issues: closed |
|
597 | 597 | label_closed_issues_plural: closed |
|
598 | 598 | label_x_open_issues_abbr_on_total: |
|
599 | 599 | zero: 0 open / %{total} |
|
600 | 600 | one: 1 open / %{total} |
|
601 | 601 | other: "%{count} open / %{total}" |
|
602 | 602 | label_x_open_issues_abbr: |
|
603 | 603 | zero: 0 open |
|
604 | 604 | one: 1 open |
|
605 | 605 | other: "%{count} open" |
|
606 | 606 | label_x_closed_issues_abbr: |
|
607 | 607 | zero: 0 closed |
|
608 | 608 | one: 1 closed |
|
609 | 609 | other: "%{count} closed" |
|
610 | 610 | label_x_issues: |
|
611 | 611 | zero: 0 issues |
|
612 | 612 | one: 1 issue |
|
613 | 613 | other: "%{count} issues" |
|
614 | 614 | label_total: Total |
|
615 | 615 | label_permissions: Permissions |
|
616 | 616 | label_current_status: Current status |
|
617 | 617 | label_new_statuses_allowed: New statuses allowed |
|
618 | 618 | label_all: all |
|
619 | 619 | label_none: none |
|
620 | 620 | label_nobody: nobody |
|
621 | 621 | label_next: Next |
|
622 | 622 | label_previous: Previous |
|
623 | 623 | label_used_by: Used by |
|
624 | 624 | label_details: Details |
|
625 | 625 | label_add_note: Add a note |
|
626 | 626 | label_per_page: Per page |
|
627 | 627 | label_calendar: Calendar |
|
628 | 628 | label_months_from: months from |
|
629 | 629 | label_gantt: Gantt |
|
630 | 630 | label_internal: Internal |
|
631 | 631 | label_last_changes: "last %{count} changes" |
|
632 | 632 | label_change_view_all: View all changes |
|
633 | 633 | label_personalize_page: Personalize this page |
|
634 | 634 | label_comment: Comment |
|
635 | 635 | label_comment_plural: Comments |
|
636 | 636 | label_x_comments: |
|
637 | 637 | zero: no comments |
|
638 | 638 | one: 1 comment |
|
639 | 639 | other: "%{count} comments" |
|
640 | 640 | label_comment_add: Add a comment |
|
641 | 641 | label_comment_added: Comment added |
|
642 | 642 | label_comment_delete: Delete comments |
|
643 | 643 | label_query: Custom query |
|
644 | 644 | label_query_plural: Custom queries |
|
645 | 645 | label_query_new: New query |
|
646 | 646 | label_my_queries: My custom queries |
|
647 | 647 | label_filter_add: Add filter |
|
648 | 648 | label_filter_plural: Filters |
|
649 | 649 | label_equals: is |
|
650 | 650 | label_not_equals: is not |
|
651 | 651 | label_in_less_than: in less than |
|
652 | 652 | label_in_more_than: in more than |
|
653 | 653 | label_greater_or_equal: '>=' |
|
654 | 654 | label_less_or_equal: '<=' |
|
655 | 655 | label_between: between |
|
656 | 656 | label_in: in |
|
657 | 657 | label_today: today |
|
658 | 658 | label_all_time: all time |
|
659 | 659 | label_yesterday: yesterday |
|
660 | 660 | label_this_week: this week |
|
661 | 661 | label_last_week: last week |
|
662 | 662 | label_last_n_days: "last %{count} days" |
|
663 | 663 | label_this_month: this month |
|
664 | 664 | label_last_month: last month |
|
665 | 665 | label_this_year: this year |
|
666 | 666 | label_date_range: Date range |
|
667 | 667 | label_less_than_ago: less than days ago |
|
668 | 668 | label_more_than_ago: more than days ago |
|
669 | 669 | label_ago: days ago |
|
670 | 670 | label_contains: contains |
|
671 | 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 | 674 | label_day_plural: days |
|
673 | 675 | label_repository: Repository |
|
674 | 676 | label_repository_new: New repository |
|
675 | 677 | label_repository_plural: Repositories |
|
676 | 678 | label_browse: Browse |
|
677 | 679 | label_modification: "%{count} change" |
|
678 | 680 | label_modification_plural: "%{count} changes" |
|
679 | 681 | label_branch: Branch |
|
680 | 682 | label_tag: Tag |
|
681 | 683 | label_revision: Revision |
|
682 | 684 | label_revision_plural: Revisions |
|
683 | 685 | label_revision_id: "Revision %{value}" |
|
684 | 686 | label_associated_revisions: Associated revisions |
|
685 | 687 | label_added: added |
|
686 | 688 | label_modified: modified |
|
687 | 689 | label_copied: copied |
|
688 | 690 | label_renamed: renamed |
|
689 | 691 | label_deleted: deleted |
|
690 | 692 | label_latest_revision: Latest revision |
|
691 | 693 | label_latest_revision_plural: Latest revisions |
|
692 | 694 | label_view_revisions: View revisions |
|
693 | 695 | label_view_all_revisions: View all revisions |
|
694 | 696 | label_max_size: Maximum size |
|
695 | 697 | label_sort_highest: Move to top |
|
696 | 698 | label_sort_higher: Move up |
|
697 | 699 | label_sort_lower: Move down |
|
698 | 700 | label_sort_lowest: Move to bottom |
|
699 | 701 | label_roadmap: Roadmap |
|
700 | 702 | label_roadmap_due_in: "Due in %{value}" |
|
701 | 703 | label_roadmap_overdue: "%{value} late" |
|
702 | 704 | label_roadmap_no_issues: No issues for this version |
|
703 | 705 | label_search: Search |
|
704 | 706 | label_result_plural: Results |
|
705 | 707 | label_all_words: All words |
|
706 | 708 | label_wiki: Wiki |
|
707 | 709 | label_wiki_edit: Wiki edit |
|
708 | 710 | label_wiki_edit_plural: Wiki edits |
|
709 | 711 | label_wiki_page: Wiki page |
|
710 | 712 | label_wiki_page_plural: Wiki pages |
|
711 | 713 | label_index_by_title: Index by title |
|
712 | 714 | label_index_by_date: Index by date |
|
713 | 715 | label_current_version: Current version |
|
714 | 716 | label_preview: Preview |
|
715 | 717 | label_feed_plural: Feeds |
|
716 | 718 | label_changes_details: Details of all changes |
|
717 | 719 | label_issue_tracking: Issue tracking |
|
718 | 720 | label_spent_time: Spent time |
|
719 | 721 | label_overall_spent_time: Overall spent time |
|
720 | 722 | label_f_hour: "%{value} hour" |
|
721 | 723 | label_f_hour_plural: "%{value} hours" |
|
722 | 724 | label_time_tracking: Time tracking |
|
723 | 725 | label_change_plural: Changes |
|
724 | 726 | label_statistics: Statistics |
|
725 | 727 | label_commits_per_month: Commits per month |
|
726 | 728 | label_commits_per_author: Commits per author |
|
727 | 729 | label_diff: diff |
|
728 | 730 | label_view_diff: View differences |
|
729 | 731 | label_diff_inline: inline |
|
730 | 732 | label_diff_side_by_side: side by side |
|
731 | 733 | label_options: Options |
|
732 | 734 | label_copy_workflow_from: Copy workflow from |
|
733 | 735 | label_permissions_report: Permissions report |
|
734 | 736 | label_watched_issues: Watched issues |
|
735 | 737 | label_related_issues: Related issues |
|
736 | 738 | label_applied_status: Applied status |
|
737 | 739 | label_loading: Loading... |
|
738 | 740 | label_relation_new: New relation |
|
739 | 741 | label_relation_delete: Delete relation |
|
740 |
label_relates_to: |
|
|
741 |
label_duplicates: |
|
|
742 |
label_duplicated_by: |
|
|
743 |
label_blocks: |
|
|
744 |
label_blocked_by: |
|
|
745 |
label_precedes: |
|
|
746 |
label_follows: |
|
|
747 |
label_copied_to: |
|
|
748 |
label_copied_from: |
|
|
742 | label_relates_to: Related to | |
|
743 | label_duplicates: Duplicates | |
|
744 | label_duplicated_by: Duplicated by | |
|
745 | label_blocks: Blocks | |
|
746 | label_blocked_by: Blocked by | |
|
747 | label_precedes: Precedes | |
|
748 | label_follows: Follows | |
|
749 | label_copied_to: Copied to | |
|
750 | label_copied_from: Copied from | |
|
749 | 751 | label_end_to_start: end to start |
|
750 | 752 | label_end_to_end: end to end |
|
751 | 753 | label_start_to_start: start to start |
|
752 | 754 | label_start_to_end: start to end |
|
753 | 755 | label_stay_logged_in: Stay logged in |
|
754 | 756 | label_disabled: disabled |
|
755 | 757 | label_show_completed_versions: Show completed versions |
|
756 | 758 | label_me: me |
|
757 | 759 | label_board: Forum |
|
758 | 760 | label_board_new: New forum |
|
759 | 761 | label_board_plural: Forums |
|
760 | 762 | label_board_locked: Locked |
|
761 | 763 | label_board_sticky: Sticky |
|
762 | 764 | label_topic_plural: Topics |
|
763 | 765 | label_message_plural: Messages |
|
764 | 766 | label_message_last: Last message |
|
765 | 767 | label_message_new: New message |
|
766 | 768 | label_message_posted: Message added |
|
767 | 769 | label_reply_plural: Replies |
|
768 | 770 | label_send_information: Send account information to the user |
|
769 | 771 | label_year: Year |
|
770 | 772 | label_month: Month |
|
771 | 773 | label_week: Week |
|
772 | 774 | label_date_from: From |
|
773 | 775 | label_date_to: To |
|
774 | 776 | label_language_based: Based on user's language |
|
775 | 777 | label_sort_by: "Sort by %{value}" |
|
776 | 778 | label_send_test_email: Send a test email |
|
777 | 779 | label_feeds_access_key: RSS access key |
|
778 | 780 | label_missing_feeds_access_key: Missing a RSS access key |
|
779 | 781 | label_feeds_access_key_created_on: "RSS access key created %{value} ago" |
|
780 | 782 | label_module_plural: Modules |
|
781 | 783 | label_added_time_by: "Added by %{author} %{age} ago" |
|
782 | 784 | label_updated_time_by: "Updated by %{author} %{age} ago" |
|
783 | 785 | label_updated_time: "Updated %{value} ago" |
|
784 | 786 | label_jump_to_a_project: Jump to a project... |
|
785 | 787 | label_file_plural: Files |
|
786 | 788 | label_changeset_plural: Changesets |
|
787 | 789 | label_default_columns: Default columns |
|
788 | 790 | label_no_change_option: (No change) |
|
789 | 791 | label_bulk_edit_selected_issues: Bulk edit selected issues |
|
790 | 792 | label_bulk_edit_selected_time_entries: Bulk edit selected time entries |
|
791 | 793 | label_theme: Theme |
|
792 | 794 | label_default: Default |
|
793 | 795 | label_search_titles_only: Search titles only |
|
794 | 796 | label_user_mail_option_all: "For any event on all my projects" |
|
795 | 797 | label_user_mail_option_selected: "For any event on the selected projects only..." |
|
796 | 798 | label_user_mail_option_none: "No events" |
|
797 | 799 | label_user_mail_option_only_my_events: "Only for things I watch or I'm involved in" |
|
798 | 800 | label_user_mail_option_only_assigned: "Only for things I am assigned to" |
|
799 | 801 | label_user_mail_option_only_owner: "Only for things I am the owner of" |
|
800 | 802 | label_user_mail_no_self_notified: "I don't want to be notified of changes that I make myself" |
|
801 | 803 | label_registration_activation_by_email: account activation by email |
|
802 | 804 | label_registration_manual_activation: manual account activation |
|
803 | 805 | label_registration_automatic_activation: automatic account activation |
|
804 | 806 | label_display_per_page: "Per page: %{value}" |
|
805 | 807 | label_age: Age |
|
806 | 808 | label_change_properties: Change properties |
|
807 | 809 | label_general: General |
|
808 | 810 | label_more: More |
|
809 | 811 | label_scm: SCM |
|
810 | 812 | label_plugins: Plugins |
|
811 | 813 | label_ldap_authentication: LDAP authentication |
|
812 | 814 | label_downloads_abbr: D/L |
|
813 | 815 | label_optional_description: Optional description |
|
814 | 816 | label_add_another_file: Add another file |
|
815 | 817 | label_preferences: Preferences |
|
816 | 818 | label_chronological_order: In chronological order |
|
817 | 819 | label_reverse_chronological_order: In reverse chronological order |
|
818 | 820 | label_planning: Planning |
|
819 | 821 | label_incoming_emails: Incoming emails |
|
820 | 822 | label_generate_key: Generate a key |
|
821 | 823 | label_issue_watchers: Watchers |
|
822 | 824 | label_example: Example |
|
823 | 825 | label_display: Display |
|
824 | 826 | label_sort: Sort |
|
825 | 827 | label_ascending: Ascending |
|
826 | 828 | label_descending: Descending |
|
827 | 829 | label_date_from_to: From %{start} to %{end} |
|
828 | 830 | label_wiki_content_added: Wiki page added |
|
829 | 831 | label_wiki_content_updated: Wiki page updated |
|
830 | 832 | label_group: Group |
|
831 | 833 | label_group_plural: Groups |
|
832 | 834 | label_group_new: New group |
|
833 | 835 | label_time_entry_plural: Spent time |
|
834 | 836 | label_version_sharing_none: Not shared |
|
835 | 837 | label_version_sharing_descendants: With subprojects |
|
836 | 838 | label_version_sharing_hierarchy: With project hierarchy |
|
837 | 839 | label_version_sharing_tree: With project tree |
|
838 | 840 | label_version_sharing_system: With all projects |
|
839 | 841 | label_update_issue_done_ratios: Update issue done ratios |
|
840 | 842 | label_copy_source: Source |
|
841 | 843 | label_copy_target: Target |
|
842 | 844 | label_copy_same_as_target: Same as target |
|
843 | 845 | label_display_used_statuses_only: Only display statuses that are used by this tracker |
|
844 | 846 | label_api_access_key: API access key |
|
845 | 847 | label_missing_api_access_key: Missing an API access key |
|
846 | 848 | label_api_access_key_created_on: "API access key created %{value} ago" |
|
847 | 849 | label_profile: Profile |
|
848 | 850 | label_subtask_plural: Subtasks |
|
849 | 851 | label_project_copy_notifications: Send email notifications during the project copy |
|
850 | 852 | label_principal_search: "Search for user or group:" |
|
851 | 853 | label_user_search: "Search for user:" |
|
852 | 854 | label_additional_workflow_transitions_for_author: Additional transitions allowed when the user is the author |
|
853 | 855 | label_additional_workflow_transitions_for_assignee: Additional transitions allowed when the user is the assignee |
|
854 | 856 | label_issues_visibility_all: All issues |
|
855 | 857 | label_issues_visibility_public: All non private issues |
|
856 | 858 | label_issues_visibility_own: Issues created by or assigned to the user |
|
857 | 859 | label_git_report_last_commit: Report last commit for files and directories |
|
858 | 860 | label_parent_revision: Parent |
|
859 | 861 | label_child_revision: Child |
|
860 | 862 | label_export_options: "%{export_format} export options" |
|
861 | 863 | label_copy_attachments: Copy attachments |
|
862 | 864 | label_copy_subtasks: Copy subtasks |
|
863 | 865 | label_item_position: "%{position} of %{count}" |
|
864 | 866 | label_completed_versions: Completed versions |
|
865 | 867 | label_search_for_watchers: Search for watchers to add |
|
866 | 868 | label_session_expiration: Session expiration |
|
867 | 869 | label_show_closed_projects: View closed projects |
|
868 | 870 | label_status_transitions: Status transitions |
|
869 | 871 | label_fields_permissions: Fields permissions |
|
870 | 872 | label_readonly: Read-only |
|
871 | 873 | label_required: Required |
|
872 | 874 | label_attribute_of_project: "Project's %{name}" |
|
873 | 875 | label_attribute_of_author: "Author's %{name}" |
|
874 | 876 | label_attribute_of_assigned_to: "Assignee's %{name}" |
|
875 | 877 | label_attribute_of_fixed_version: "Target version's %{name}" |
|
876 | 878 | |
|
877 | 879 | button_login: Login |
|
878 | 880 | button_submit: Submit |
|
879 | 881 | button_save: Save |
|
880 | 882 | button_check_all: Check all |
|
881 | 883 | button_uncheck_all: Uncheck all |
|
882 | 884 | button_collapse_all: Collapse all |
|
883 | 885 | button_expand_all: Expand all |
|
884 | 886 | button_delete: Delete |
|
885 | 887 | button_create: Create |
|
886 | 888 | button_create_and_continue: Create and continue |
|
887 | 889 | button_test: Test |
|
888 | 890 | button_edit: Edit |
|
889 | 891 | button_edit_associated_wikipage: "Edit associated Wiki page: %{page_title}" |
|
890 | 892 | button_add: Add |
|
891 | 893 | button_change: Change |
|
892 | 894 | button_apply: Apply |
|
893 | 895 | button_clear: Clear |
|
894 | 896 | button_lock: Lock |
|
895 | 897 | button_unlock: Unlock |
|
896 | 898 | button_download: Download |
|
897 | 899 | button_list: List |
|
898 | 900 | button_view: View |
|
899 | 901 | button_move: Move |
|
900 | 902 | button_move_and_follow: Move and follow |
|
901 | 903 | button_back: Back |
|
902 | 904 | button_cancel: Cancel |
|
903 | 905 | button_activate: Activate |
|
904 | 906 | button_sort: Sort |
|
905 | 907 | button_log_time: Log time |
|
906 | 908 | button_rollback: Rollback to this version |
|
907 | 909 | button_watch: Watch |
|
908 | 910 | button_unwatch: Unwatch |
|
909 | 911 | button_reply: Reply |
|
910 | 912 | button_archive: Archive |
|
911 | 913 | button_unarchive: Unarchive |
|
912 | 914 | button_reset: Reset |
|
913 | 915 | button_rename: Rename |
|
914 | 916 | button_change_password: Change password |
|
915 | 917 | button_copy: Copy |
|
916 | 918 | button_copy_and_follow: Copy and follow |
|
917 | 919 | button_annotate: Annotate |
|
918 | 920 | button_update: Update |
|
919 | 921 | button_configure: Configure |
|
920 | 922 | button_quote: Quote |
|
921 | 923 | button_duplicate: Duplicate |
|
922 | 924 | button_show: Show |
|
923 | 925 | button_edit_section: Edit this section |
|
924 | 926 | button_export: Export |
|
925 | 927 | button_delete_my_account: Delete my account |
|
926 | 928 | button_close: Close |
|
927 | 929 | button_reopen: Reopen |
|
928 | 930 | |
|
929 | 931 | status_active: active |
|
930 | 932 | status_registered: registered |
|
931 | 933 | status_locked: locked |
|
932 | 934 | |
|
933 | 935 | project_status_active: active |
|
934 | 936 | project_status_closed: closed |
|
935 | 937 | project_status_archived: archived |
|
936 | 938 | |
|
937 | 939 | version_status_open: open |
|
938 | 940 | version_status_locked: locked |
|
939 | 941 | version_status_closed: closed |
|
940 | 942 | |
|
941 | 943 | field_active: Active |
|
942 | 944 | |
|
943 | 945 | text_select_mail_notifications: Select actions for which email notifications should be sent. |
|
944 | 946 | text_regexp_info: eg. ^[A-Z0-9]+$ |
|
945 | 947 | text_min_max_length_info: 0 means no restriction |
|
946 | 948 | text_project_destroy_confirmation: Are you sure you want to delete this project and related data? |
|
947 | 949 | text_subprojects_destroy_warning: "Its subproject(s): %{value} will be also deleted." |
|
948 | 950 | text_workflow_edit: Select a role and a tracker to edit the workflow |
|
949 | 951 | text_are_you_sure: Are you sure? |
|
950 | 952 | text_are_you_sure_with_children: "Delete issue and all child issues?" |
|
951 | 953 | text_journal_changed: "%{label} changed from %{old} to %{new}" |
|
952 | 954 | text_journal_changed_no_detail: "%{label} updated" |
|
953 | 955 | text_journal_set_to: "%{label} set to %{value}" |
|
954 | 956 | text_journal_deleted: "%{label} deleted (%{old})" |
|
955 | 957 | text_journal_added: "%{label} %{value} added" |
|
956 | 958 | text_tip_issue_begin_day: issue beginning this day |
|
957 | 959 | text_tip_issue_end_day: issue ending this day |
|
958 | 960 | text_tip_issue_begin_end_day: issue beginning and ending this day |
|
959 | 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 | 962 | text_caracters_maximum: "%{count} characters maximum." |
|
961 | 963 | text_caracters_minimum: "Must be at least %{count} characters long." |
|
962 | 964 | text_length_between: "Length between %{min} and %{max} characters." |
|
963 | 965 | text_tracker_no_workflow: No workflow defined for this tracker |
|
964 | 966 | text_unallowed_characters: Unallowed characters |
|
965 | 967 | text_comma_separated: Multiple values allowed (comma separated). |
|
966 | 968 | text_line_separated: Multiple values allowed (one line for each value). |
|
967 | 969 | text_issues_ref_in_commit_messages: Referencing and fixing issues in commit messages |
|
968 | 970 | text_issue_added: "Issue %{id} has been reported by %{author}." |
|
969 | 971 | text_issue_updated: "Issue %{id} has been updated by %{author}." |
|
970 | 972 | text_wiki_destroy_confirmation: Are you sure you want to delete this wiki and all its content? |
|
971 | 973 | text_issue_category_destroy_question: "Some issues (%{count}) are assigned to this category. What do you want to do?" |
|
972 | 974 | text_issue_category_destroy_assignments: Remove category assignments |
|
973 | 975 | text_issue_category_reassign_to: Reassign issues to this category |
|
974 | 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 | 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 | 978 | text_load_default_configuration: Load the default configuration |
|
977 | 979 | text_status_changed_by_changeset: "Applied in changeset %{value}." |
|
978 | 980 | text_time_logged_by_changeset: "Applied in changeset %{value}." |
|
979 | 981 | text_issues_destroy_confirmation: 'Are you sure you want to delete the selected issue(s)?' |
|
980 | 982 | text_issues_destroy_descendants_confirmation: "This will also delete %{count} subtask(s)." |
|
981 | 983 | text_time_entries_destroy_confirmation: 'Are you sure you want to delete the selected time entr(y/ies)?' |
|
982 | 984 | text_select_project_modules: 'Select modules to enable for this project:' |
|
983 | 985 | text_default_administrator_account_changed: Default administrator account changed |
|
984 | 986 | text_file_repository_writable: Attachments directory writable |
|
985 | 987 | text_plugin_assets_writable: Plugin assets directory writable |
|
986 | 988 | text_rmagick_available: RMagick available (optional) |
|
987 | 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 | 990 | text_destroy_time_entries: Delete reported hours |
|
989 | 991 | text_assign_time_entries_to_project: Assign reported hours to the project |
|
990 | 992 | text_reassign_time_entries: 'Reassign reported hours to this issue:' |
|
991 | 993 | text_user_wrote: "%{value} wrote:" |
|
992 | 994 | text_enumeration_destroy_question: "%{count} objects are assigned to this value." |
|
993 | 995 | text_enumeration_category_reassign_to: 'Reassign them to this value:' |
|
994 | 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 | 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 | 998 | text_diff_truncated: '... This diff was truncated because it exceeds the maximum size that can be displayed.' |
|
997 | 999 | text_custom_field_possible_values_info: 'One line for each value' |
|
998 | 1000 | text_wiki_page_destroy_question: "This page has %{descendants} child page(s) and descendant(s). What do you want to do?" |
|
999 | 1001 | text_wiki_page_nullify_children: "Keep child pages as root pages" |
|
1000 | 1002 | text_wiki_page_destroy_children: "Delete child pages and all their descendants" |
|
1001 | 1003 | text_wiki_page_reassign_children: "Reassign child pages to this parent page" |
|
1002 | 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 | 1005 | text_zoom_in: Zoom in |
|
1004 | 1006 | text_zoom_out: Zoom out |
|
1005 | 1007 | text_warn_on_leaving_unsaved: "The current page contains unsaved text that will be lost if you leave this page." |
|
1006 | 1008 | text_scm_path_encoding_note: "Default: UTF-8" |
|
1007 | 1009 | text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) |
|
1008 | 1010 | text_mercurial_repository_note: Local repository (e.g. /hgrepo, c:\hgrepo) |
|
1009 | 1011 | text_scm_command: Command |
|
1010 | 1012 | text_scm_command_version: Version |
|
1011 | 1013 | text_scm_config: You can configure your scm commands in config/configuration.yml. Please restart the application after editing it. |
|
1012 | 1014 | text_scm_command_not_available: Scm command is not available. Please check settings on the administration panel. |
|
1013 | 1015 | text_issue_conflict_resolution_overwrite: "Apply my changes anyway (previous notes will be kept but some changes may be overwritten)" |
|
1014 | 1016 | text_issue_conflict_resolution_add_notes: "Add my notes and discard my other changes" |
|
1015 | 1017 | text_issue_conflict_resolution_cancel: "Discard all my changes and redisplay %{link}" |
|
1016 | 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 | 1019 | text_session_expiration_settings: "Warning: changing these settings may expire the current sessions including yours." |
|
1018 | 1020 | text_project_closed: This project is closed and read-only. |
|
1019 | 1021 | |
|
1020 | 1022 | default_role_manager: Manager |
|
1021 | 1023 | default_role_developer: Developer |
|
1022 | 1024 | default_role_reporter: Reporter |
|
1023 | 1025 | default_tracker_bug: Bug |
|
1024 | 1026 | default_tracker_feature: Feature |
|
1025 | 1027 | default_tracker_support: Support |
|
1026 | 1028 | default_issue_status_new: New |
|
1027 | 1029 | default_issue_status_in_progress: In Progress |
|
1028 | 1030 | default_issue_status_resolved: Resolved |
|
1029 | 1031 | default_issue_status_feedback: Feedback |
|
1030 | 1032 | default_issue_status_closed: Closed |
|
1031 | 1033 | default_issue_status_rejected: Rejected |
|
1032 | 1034 | default_doc_category_user: User documentation |
|
1033 | 1035 | default_doc_category_tech: Technical documentation |
|
1034 | 1036 | default_priority_low: Low |
|
1035 | 1037 | default_priority_normal: Normal |
|
1036 | 1038 | default_priority_high: High |
|
1037 | 1039 | default_priority_urgent: Urgent |
|
1038 | 1040 | default_priority_immediate: Immediate |
|
1039 | 1041 | default_activity_design: Design |
|
1040 | 1042 | default_activity_development: Development |
|
1041 | 1043 | |
|
1042 | 1044 | enumeration_issue_priorities: Issue priorities |
|
1043 | 1045 | enumeration_doc_categories: Document categories |
|
1044 | 1046 | enumeration_activities: Activities (time tracking) |
|
1045 | 1047 | enumeration_system_activity: System Activity |
|
1046 | 1048 | description_filter: Filter |
|
1047 | 1049 | description_search: Searchfield |
|
1048 | 1050 | description_choose_project: Projects |
|
1049 | 1051 | description_project_scope: Search scope |
|
1050 | 1052 | description_notes: Notes |
|
1051 | 1053 | description_message_content: Message content |
|
1052 | 1054 | description_query_sort_criteria_attribute: Sort attribute |
|
1053 | 1055 | description_query_sort_criteria_direction: Sort direction |
|
1054 | 1056 | description_user_mail_notification: Mail notification settings |
|
1055 | 1057 | description_available_columns: Available Columns |
|
1056 | 1058 | description_selected_columns: Selected Columns |
|
1057 | 1059 | description_all_columns: All Columns |
|
1058 | 1060 | description_issue_category_reassign: Choose issue category |
|
1059 | 1061 | description_wiki_subpages_reassign: Choose new parent page |
|
1060 | 1062 | description_date_range_list: Choose range from list |
|
1061 | 1063 | description_date_range_interval: Choose range by selecting start and end date |
|
1062 | 1064 | description_date_from: Enter start date |
|
1063 | 1065 | description_date_to: Enter end date |
|
1064 | 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 | 1 | # French translations for Ruby on Rails |
|
2 | 2 | # by Christian Lescuyer (christian@flyingcoders.com) |
|
3 | 3 | # contributor: Sebastien Grosjean - ZenCocoon.com |
|
4 | 4 | # contributor: Thibaut Cuvelier - Developpez.com |
|
5 | 5 | |
|
6 | 6 | fr: |
|
7 | 7 | direction: ltr |
|
8 | 8 | date: |
|
9 | 9 | formats: |
|
10 | 10 | default: "%d/%m/%Y" |
|
11 | 11 | short: "%e %b" |
|
12 | 12 | long: "%e %B %Y" |
|
13 | 13 | long_ordinal: "%e %B %Y" |
|
14 | 14 | only_day: "%e" |
|
15 | 15 | |
|
16 | 16 | day_names: [dimanche, lundi, mardi, mercredi, jeudi, vendredi, samedi] |
|
17 | 17 | abbr_day_names: [dim, lun, mar, mer, jeu, ven, sam] |
|
18 | 18 | month_names: [~, janvier, février, mars, avril, mai, juin, juillet, août, septembre, octobre, novembre, décembre] |
|
19 | 19 | abbr_month_names: [~, jan., fév., mar., avr., mai, juin, juil., août, sept., oct., nov., déc.] |
|
20 | 20 | order: |
|
21 | 21 | - :day |
|
22 | 22 | - :month |
|
23 | 23 | - :year |
|
24 | 24 | |
|
25 | 25 | time: |
|
26 | 26 | formats: |
|
27 | 27 | default: "%d/%m/%Y %H:%M" |
|
28 | 28 | time: "%H:%M" |
|
29 | 29 | short: "%d %b %H:%M" |
|
30 | 30 | long: "%A %d %B %Y %H:%M:%S %Z" |
|
31 | 31 | long_ordinal: "%A %d %B %Y %H:%M:%S %Z" |
|
32 | 32 | only_second: "%S" |
|
33 | 33 | am: 'am' |
|
34 | 34 | pm: 'pm' |
|
35 | 35 | |
|
36 | 36 | datetime: |
|
37 | 37 | distance_in_words: |
|
38 | 38 | half_a_minute: "30 secondes" |
|
39 | 39 | less_than_x_seconds: |
|
40 | 40 | zero: "moins d'une seconde" |
|
41 | 41 | one: "moins d'une seconde" |
|
42 | 42 | other: "moins de %{count} secondes" |
|
43 | 43 | x_seconds: |
|
44 | 44 | one: "1 seconde" |
|
45 | 45 | other: "%{count} secondes" |
|
46 | 46 | less_than_x_minutes: |
|
47 | 47 | zero: "moins d'une minute" |
|
48 | 48 | one: "moins d'une minute" |
|
49 | 49 | other: "moins de %{count} minutes" |
|
50 | 50 | x_minutes: |
|
51 | 51 | one: "1 minute" |
|
52 | 52 | other: "%{count} minutes" |
|
53 | 53 | about_x_hours: |
|
54 | 54 | one: "environ une heure" |
|
55 | 55 | other: "environ %{count} heures" |
|
56 | 56 | x_hours: |
|
57 | 57 | one: "une heure" |
|
58 | 58 | other: "%{count} heures" |
|
59 | 59 | x_days: |
|
60 | 60 | one: "un jour" |
|
61 | 61 | other: "%{count} jours" |
|
62 | 62 | about_x_months: |
|
63 | 63 | one: "environ un mois" |
|
64 | 64 | other: "environ %{count} mois" |
|
65 | 65 | x_months: |
|
66 | 66 | one: "un mois" |
|
67 | 67 | other: "%{count} mois" |
|
68 | 68 | about_x_years: |
|
69 | 69 | one: "environ un an" |
|
70 | 70 | other: "environ %{count} ans" |
|
71 | 71 | over_x_years: |
|
72 | 72 | one: "plus d'un an" |
|
73 | 73 | other: "plus de %{count} ans" |
|
74 | 74 | almost_x_years: |
|
75 | 75 | one: "presqu'un an" |
|
76 | 76 | other: "presque %{count} ans" |
|
77 | 77 | prompts: |
|
78 | 78 | year: "Année" |
|
79 | 79 | month: "Mois" |
|
80 | 80 | day: "Jour" |
|
81 | 81 | hour: "Heure" |
|
82 | 82 | minute: "Minute" |
|
83 | 83 | second: "Seconde" |
|
84 | 84 | |
|
85 | 85 | number: |
|
86 | 86 | format: |
|
87 | 87 | precision: 3 |
|
88 | 88 | separator: ',' |
|
89 | 89 | delimiter: ' ' |
|
90 | 90 | currency: |
|
91 | 91 | format: |
|
92 | 92 | unit: '€' |
|
93 | 93 | precision: 2 |
|
94 | 94 | format: '%n %u' |
|
95 | 95 | human: |
|
96 | 96 | format: |
|
97 | 97 | precision: 3 |
|
98 | 98 | storage_units: |
|
99 | 99 | format: "%n %u" |
|
100 | 100 | units: |
|
101 | 101 | byte: |
|
102 | 102 | one: "octet" |
|
103 | 103 | other: "octet" |
|
104 | 104 | kb: "ko" |
|
105 | 105 | mb: "Mo" |
|
106 | 106 | gb: "Go" |
|
107 | 107 | tb: "To" |
|
108 | 108 | |
|
109 | 109 | support: |
|
110 | 110 | array: |
|
111 | 111 | sentence_connector: 'et' |
|
112 | 112 | skip_last_comma: true |
|
113 | 113 | word_connector: ", " |
|
114 | 114 | two_words_connector: " et " |
|
115 | 115 | last_word_connector: " et " |
|
116 | 116 | |
|
117 | 117 | activerecord: |
|
118 | 118 | errors: |
|
119 | 119 | template: |
|
120 | 120 | header: |
|
121 | 121 | one: "Impossible d'enregistrer %{model} : une erreur" |
|
122 | 122 | other: "Impossible d'enregistrer %{model} : %{count} erreurs." |
|
123 | 123 | body: "Veuillez vérifier les champs suivants :" |
|
124 | 124 | messages: |
|
125 | 125 | inclusion: "n'est pas inclus(e) dans la liste" |
|
126 | 126 | exclusion: "n'est pas disponible" |
|
127 | 127 | invalid: "n'est pas valide" |
|
128 | 128 | confirmation: "ne concorde pas avec la confirmation" |
|
129 | 129 | accepted: "doit être accepté(e)" |
|
130 | 130 | empty: "doit être renseigné(e)" |
|
131 | 131 | blank: "doit être renseigné(e)" |
|
132 | 132 | too_long: "est trop long (pas plus de %{count} caractères)" |
|
133 | 133 | too_short: "est trop court (au moins %{count} caractères)" |
|
134 | 134 | wrong_length: "ne fait pas la bonne longueur (doit comporter %{count} caractères)" |
|
135 | 135 | taken: "est déjà utilisé" |
|
136 | 136 | not_a_number: "n'est pas un nombre" |
|
137 | 137 | not_a_date: "n'est pas une date valide" |
|
138 | 138 | greater_than: "doit être supérieur à %{count}" |
|
139 | 139 | greater_than_or_equal_to: "doit être supérieur ou égal à %{count}" |
|
140 | 140 | equal_to: "doit être égal à %{count}" |
|
141 | 141 | less_than: "doit être inférieur à %{count}" |
|
142 | 142 | less_than_or_equal_to: "doit être inférieur ou égal à %{count}" |
|
143 | 143 | odd: "doit être impair" |
|
144 | 144 | even: "doit être pair" |
|
145 | 145 | greater_than_start_date: "doit être postérieure à la date de début" |
|
146 | 146 | not_same_project: "n'appartient pas au même projet" |
|
147 | 147 | circular_dependency: "Cette relation créerait une dépendance circulaire" |
|
148 | 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 | 150 | actionview_instancetag_blank_option: Choisir |
|
151 | 151 | |
|
152 | 152 | general_text_No: 'Non' |
|
153 | 153 | general_text_Yes: 'Oui' |
|
154 | 154 | general_text_no: 'non' |
|
155 | 155 | general_text_yes: 'oui' |
|
156 | 156 | general_lang_name: 'Français' |
|
157 | 157 | general_csv_separator: ';' |
|
158 | 158 | general_csv_decimal_separator: ',' |
|
159 | 159 | general_csv_encoding: ISO-8859-1 |
|
160 | 160 | general_pdf_encoding: UTF-8 |
|
161 | 161 | general_first_day_of_week: '1' |
|
162 | 162 | |
|
163 | 163 | notice_account_updated: Le compte a été mis à jour avec succès. |
|
164 | 164 | notice_account_invalid_creditentials: Identifiant ou mot de passe invalide. |
|
165 | 165 | notice_account_password_updated: Mot de passe mis à jour avec succès. |
|
166 | 166 | notice_account_wrong_password: Mot de passe incorrect |
|
167 | 167 | notice_account_register_done: Un message contenant les instructions pour activer votre compte vous a été envoyé. |
|
168 | 168 | notice_account_unknown_email: Aucun compte ne correspond à cette adresse. |
|
169 | 169 | notice_can_t_change_password: Ce compte utilise une authentification externe. Impossible de changer le mot de passe. |
|
170 | 170 | notice_account_lost_email_sent: Un message contenant les instructions pour choisir un nouveau mot de passe vous a été envoyé. |
|
171 | 171 | notice_account_activated: Votre compte a été activé. Vous pouvez à présent vous connecter. |
|
172 | 172 | notice_successful_create: Création effectuée avec succès. |
|
173 | 173 | notice_successful_update: Mise à jour effectuée avec succès. |
|
174 | 174 | notice_successful_delete: Suppression effectuée avec succès. |
|
175 | 175 | notice_successful_connection: Connexion réussie. |
|
176 | 176 | notice_file_not_found: "La page à laquelle vous souhaitez accéder n'existe pas ou a été supprimée." |
|
177 | 177 | notice_locking_conflict: Les données ont été mises à jour par un autre utilisateur. Mise à jour impossible. |
|
178 | 178 | notice_not_authorized: "Vous n'êtes pas autorisé à accéder à cette page." |
|
179 | 179 | notice_not_authorized_archived_project: Le projet auquel vous tentez d'accéder a été archivé. |
|
180 | 180 | notice_email_sent: "Un email a été envoyé à %{value}" |
|
181 | 181 | notice_email_error: "Erreur lors de l'envoi de l'email (%{value})" |
|
182 | 182 | notice_feeds_access_key_reseted: "Votre clé d'accès aux flux RSS a été réinitialisée." |
|
183 | 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 | 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 | 185 | notice_no_issue_selected: "Aucune demande sélectionnée ! Cochez les demandes que vous voulez mettre à jour." |
|
186 | 186 | notice_account_pending: "Votre compte a été créé et attend l'approbation de l'administrateur." |
|
187 | 187 | notice_default_data_loaded: Paramétrage par défaut chargé avec succès. |
|
188 | 188 | notice_unable_delete_version: Impossible de supprimer cette version. |
|
189 | 189 | notice_issue_done_ratios_updated: L'avancement des demandes a été mis à jour. |
|
190 | 190 | notice_api_access_key_reseted: Votre clé d'accès API a été réinitialisée. |
|
191 | 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 | 192 | notice_issue_successful_create: "Demande %{id} créée." |
|
193 | 193 | notice_issue_update_conflict: "La demande a été mise à jour par un autre utilisateur pendant que vous la modifiez." |
|
194 | 194 | notice_account_deleted: "Votre compte a été définitivement supprimé." |
|
195 | 195 | notice_user_successful_create: "Utilisateur %{id} créé." |
|
196 | 196 | |
|
197 | 197 | error_can_t_load_default_data: "Une erreur s'est produite lors du chargement du paramétrage : %{value}" |
|
198 | 198 | error_scm_not_found: "L'entrée et/ou la révision demandée n'existe pas dans le dépôt." |
|
199 | 199 | error_scm_command_failed: "Une erreur s'est produite lors de l'accès au dépôt : %{value}" |
|
200 | 200 | error_scm_annotate: "L'entrée n'existe pas ou ne peut pas être annotée." |
|
201 | 201 | error_issue_not_found_in_project: "La demande n'existe pas ou n'appartient pas à ce projet" |
|
202 | 202 | error_can_not_reopen_issue_on_closed_version: 'Une demande assignée à une version fermée ne peut pas être réouverte' |
|
203 | 203 | error_can_not_archive_project: "Ce projet ne peut pas être archivé" |
|
204 | 204 | error_workflow_copy_source: 'Veuillez sélectionner un tracker et/ou un rôle source' |
|
205 | 205 | error_workflow_copy_target: 'Veuillez sélectionner les trackers et rôles cibles' |
|
206 | 206 | error_issue_done_ratios_not_updated: L'avancement des demandes n'a pas pu être mis à jour. |
|
207 | 207 | error_attachment_too_big: Ce fichier ne peut pas être attaché car il excède la taille maximale autorisée (%{max_size}) |
|
208 | 208 | error_session_expired: "Votre session a expiré. Veuillez vous reconnecter." |
|
209 | 209 | |
|
210 | 210 | warning_attachments_not_saved: "%{count} fichier(s) n'ont pas pu être sauvegardés." |
|
211 | 211 | |
|
212 | 212 | mail_subject_lost_password: "Votre mot de passe %{value}" |
|
213 | 213 | mail_body_lost_password: 'Pour changer votre mot de passe, cliquez sur le lien suivant :' |
|
214 | 214 | mail_subject_register: "Activation de votre compte %{value}" |
|
215 | 215 | mail_body_register: 'Pour activer votre compte, cliquez sur le lien suivant :' |
|
216 | 216 | mail_body_account_information_external: "Vous pouvez utiliser votre compte %{value} pour vous connecter." |
|
217 | 217 | mail_body_account_information: Paramètres de connexion de votre compte |
|
218 | 218 | mail_subject_account_activation_request: "Demande d'activation d'un compte %{value}" |
|
219 | 219 | mail_body_account_activation_request: "Un nouvel utilisateur (%{value}) s'est inscrit. Son compte nécessite votre approbation :" |
|
220 | 220 | mail_subject_reminder: "%{count} demande(s) arrivent à échéance (%{days})" |
|
221 | 221 | mail_body_reminder: "%{count} demande(s) qui vous sont assignées arrivent à échéance dans les %{days} prochains jours :" |
|
222 | 222 | mail_subject_wiki_content_added: "Page wiki '%{id}' ajoutée" |
|
223 | 223 | mail_body_wiki_content_added: "La page wiki '%{id}' a été ajoutée par %{author}." |
|
224 | 224 | mail_subject_wiki_content_updated: "Page wiki '%{id}' mise à jour" |
|
225 | 225 | mail_body_wiki_content_updated: "La page wiki '%{id}' a été mise à jour par %{author}." |
|
226 | 226 | |
|
227 | 227 | gui_validation_error: 1 erreur |
|
228 | 228 | gui_validation_error_plural: "%{count} erreurs" |
|
229 | 229 | |
|
230 | 230 | field_name: Nom |
|
231 | 231 | field_description: Description |
|
232 | 232 | field_summary: Résumé |
|
233 | 233 | field_is_required: Obligatoire |
|
234 | 234 | field_firstname: Prénom |
|
235 | 235 | field_lastname: Nom |
|
236 | 236 | field_mail: "Email " |
|
237 | 237 | field_filename: Fichier |
|
238 | 238 | field_filesize: Taille |
|
239 | 239 | field_downloads: Téléchargements |
|
240 | 240 | field_author: Auteur |
|
241 | 241 | field_created_on: "Créé " |
|
242 | 242 | field_updated_on: "Mis-à-jour " |
|
243 | 243 | field_field_format: Format |
|
244 | 244 | field_is_for_all: Pour tous les projets |
|
245 | 245 | field_possible_values: Valeurs possibles |
|
246 | 246 | field_regexp: Expression régulière |
|
247 | 247 | field_min_length: Longueur minimum |
|
248 | 248 | field_max_length: Longueur maximum |
|
249 | 249 | field_value: Valeur |
|
250 | 250 | field_category: Catégorie |
|
251 | 251 | field_title: Titre |
|
252 | 252 | field_project: Projet |
|
253 | 253 | field_issue: Demande |
|
254 | 254 | field_status: Statut |
|
255 | 255 | field_notes: Notes |
|
256 | 256 | field_is_closed: Demande fermée |
|
257 | 257 | field_is_default: Valeur par défaut |
|
258 | 258 | field_tracker: Tracker |
|
259 | 259 | field_subject: Sujet |
|
260 | 260 | field_due_date: Echéance |
|
261 | 261 | field_assigned_to: Assigné à |
|
262 | 262 | field_priority: Priorité |
|
263 | 263 | field_fixed_version: Version cible |
|
264 | 264 | field_user: Utilisateur |
|
265 | 265 | field_role: Rôle |
|
266 | 266 | field_homepage: "Site web " |
|
267 | 267 | field_is_public: Public |
|
268 | 268 | field_parent: Sous-projet de |
|
269 | 269 | field_is_in_roadmap: Demandes affichées dans la roadmap |
|
270 | 270 | field_login: "Identifiant " |
|
271 | 271 | field_mail_notification: Notifications par mail |
|
272 | 272 | field_admin: Administrateur |
|
273 | 273 | field_last_login_on: "Dernière connexion " |
|
274 | 274 | field_language: Langue |
|
275 | 275 | field_effective_date: Date |
|
276 | 276 | field_password: Mot de passe |
|
277 | 277 | field_new_password: Nouveau mot de passe |
|
278 | 278 | field_password_confirmation: Confirmation |
|
279 | 279 | field_version: Version |
|
280 | 280 | field_type: Type |
|
281 | 281 | field_host: Hôte |
|
282 | 282 | field_port: Port |
|
283 | 283 | field_account: Compte |
|
284 | 284 | field_base_dn: Base DN |
|
285 | 285 | field_attr_login: Attribut Identifiant |
|
286 | 286 | field_attr_firstname: Attribut Prénom |
|
287 | 287 | field_attr_lastname: Attribut Nom |
|
288 | 288 | field_attr_mail: Attribut Email |
|
289 | 289 | field_onthefly: Création des utilisateurs à la volée |
|
290 | 290 | field_start_date: Début |
|
291 | 291 | field_done_ratio: "% réalisé" |
|
292 | 292 | field_auth_source: Mode d'authentification |
|
293 | 293 | field_hide_mail: Cacher mon adresse mail |
|
294 | 294 | field_comments: Commentaire |
|
295 | 295 | field_url: URL |
|
296 | 296 | field_start_page: Page de démarrage |
|
297 | 297 | field_subproject: Sous-projet |
|
298 | 298 | field_hours: Heures |
|
299 | 299 | field_activity: Activité |
|
300 | 300 | field_spent_on: Date |
|
301 | 301 | field_identifier: Identifiant |
|
302 | 302 | field_is_filter: Utilisé comme filtre |
|
303 | 303 | field_issue_to: Demande liée |
|
304 | 304 | field_delay: Retard |
|
305 | 305 | field_assignable: Demandes assignables à ce rôle |
|
306 | 306 | field_redirect_existing_links: Rediriger les liens existants |
|
307 | 307 | field_estimated_hours: Temps estimé |
|
308 | 308 | field_column_names: Colonnes |
|
309 | 309 | field_time_zone: Fuseau horaire |
|
310 | 310 | field_searchable: Utilisé pour les recherches |
|
311 | 311 | field_default_value: Valeur par défaut |
|
312 | 312 | field_comments_sorting: Afficher les commentaires |
|
313 | 313 | field_parent_title: Page parent |
|
314 | 314 | field_editable: Modifiable |
|
315 | 315 | field_watcher: Observateur |
|
316 | 316 | field_identity_url: URL OpenID |
|
317 | 317 | field_content: Contenu |
|
318 | 318 | field_group_by: Grouper par |
|
319 | 319 | field_sharing: Partage |
|
320 | 320 | field_active: Actif |
|
321 | 321 | field_parent_issue: Tâche parente |
|
322 | 322 | field_visible: Visible |
|
323 | 323 | field_warn_on_leaving_unsaved: "M'avertir lorsque je quitte une page contenant du texte non sauvegardé" |
|
324 | 324 | field_issues_visibility: Visibilité des demandes |
|
325 | 325 | field_is_private: Privée |
|
326 | 326 | field_commit_logs_encoding: Encodage des messages de commit |
|
327 | 327 | field_repository_is_default: Dépôt principal |
|
328 | 328 | field_multiple: Valeurs multiples |
|
329 | 329 | field_auth_source_ldap_filter: Filtre LDAP |
|
330 | 330 | field_core_fields: Champs standards |
|
331 | 331 | field_timeout: "Timeout (en secondes)" |
|
332 | 332 | field_board_parent: Forum parent |
|
333 | 333 | |
|
334 | 334 | setting_app_title: Titre de l'application |
|
335 | 335 | setting_app_subtitle: Sous-titre de l'application |
|
336 | 336 | setting_welcome_text: Texte d'accueil |
|
337 | 337 | setting_default_language: Langue par défaut |
|
338 | 338 | setting_login_required: Authentification obligatoire |
|
339 | 339 | setting_self_registration: Inscription des nouveaux utilisateurs |
|
340 | 340 | setting_attachment_max_size: Taille maximale des fichiers |
|
341 | 341 | setting_issues_export_limit: Limite d'exportation des demandes |
|
342 | 342 | setting_mail_from: Adresse d'émission |
|
343 | 343 | setting_bcc_recipients: Destinataires en copie cachée (cci) |
|
344 | 344 | setting_plain_text_mail: Mail en texte brut (non HTML) |
|
345 | 345 | setting_host_name: Nom d'hôte et chemin |
|
346 | 346 | setting_text_formatting: Formatage du texte |
|
347 | 347 | setting_wiki_compression: Compression de l'historique des pages wiki |
|
348 | 348 | setting_feeds_limit: Nombre maximal d'éléments dans les flux Atom |
|
349 | 349 | setting_default_projects_public: Définir les nouveaux projets comme publics par défaut |
|
350 | 350 | setting_autofetch_changesets: Récupération automatique des commits |
|
351 | 351 | setting_sys_api_enabled: Activer les WS pour la gestion des dépôts |
|
352 | 352 | setting_commit_ref_keywords: Mots-clés de référencement |
|
353 | 353 | setting_commit_fix_keywords: Mots-clés de résolution |
|
354 | 354 | setting_autologin: Durée maximale de connexion automatique |
|
355 | 355 | setting_date_format: Format de date |
|
356 | 356 | setting_time_format: Format d'heure |
|
357 | 357 | setting_cross_project_issue_relations: Autoriser les relations entre demandes de différents projets |
|
358 | 358 | setting_issue_list_default_columns: Colonnes affichées par défaut sur la liste des demandes |
|
359 | 359 | setting_emails_footer: Pied-de-page des emails |
|
360 | 360 | setting_protocol: Protocole |
|
361 | 361 | setting_per_page_options: Options d'objets affichés par page |
|
362 | 362 | setting_user_format: Format d'affichage des utilisateurs |
|
363 | 363 | setting_activity_days_default: Nombre de jours affichés sur l'activité des projets |
|
364 | 364 | setting_display_subprojects_issues: Afficher par défaut les demandes des sous-projets sur les projets principaux |
|
365 | 365 | setting_enabled_scm: SCM activés |
|
366 | 366 | setting_mail_handler_body_delimiters: "Tronquer les emails après l'une de ces lignes" |
|
367 | 367 | setting_mail_handler_api_enabled: "Activer le WS pour la réception d'emails" |
|
368 | 368 | setting_mail_handler_api_key: Clé de protection de l'API |
|
369 | 369 | setting_sequential_project_identifiers: Générer des identifiants de projet séquentiels |
|
370 | 370 | setting_gravatar_enabled: Afficher les Gravatar des utilisateurs |
|
371 | 371 | setting_diff_max_lines_displayed: Nombre maximum de lignes de diff affichées |
|
372 | 372 | setting_file_max_size_displayed: Taille maximum des fichiers texte affichés en ligne |
|
373 | 373 | setting_repository_log_display_limit: "Nombre maximum de révisions affichées sur l'historique d'un fichier" |
|
374 | 374 | setting_openid: "Autoriser l'authentification et l'enregistrement OpenID" |
|
375 | 375 | setting_password_min_length: Longueur minimum des mots de passe |
|
376 | 376 | setting_new_project_user_role_id: Rôle donné à un utilisateur non-administrateur qui crée un projet |
|
377 | 377 | setting_default_projects_modules: Modules activés par défaut pour les nouveaux projets |
|
378 | 378 | setting_issue_done_ratio: Calcul de l'avancement des demandes |
|
379 | 379 | setting_issue_done_ratio_issue_status: Utiliser le statut |
|
380 | 380 | setting_issue_done_ratio_issue_field: 'Utiliser le champ % effectué' |
|
381 | 381 | setting_rest_api_enabled: Activer l'API REST |
|
382 | 382 | setting_gravatar_default: Image Gravatar par défaut |
|
383 | 383 | setting_start_of_week: Jour de début des calendriers |
|
384 | 384 | setting_cache_formatted_text: Mettre en cache le texte formaté |
|
385 | 385 | setting_commit_logtime_enabled: Permettre la saisie de temps |
|
386 | 386 | setting_commit_logtime_activity_id: Activité pour le temps saisi |
|
387 | 387 | setting_gantt_items_limit: Nombre maximum d'éléments affichés sur le gantt |
|
388 | 388 | setting_issue_group_assignment: Permettre l'assignement des demandes aux groupes |
|
389 | 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 | 390 | setting_commit_cross_project_ref: Permettre le référencement et la résolution des demandes de tous les autres projets |
|
391 | 391 | setting_unsubscribe: Permettre aux utilisateurs de supprimer leur propre compte |
|
392 | 392 | setting_session_lifetime: Durée de vie maximale des sessions |
|
393 | 393 | setting_session_timeout: Durée maximale d'inactivité |
|
394 | 394 | setting_thumbnails_enabled: Afficher les vignettes des images |
|
395 | 395 | setting_thumbnails_size: Taille des vignettes (en pixels) |
|
396 | 396 | |
|
397 | 397 | permission_add_project: Créer un projet |
|
398 | 398 | permission_add_subprojects: Créer des sous-projets |
|
399 | 399 | permission_edit_project: Modifier le projet |
|
400 | 400 | permission_close_project: Fermer / réouvrir le projet |
|
401 | 401 | permission_select_project_modules: Choisir les modules |
|
402 | 402 | permission_manage_members: Gérer les membres |
|
403 | 403 | permission_manage_versions: Gérer les versions |
|
404 | 404 | permission_manage_categories: Gérer les catégories de demandes |
|
405 | 405 | permission_view_issues: Voir les demandes |
|
406 | 406 | permission_add_issues: Créer des demandes |
|
407 | 407 | permission_edit_issues: Modifier les demandes |
|
408 | 408 | permission_manage_issue_relations: Gérer les relations |
|
409 | 409 | permission_set_issues_private: Rendre les demandes publiques ou privées |
|
410 | 410 | permission_set_own_issues_private: Rendre ses propres demandes publiques ou privées |
|
411 | 411 | permission_add_issue_notes: Ajouter des notes |
|
412 | 412 | permission_edit_issue_notes: Modifier les notes |
|
413 | 413 | permission_edit_own_issue_notes: Modifier ses propres notes |
|
414 | 414 | permission_move_issues: Déplacer les demandes |
|
415 | 415 | permission_delete_issues: Supprimer les demandes |
|
416 | 416 | permission_manage_public_queries: Gérer les requêtes publiques |
|
417 | 417 | permission_save_queries: Sauvegarder les requêtes |
|
418 | 418 | permission_view_gantt: Voir le gantt |
|
419 | 419 | permission_view_calendar: Voir le calendrier |
|
420 | 420 | permission_view_issue_watchers: Voir la liste des observateurs |
|
421 | 421 | permission_add_issue_watchers: Ajouter des observateurs |
|
422 | 422 | permission_delete_issue_watchers: Supprimer des observateurs |
|
423 | 423 | permission_log_time: Saisir le temps passé |
|
424 | 424 | permission_view_time_entries: Voir le temps passé |
|
425 | 425 | permission_edit_time_entries: Modifier les temps passés |
|
426 | 426 | permission_edit_own_time_entries: Modifier son propre temps passé |
|
427 | 427 | permission_manage_news: Gérer les annonces |
|
428 | 428 | permission_comment_news: Commenter les annonces |
|
429 | 429 | permission_manage_documents: Gérer les documents |
|
430 | 430 | permission_view_documents: Voir les documents |
|
431 | 431 | permission_manage_files: Gérer les fichiers |
|
432 | 432 | permission_view_files: Voir les fichiers |
|
433 | 433 | permission_manage_wiki: Gérer le wiki |
|
434 | 434 | permission_rename_wiki_pages: Renommer les pages |
|
435 | 435 | permission_delete_wiki_pages: Supprimer les pages |
|
436 | 436 | permission_view_wiki_pages: Voir le wiki |
|
437 | 437 | permission_view_wiki_edits: "Voir l'historique des modifications" |
|
438 | 438 | permission_edit_wiki_pages: Modifier les pages |
|
439 | 439 | permission_delete_wiki_pages_attachments: Supprimer les fichiers joints |
|
440 | 440 | permission_protect_wiki_pages: Protéger les pages |
|
441 | 441 | permission_manage_repository: Gérer le dépôt de sources |
|
442 | 442 | permission_browse_repository: Parcourir les sources |
|
443 | 443 | permission_view_changesets: Voir les révisions |
|
444 | 444 | permission_commit_access: Droit de commit |
|
445 | 445 | permission_manage_boards: Gérer les forums |
|
446 | 446 | permission_view_messages: Voir les messages |
|
447 | 447 | permission_add_messages: Poster un message |
|
448 | 448 | permission_edit_messages: Modifier les messages |
|
449 | 449 | permission_edit_own_messages: Modifier ses propres messages |
|
450 | 450 | permission_delete_messages: Supprimer les messages |
|
451 | 451 | permission_delete_own_messages: Supprimer ses propres messages |
|
452 | 452 | permission_export_wiki_pages: Exporter les pages |
|
453 | 453 | permission_manage_project_activities: Gérer les activités |
|
454 | 454 | permission_manage_subtasks: Gérer les sous-tâches |
|
455 | 455 | permission_manage_related_issues: Gérer les demandes associées |
|
456 | 456 | |
|
457 | 457 | project_module_issue_tracking: Suivi des demandes |
|
458 | 458 | project_module_time_tracking: Suivi du temps passé |
|
459 | 459 | project_module_news: Publication d'annonces |
|
460 | 460 | project_module_documents: Publication de documents |
|
461 | 461 | project_module_files: Publication de fichiers |
|
462 | 462 | project_module_wiki: Wiki |
|
463 | 463 | project_module_repository: Dépôt de sources |
|
464 | 464 | project_module_boards: Forums de discussion |
|
465 | 465 | |
|
466 | 466 | label_user: Utilisateur |
|
467 | 467 | label_user_plural: Utilisateurs |
|
468 | 468 | label_user_new: Nouvel utilisateur |
|
469 | 469 | label_user_anonymous: Anonyme |
|
470 | 470 | label_project: Projet |
|
471 | 471 | label_project_new: Nouveau projet |
|
472 | 472 | label_project_plural: Projets |
|
473 | 473 | label_x_projects: |
|
474 | 474 | zero: aucun projet |
|
475 | 475 | one: un projet |
|
476 | 476 | other: "%{count} projets" |
|
477 | 477 | label_project_all: Tous les projets |
|
478 | 478 | label_project_latest: Derniers projets |
|
479 | 479 | label_issue: Demande |
|
480 | 480 | label_issue_new: Nouvelle demande |
|
481 | 481 | label_issue_plural: Demandes |
|
482 | 482 | label_issue_view_all: Voir toutes les demandes |
|
483 | 483 | label_issue_added: Demande ajoutée |
|
484 | 484 | label_issue_updated: Demande mise à jour |
|
485 | 485 | label_issue_note_added: Note ajoutée |
|
486 | 486 | label_issue_status_updated: Statut changé |
|
487 | 487 | label_issue_priority_updated: Priorité changée |
|
488 | 488 | label_issues_by: "Demandes par %{value}" |
|
489 | 489 | label_document: Document |
|
490 | 490 | label_document_new: Nouveau document |
|
491 | 491 | label_document_plural: Documents |
|
492 | 492 | label_document_added: Document ajouté |
|
493 | 493 | label_role: Rôle |
|
494 | 494 | label_role_plural: Rôles |
|
495 | 495 | label_role_new: Nouveau rôle |
|
496 | 496 | label_role_and_permissions: Rôles et permissions |
|
497 | 497 | label_role_anonymous: Anonyme |
|
498 | 498 | label_role_non_member: Non membre |
|
499 | 499 | label_member: Membre |
|
500 | 500 | label_member_new: Nouveau membre |
|
501 | 501 | label_member_plural: Membres |
|
502 | 502 | label_tracker: Tracker |
|
503 | 503 | label_tracker_plural: Trackers |
|
504 | 504 | label_tracker_new: Nouveau tracker |
|
505 | 505 | label_workflow: Workflow |
|
506 | 506 | label_issue_status: Statut de demandes |
|
507 | 507 | label_issue_status_plural: Statuts de demandes |
|
508 | 508 | label_issue_status_new: Nouveau statut |
|
509 | 509 | label_issue_category: Catégorie de demandes |
|
510 | 510 | label_issue_category_plural: Catégories de demandes |
|
511 | 511 | label_issue_category_new: Nouvelle catégorie |
|
512 | 512 | label_custom_field: Champ personnalisé |
|
513 | 513 | label_custom_field_plural: Champs personnalisés |
|
514 | 514 | label_custom_field_new: Nouveau champ personnalisé |
|
515 | 515 | label_enumerations: Listes de valeurs |
|
516 | 516 | label_enumeration_new: Nouvelle valeur |
|
517 | 517 | label_information: Information |
|
518 | 518 | label_information_plural: Informations |
|
519 | 519 | label_please_login: Identification |
|
520 | 520 | label_register: S'enregistrer |
|
521 | 521 | label_login_with_open_id_option: S'authentifier avec OpenID |
|
522 | 522 | label_password_lost: Mot de passe perdu |
|
523 | 523 | label_home: Accueil |
|
524 | 524 | label_my_page: Ma page |
|
525 | 525 | label_my_account: Mon compte |
|
526 | 526 | label_my_projects: Mes projets |
|
527 | 527 | label_my_page_block: Blocs disponibles |
|
528 | 528 | label_administration: Administration |
|
529 | 529 | label_login: Connexion |
|
530 | 530 | label_logout: Déconnexion |
|
531 | 531 | label_help: Aide |
|
532 | 532 | label_reported_issues: "Demandes soumises " |
|
533 | 533 | label_assigned_to_me_issues: Demandes qui me sont assignées |
|
534 | 534 | label_last_login: "Dernière connexion " |
|
535 | 535 | label_registered_on: "Inscrit le " |
|
536 | 536 | label_activity: Activité |
|
537 | 537 | label_overall_activity: Activité globale |
|
538 | 538 | label_user_activity: "Activité de %{value}" |
|
539 | 539 | label_new: Nouveau |
|
540 | 540 | label_logged_as: Connecté en tant que |
|
541 | 541 | label_environment: Environnement |
|
542 | 542 | label_authentication: Authentification |
|
543 | 543 | label_auth_source: Mode d'authentification |
|
544 | 544 | label_auth_source_new: Nouveau mode d'authentification |
|
545 | 545 | label_auth_source_plural: Modes d'authentification |
|
546 | 546 | label_subproject_plural: Sous-projets |
|
547 | 547 | label_subproject_new: Nouveau sous-projet |
|
548 | 548 | label_and_its_subprojects: "%{value} et ses sous-projets" |
|
549 | 549 | label_min_max_length: Longueurs mini - maxi |
|
550 | 550 | label_list: Liste |
|
551 | 551 | label_date: Date |
|
552 | 552 | label_integer: Entier |
|
553 | 553 | label_float: Nombre décimal |
|
554 | 554 | label_boolean: Booléen |
|
555 | 555 | label_string: Texte |
|
556 | 556 | label_text: Texte long |
|
557 | 557 | label_attribute: Attribut |
|
558 | 558 | label_attribute_plural: Attributs |
|
559 | 559 | label_download: "%{count} téléchargement" |
|
560 | 560 | label_download_plural: "%{count} téléchargements" |
|
561 | 561 | label_no_data: Aucune donnée à afficher |
|
562 | 562 | label_change_status: Changer le statut |
|
563 | 563 | label_history: Historique |
|
564 | 564 | label_attachment: Fichier |
|
565 | 565 | label_attachment_new: Nouveau fichier |
|
566 | 566 | label_attachment_delete: Supprimer le fichier |
|
567 | 567 | label_attachment_plural: Fichiers |
|
568 | 568 | label_file_added: Fichier ajouté |
|
569 | 569 | label_report: Rapport |
|
570 | 570 | label_report_plural: Rapports |
|
571 | 571 | label_news: Annonce |
|
572 | 572 | label_news_new: Nouvelle annonce |
|
573 | 573 | label_news_plural: Annonces |
|
574 | 574 | label_news_latest: Dernières annonces |
|
575 | 575 | label_news_view_all: Voir toutes les annonces |
|
576 | 576 | label_news_added: Annonce ajoutée |
|
577 | 577 | label_news_comment_added: Commentaire ajouté à une annonce |
|
578 | 578 | label_settings: Configuration |
|
579 | 579 | label_overview: Aperçu |
|
580 | 580 | label_version: Version |
|
581 | 581 | label_version_new: Nouvelle version |
|
582 | 582 | label_version_plural: Versions |
|
583 | 583 | label_confirmation: Confirmation |
|
584 | 584 | label_export_to: 'Formats disponibles :' |
|
585 | 585 | label_read: Lire... |
|
586 | 586 | label_public_projects: Projets publics |
|
587 | 587 | label_open_issues: ouvert |
|
588 | 588 | label_open_issues_plural: ouverts |
|
589 | 589 | label_closed_issues: fermé |
|
590 | 590 | label_closed_issues_plural: fermés |
|
591 | 591 | label_x_open_issues_abbr_on_total: |
|
592 | 592 | zero: 0 ouverte sur %{total} |
|
593 | 593 | one: 1 ouverte sur %{total} |
|
594 | 594 | other: "%{count} ouvertes sur %{total}" |
|
595 | 595 | label_x_open_issues_abbr: |
|
596 | 596 | zero: 0 ouverte |
|
597 | 597 | one: 1 ouverte |
|
598 | 598 | other: "%{count} ouvertes" |
|
599 | 599 | label_x_closed_issues_abbr: |
|
600 | 600 | zero: 0 fermée |
|
601 | 601 | one: 1 fermée |
|
602 | 602 | other: "%{count} fermées" |
|
603 | 603 | label_x_issues: |
|
604 | 604 | zero: 0 demande |
|
605 | 605 | one: 1 demande |
|
606 | 606 | other: "%{count} demandes" |
|
607 | 607 | label_total: Total |
|
608 | 608 | label_permissions: Permissions |
|
609 | 609 | label_current_status: Statut actuel |
|
610 | 610 | label_new_statuses_allowed: Nouveaux statuts autorisés |
|
611 | 611 | label_all: tous |
|
612 | 612 | label_none: aucun |
|
613 | 613 | label_nobody: personne |
|
614 | 614 | label_next: Suivant |
|
615 | 615 | label_previous: Précédent |
|
616 | 616 | label_used_by: Utilisé par |
|
617 | 617 | label_details: Détails |
|
618 | 618 | label_add_note: Ajouter une note |
|
619 | 619 | label_per_page: Par page |
|
620 | 620 | label_calendar: Calendrier |
|
621 | 621 | label_months_from: mois depuis |
|
622 | 622 | label_gantt: Gantt |
|
623 | 623 | label_internal: Interne |
|
624 | 624 | label_last_changes: "%{count} derniers changements" |
|
625 | 625 | label_change_view_all: Voir tous les changements |
|
626 | 626 | label_personalize_page: Personnaliser cette page |
|
627 | 627 | label_comment: Commentaire |
|
628 | 628 | label_comment_plural: Commentaires |
|
629 | 629 | label_x_comments: |
|
630 | 630 | zero: aucun commentaire |
|
631 | 631 | one: un commentaire |
|
632 | 632 | other: "%{count} commentaires" |
|
633 | 633 | label_comment_add: Ajouter un commentaire |
|
634 | 634 | label_comment_added: Commentaire ajouté |
|
635 | 635 | label_comment_delete: Supprimer les commentaires |
|
636 | 636 | label_query: Rapport personnalisé |
|
637 | 637 | label_query_plural: Rapports personnalisés |
|
638 | 638 | label_query_new: Nouveau rapport |
|
639 | 639 | label_my_queries: Mes rapports personnalisés |
|
640 | 640 | label_filter_add: "Ajouter le filtre " |
|
641 | 641 | label_filter_plural: Filtres |
|
642 | 642 | label_equals: égal |
|
643 | 643 | label_not_equals: différent |
|
644 | 644 | label_in_less_than: dans moins de |
|
645 | 645 | label_in_more_than: dans plus de |
|
646 | 646 | label_in: dans |
|
647 | 647 | label_today: aujourd'hui |
|
648 | 648 | label_all_time: toute la période |
|
649 | 649 | label_yesterday: hier |
|
650 | 650 | label_this_week: cette semaine |
|
651 | 651 | label_last_week: la semaine dernière |
|
652 | 652 | label_last_n_days: "les %{count} derniers jours" |
|
653 | 653 | label_this_month: ce mois-ci |
|
654 | 654 | label_last_month: le mois dernier |
|
655 | 655 | label_this_year: cette année |
|
656 | 656 | label_date_range: Période |
|
657 | 657 | label_less_than_ago: il y a moins de |
|
658 | 658 | label_more_than_ago: il y a plus de |
|
659 | 659 | label_ago: il y a |
|
660 | 660 | label_contains: contient |
|
661 | 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 | 664 | label_day_plural: jours |
|
663 | 665 | label_repository: Dépôt |
|
664 | 666 | label_repository_new: Nouveau dépôt |
|
665 | 667 | label_repository_plural: Dépôts |
|
666 | 668 | label_browse: Parcourir |
|
667 | 669 | label_modification: "%{count} modification" |
|
668 | 670 | label_modification_plural: "%{count} modifications" |
|
669 | 671 | label_revision: "Révision " |
|
670 | 672 | label_revision_plural: Révisions |
|
671 | 673 | label_associated_revisions: Révisions associées |
|
672 | 674 | label_added: ajouté |
|
673 | 675 | label_modified: modifié |
|
674 | 676 | label_copied: copié |
|
675 | 677 | label_renamed: renommé |
|
676 | 678 | label_deleted: supprimé |
|
677 | 679 | label_latest_revision: Dernière révision |
|
678 | 680 | label_latest_revision_plural: Dernières révisions |
|
679 | 681 | label_view_revisions: Voir les révisions |
|
680 | 682 | label_max_size: Taille maximale |
|
681 | 683 | label_sort_highest: Remonter en premier |
|
682 | 684 | label_sort_higher: Remonter |
|
683 | 685 | label_sort_lower: Descendre |
|
684 | 686 | label_sort_lowest: Descendre en dernier |
|
685 | 687 | label_roadmap: Roadmap |
|
686 | 688 | label_roadmap_due_in: "Échéance dans %{value}" |
|
687 | 689 | label_roadmap_overdue: "En retard de %{value}" |
|
688 | 690 | label_roadmap_no_issues: Aucune demande pour cette version |
|
689 | 691 | label_search: "Recherche " |
|
690 | 692 | label_result_plural: Résultats |
|
691 | 693 | label_all_words: Tous les mots |
|
692 | 694 | label_wiki: Wiki |
|
693 | 695 | label_wiki_edit: Révision wiki |
|
694 | 696 | label_wiki_edit_plural: Révisions wiki |
|
695 | 697 | label_wiki_page: Page wiki |
|
696 | 698 | label_wiki_page_plural: Pages wiki |
|
697 | 699 | label_index_by_title: Index par titre |
|
698 | 700 | label_index_by_date: Index par date |
|
699 | 701 | label_current_version: Version actuelle |
|
700 | 702 | label_preview: Prévisualisation |
|
701 | 703 | label_feed_plural: Flux RSS |
|
702 | 704 | label_changes_details: Détails de tous les changements |
|
703 | 705 | label_issue_tracking: Suivi des demandes |
|
704 | 706 | label_spent_time: Temps passé |
|
705 | 707 | label_f_hour: "%{value} heure" |
|
706 | 708 | label_f_hour_plural: "%{value} heures" |
|
707 | 709 | label_time_tracking: Suivi du temps |
|
708 | 710 | label_change_plural: Changements |
|
709 | 711 | label_statistics: Statistiques |
|
710 | 712 | label_commits_per_month: Commits par mois |
|
711 | 713 | label_commits_per_author: Commits par auteur |
|
712 | 714 | label_view_diff: Voir les différences |
|
713 | 715 | label_diff_inline: en ligne |
|
714 | 716 | label_diff_side_by_side: côte à côte |
|
715 | 717 | label_options: Options |
|
716 | 718 | label_copy_workflow_from: Copier le workflow de |
|
717 | 719 | label_permissions_report: Synthèse des permissions |
|
718 | 720 | label_watched_issues: Demandes surveillées |
|
719 | 721 | label_related_issues: Demandes liées |
|
720 | 722 | label_applied_status: Statut appliqué |
|
721 | 723 | label_loading: Chargement... |
|
722 | 724 | label_relation_new: Nouvelle relation |
|
723 | 725 | label_relation_delete: Supprimer la relation |
|
724 |
label_relates_to: |
|
|
725 |
label_duplicates: |
|
|
726 |
label_duplicated_by: |
|
|
727 |
label_blocks: |
|
|
728 |
label_blocked_by: |
|
|
729 |
label_precedes: |
|
|
730 |
label_follows: |
|
|
731 |
label_copied_to: |
|
|
732 |
label_copied_from: |
|
|
726 | label_relates_to: Lié à | |
|
727 | label_duplicates: Duplique | |
|
728 | label_duplicated_by: Dupliqué par | |
|
729 | label_blocks: Bloque | |
|
730 | label_blocked_by: Bloqué par | |
|
731 | label_precedes: Précède | |
|
732 | label_follows: Suit | |
|
733 | label_copied_to: Copié vers | |
|
734 | label_copied_from: Copié depuis | |
|
733 | 735 | label_end_to_start: fin à début |
|
734 | 736 | label_end_to_end: fin à fin |
|
735 | 737 | label_start_to_start: début à début |
|
736 | 738 | label_start_to_end: début à fin |
|
737 | 739 | label_stay_logged_in: Rester connecté |
|
738 | 740 | label_disabled: désactivé |
|
739 | 741 | label_show_completed_versions: Voir les versions passées |
|
740 | 742 | label_me: moi |
|
741 | 743 | label_board: Forum |
|
742 | 744 | label_board_new: Nouveau forum |
|
743 | 745 | label_board_plural: Forums |
|
744 | 746 | label_topic_plural: Discussions |
|
745 | 747 | label_message_plural: Messages |
|
746 | 748 | label_message_last: Dernier message |
|
747 | 749 | label_message_new: Nouveau message |
|
748 | 750 | label_message_posted: Message ajouté |
|
749 | 751 | label_reply_plural: Réponses |
|
750 | 752 | label_send_information: Envoyer les informations à l'utilisateur |
|
751 | 753 | label_year: Année |
|
752 | 754 | label_month: Mois |
|
753 | 755 | label_week: Semaine |
|
754 | 756 | label_date_from: Du |
|
755 | 757 | label_date_to: Au |
|
756 | 758 | label_language_based: Basé sur la langue de l'utilisateur |
|
757 | 759 | label_sort_by: "Trier par %{value}" |
|
758 | 760 | label_send_test_email: Envoyer un email de test |
|
759 | 761 | label_feeds_access_key_created_on: "Clé d'accès RSS créée il y a %{value}" |
|
760 | 762 | label_module_plural: Modules |
|
761 | 763 | label_added_time_by: "Ajouté par %{author} il y a %{age}" |
|
762 | 764 | label_updated_time_by: "Mis à jour par %{author} il y a %{age}" |
|
763 | 765 | label_updated_time: "Mis à jour il y a %{value}" |
|
764 | 766 | label_jump_to_a_project: Aller à un projet... |
|
765 | 767 | label_file_plural: Fichiers |
|
766 | 768 | label_changeset_plural: Révisions |
|
767 | 769 | label_default_columns: Colonnes par défaut |
|
768 | 770 | label_no_change_option: (Pas de changement) |
|
769 | 771 | label_bulk_edit_selected_issues: Modifier les demandes sélectionnées |
|
770 | 772 | label_theme: Thème |
|
771 | 773 | label_default: Défaut |
|
772 | 774 | label_search_titles_only: Uniquement dans les titres |
|
773 | 775 | label_user_mail_option_all: "Pour tous les événements de tous mes projets" |
|
774 | 776 | label_user_mail_option_selected: "Pour tous les événements des projets sélectionnés..." |
|
775 | 777 | label_user_mail_no_self_notified: "Je ne veux pas être notifié des changements que j'effectue" |
|
776 | 778 | label_registration_activation_by_email: activation du compte par email |
|
777 | 779 | label_registration_manual_activation: activation manuelle du compte |
|
778 | 780 | label_registration_automatic_activation: activation automatique du compte |
|
779 | 781 | label_display_per_page: "Par page : %{value}" |
|
780 | 782 | label_age: Âge |
|
781 | 783 | label_change_properties: Changer les propriétés |
|
782 | 784 | label_general: Général |
|
783 | 785 | label_more: Plus |
|
784 | 786 | label_scm: SCM |
|
785 | 787 | label_plugins: Plugins |
|
786 | 788 | label_ldap_authentication: Authentification LDAP |
|
787 | 789 | label_downloads_abbr: D/L |
|
788 | 790 | label_optional_description: Description facultative |
|
789 | 791 | label_add_another_file: Ajouter un autre fichier |
|
790 | 792 | label_preferences: Préférences |
|
791 | 793 | label_chronological_order: Dans l'ordre chronologique |
|
792 | 794 | label_reverse_chronological_order: Dans l'ordre chronologique inverse |
|
793 | 795 | label_planning: Planning |
|
794 | 796 | label_incoming_emails: Emails entrants |
|
795 | 797 | label_generate_key: Générer une clé |
|
796 | 798 | label_issue_watchers: Observateurs |
|
797 | 799 | label_example: Exemple |
|
798 | 800 | label_display: Affichage |
|
799 | 801 | label_sort: Tri |
|
800 | 802 | label_ascending: Croissant |
|
801 | 803 | label_descending: Décroissant |
|
802 | 804 | label_date_from_to: Du %{start} au %{end} |
|
803 | 805 | label_wiki_content_added: Page wiki ajoutée |
|
804 | 806 | label_wiki_content_updated: Page wiki mise à jour |
|
805 | 807 | label_group_plural: Groupes |
|
806 | 808 | label_group: Groupe |
|
807 | 809 | label_group_new: Nouveau groupe |
|
808 | 810 | label_time_entry_plural: Temps passé |
|
809 | 811 | label_version_sharing_none: Non partagé |
|
810 | 812 | label_version_sharing_descendants: Avec les sous-projets |
|
811 | 813 | label_version_sharing_hierarchy: Avec toute la hiérarchie |
|
812 | 814 | label_version_sharing_tree: Avec tout l'arbre |
|
813 | 815 | label_version_sharing_system: Avec tous les projets |
|
814 | 816 | label_copy_source: Source |
|
815 | 817 | label_copy_target: Cible |
|
816 | 818 | label_copy_same_as_target: Comme la cible |
|
817 | 819 | label_update_issue_done_ratios: Mettre à jour l'avancement des demandes |
|
818 | 820 | label_display_used_statuses_only: N'afficher que les statuts utilisés dans ce tracker |
|
819 | 821 | label_api_access_key: Clé d'accès API |
|
820 | 822 | label_api_access_key_created_on: Clé d'accès API créée il y a %{value} |
|
821 | 823 | label_feeds_access_key: Clé d'accès RSS |
|
822 | 824 | label_missing_api_access_key: Clé d'accès API manquante |
|
823 | 825 | label_missing_feeds_access_key: Clé d'accès RSS manquante |
|
824 | 826 | label_close_versions: Fermer les versions terminées |
|
825 | 827 | label_revision_id: Révision %{value} |
|
826 | 828 | label_profile: Profil |
|
827 | 829 | label_subtask_plural: Sous-tâches |
|
828 | 830 | label_project_copy_notifications: Envoyer les notifications durant la copie du projet |
|
829 | 831 | label_principal_search: "Rechercher un utilisateur ou un groupe :" |
|
830 | 832 | label_user_search: "Rechercher un utilisateur :" |
|
831 | 833 | label_additional_workflow_transitions_for_author: Autorisations supplémentaires lorsque l'utilisateur a créé la demande |
|
832 | 834 | label_additional_workflow_transitions_for_assignee: Autorisations supplémentaires lorsque la demande est assignée à l'utilisateur |
|
833 | 835 | label_issues_visibility_all: Toutes les demandes |
|
834 | 836 | label_issues_visibility_public: Toutes les demandes non privées |
|
835 | 837 | label_issues_visibility_own: Demandes créées par ou assignées à l'utilisateur |
|
836 | 838 | label_export_options: Options d'exportation %{export_format} |
|
837 | 839 | label_copy_attachments: Copier les fichiers |
|
838 | 840 | label_copy_subtasks: Copier les sous-tâches |
|
839 | 841 | label_item_position: "%{position} sur %{count}" |
|
840 | 842 | label_completed_versions: Versions passées |
|
841 | 843 | label_session_expiration: Expiration des sessions |
|
842 | 844 | label_show_closed_projects: Voir les projets fermés |
|
843 | 845 | label_status_transitions: Changements de statut |
|
844 | 846 | label_fields_permissions: Permissions sur les champs |
|
845 | 847 | label_readonly: Lecture |
|
846 | 848 | label_required: Obligatoire |
|
847 | 849 | label_attribute_of_project: "%{name} du projet" |
|
848 | 850 | label_attribute_of_author: "%{name} de l'auteur" |
|
849 | 851 | label_attribute_of_assigned_to: "%{name} de l'assigné" |
|
850 | 852 | label_attribute_of_fixed_version: "%{name} de la version cible" |
|
851 | 853 | |
|
852 | 854 | button_login: Connexion |
|
853 | 855 | button_submit: Soumettre |
|
854 | 856 | button_save: Sauvegarder |
|
855 | 857 | button_check_all: Tout cocher |
|
856 | 858 | button_uncheck_all: Tout décocher |
|
857 | 859 | button_collapse_all: Plier tout |
|
858 | 860 | button_expand_all: Déplier tout |
|
859 | 861 | button_delete: Supprimer |
|
860 | 862 | button_create: Créer |
|
861 | 863 | button_create_and_continue: Créer et continuer |
|
862 | 864 | button_test: Tester |
|
863 | 865 | button_edit: Modifier |
|
864 | 866 | button_add: Ajouter |
|
865 | 867 | button_change: Changer |
|
866 | 868 | button_apply: Appliquer |
|
867 | 869 | button_clear: Effacer |
|
868 | 870 | button_lock: Verrouiller |
|
869 | 871 | button_unlock: Déverrouiller |
|
870 | 872 | button_download: Télécharger |
|
871 | 873 | button_list: Lister |
|
872 | 874 | button_view: Voir |
|
873 | 875 | button_move: Déplacer |
|
874 | 876 | button_move_and_follow: Déplacer et suivre |
|
875 | 877 | button_back: Retour |
|
876 | 878 | button_cancel: Annuler |
|
877 | 879 | button_activate: Activer |
|
878 | 880 | button_sort: Trier |
|
879 | 881 | button_log_time: Saisir temps |
|
880 | 882 | button_rollback: Revenir à cette version |
|
881 | 883 | button_watch: Surveiller |
|
882 | 884 | button_unwatch: Ne plus surveiller |
|
883 | 885 | button_reply: Répondre |
|
884 | 886 | button_archive: Archiver |
|
885 | 887 | button_unarchive: Désarchiver |
|
886 | 888 | button_reset: Réinitialiser |
|
887 | 889 | button_rename: Renommer |
|
888 | 890 | button_change_password: Changer de mot de passe |
|
889 | 891 | button_copy: Copier |
|
890 | 892 | button_copy_and_follow: Copier et suivre |
|
891 | 893 | button_annotate: Annoter |
|
892 | 894 | button_update: Mettre à jour |
|
893 | 895 | button_configure: Configurer |
|
894 | 896 | button_quote: Citer |
|
895 | 897 | button_duplicate: Dupliquer |
|
896 | 898 | button_show: Afficher |
|
897 | 899 | button_edit_section: Modifier cette section |
|
898 | 900 | button_export: Exporter |
|
899 | 901 | button_delete_my_account: Supprimer mon compte |
|
900 | 902 | button_close: Fermer |
|
901 | 903 | button_reopen: Réouvrir |
|
902 | 904 | |
|
903 | 905 | status_active: actif |
|
904 | 906 | status_registered: enregistré |
|
905 | 907 | status_locked: verrouillé |
|
906 | 908 | |
|
907 | 909 | project_status_active: actif |
|
908 | 910 | project_status_closed: fermé |
|
909 | 911 | project_status_archived: archivé |
|
910 | 912 | |
|
911 | 913 | version_status_open: ouvert |
|
912 | 914 | version_status_locked: verrouillé |
|
913 | 915 | version_status_closed: fermé |
|
914 | 916 | |
|
915 | 917 | text_select_mail_notifications: Actions pour lesquelles une notification par e-mail est envoyée |
|
916 | 918 | text_regexp_info: ex. ^[A-Z0-9]+$ |
|
917 | 919 | text_min_max_length_info: 0 pour aucune restriction |
|
918 | 920 | text_project_destroy_confirmation: Êtes-vous sûr de vouloir supprimer ce projet et toutes ses données ? |
|
919 | 921 | text_subprojects_destroy_warning: "Ses sous-projets : %{value} seront également supprimés." |
|
920 | 922 | text_workflow_edit: Sélectionner un tracker et un rôle pour éditer le workflow |
|
921 | 923 | text_are_you_sure: Êtes-vous sûr ? |
|
922 | 924 | text_tip_issue_begin_day: tâche commençant ce jour |
|
923 | 925 | text_tip_issue_end_day: tâche finissant ce jour |
|
924 | 926 | text_tip_issue_begin_end_day: tâche commençant et finissant ce jour |
|
925 | 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 | 928 | text_caracters_maximum: "%{count} caractères maximum." |
|
927 | 929 | text_caracters_minimum: "%{count} caractères minimum." |
|
928 | 930 | text_length_between: "Longueur comprise entre %{min} et %{max} caractères." |
|
929 | 931 | text_tracker_no_workflow: Aucun worflow n'est défini pour ce tracker |
|
930 | 932 | text_unallowed_characters: Caractères non autorisés |
|
931 | 933 | text_comma_separated: Plusieurs valeurs possibles (séparées par des virgules). |
|
932 | 934 | text_line_separated: Plusieurs valeurs possibles (une valeur par ligne). |
|
933 | 935 | text_issues_ref_in_commit_messages: Référencement et résolution des demandes dans les commentaires de commits |
|
934 | 936 | text_issue_added: "La demande %{id} a été soumise par %{author}." |
|
935 | 937 | text_issue_updated: "La demande %{id} a été mise à jour par %{author}." |
|
936 | 938 | text_wiki_destroy_confirmation: Etes-vous sûr de vouloir supprimer ce wiki et tout son contenu ? |
|
937 | 939 | text_issue_category_destroy_question: "%{count} demandes sont affectées à cette catégorie. Que voulez-vous faire ?" |
|
938 | 940 | text_issue_category_destroy_assignments: N'affecter les demandes à aucune autre catégorie |
|
939 | 941 | text_issue_category_reassign_to: Réaffecter les demandes à cette catégorie |
|
940 | 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 | 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 | 944 | text_load_default_configuration: Charger le paramétrage par défaut |
|
943 | 945 | text_status_changed_by_changeset: "Appliqué par commit %{value}." |
|
944 | 946 | text_time_logged_by_changeset: "Appliqué par commit %{value}" |
|
945 | 947 | text_issues_destroy_confirmation: 'Êtes-vous sûr de vouloir supprimer la ou les demandes(s) selectionnée(s) ?' |
|
946 | 948 | text_issues_destroy_descendants_confirmation: "Cela entrainera également la suppression de %{count} sous-tâche(s)." |
|
947 | 949 | text_select_project_modules: 'Sélectionner les modules à activer pour ce projet :' |
|
948 | 950 | text_default_administrator_account_changed: Compte administrateur par défaut changé |
|
949 | 951 | text_file_repository_writable: Répertoire de stockage des fichiers accessible en écriture |
|
950 | 952 | text_plugin_assets_writable: Répertoire public des plugins accessible en écriture |
|
951 | 953 | text_rmagick_available: Bibliothèque RMagick présente (optionnelle) |
|
952 | 954 | text_destroy_time_entries_question: "%{hours} heures ont été enregistrées sur les demandes à supprimer. Que voulez-vous faire ?" |
|
953 | 955 | text_destroy_time_entries: Supprimer les heures |
|
954 | 956 | text_assign_time_entries_to_project: Reporter les heures sur le projet |
|
955 | 957 | text_reassign_time_entries: 'Reporter les heures sur cette demande:' |
|
956 | 958 | text_user_wrote: "%{value} a écrit :" |
|
957 | 959 | text_enumeration_destroy_question: "Cette valeur est affectée à %{count} objets." |
|
958 | 960 | text_enumeration_category_reassign_to: 'Réaffecter les objets à cette valeur:' |
|
959 | 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 | 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 | 963 | text_diff_truncated: '... Ce différentiel a été tronqué car il excède la taille maximale pouvant être affichée.' |
|
962 | 964 | text_custom_field_possible_values_info: 'Une ligne par valeur' |
|
963 | 965 | text_wiki_page_destroy_question: "Cette page possède %{descendants} sous-page(s) et descendante(s). Que voulez-vous faire ?" |
|
964 | 966 | text_wiki_page_nullify_children: "Conserver les sous-pages en tant que pages racines" |
|
965 | 967 | text_wiki_page_destroy_children: "Supprimer les sous-pages et toutes leurs descedantes" |
|
966 | 968 | text_wiki_page_reassign_children: "Réaffecter les sous-pages à cette page" |
|
967 | 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 | 970 | text_warn_on_leaving_unsaved: "Cette page contient du texte non sauvegardé qui sera perdu si vous quittez la page." |
|
969 | 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 | 972 | text_issue_conflict_resolution_add_notes: "Ajouter mes notes et ignorer mes autres changements" |
|
971 | 973 | text_issue_conflict_resolution_cancel: "Annuler ma mise à jour et réafficher %{link}" |
|
972 | 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 | 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 | 976 | text_project_closed: Ce projet est fermé et accessible en lecture seule. |
|
975 | 977 | |
|
976 | 978 | default_role_manager: "Manager " |
|
977 | 979 | default_role_developer: "Développeur " |
|
978 | 980 | default_role_reporter: "Rapporteur " |
|
979 | 981 | default_tracker_bug: Anomalie |
|
980 | 982 | default_tracker_feature: Evolution |
|
981 | 983 | default_tracker_support: Assistance |
|
982 | 984 | default_issue_status_new: Nouveau |
|
983 | 985 | default_issue_status_in_progress: En cours |
|
984 | 986 | default_issue_status_resolved: Résolu |
|
985 | 987 | default_issue_status_feedback: Commentaire |
|
986 | 988 | default_issue_status_closed: Fermé |
|
987 | 989 | default_issue_status_rejected: Rejeté |
|
988 | 990 | default_doc_category_user: Documentation utilisateur |
|
989 | 991 | default_doc_category_tech: Documentation technique |
|
990 | 992 | default_priority_low: Bas |
|
991 | 993 | default_priority_normal: Normal |
|
992 | 994 | default_priority_high: Haut |
|
993 | 995 | default_priority_urgent: Urgent |
|
994 | 996 | default_priority_immediate: Immédiat |
|
995 | 997 | default_activity_design: Conception |
|
996 | 998 | default_activity_development: Développement |
|
997 | 999 | |
|
998 | 1000 | enumeration_issue_priorities: Priorités des demandes |
|
999 | 1001 | enumeration_doc_categories: Catégories des documents |
|
1000 | 1002 | enumeration_activities: Activités (suivi du temps) |
|
1001 | 1003 | label_greater_or_equal: ">=" |
|
1002 | 1004 | label_less_or_equal: "<=" |
|
1003 | 1005 | label_between: entre |
|
1004 | 1006 | label_view_all_revisions: Voir toutes les révisions |
|
1005 | 1007 | label_tag: Tag |
|
1006 | 1008 | label_branch: Branche |
|
1007 | 1009 | error_no_tracker_in_project: "Aucun tracker n'est associé à ce projet. Vérifier la configuration du projet." |
|
1008 | 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 | 1011 | text_journal_changed: "%{label} changé de %{old} à %{new}" |
|
1010 | 1012 | text_journal_changed_no_detail: "%{label} mis à jour" |
|
1011 | 1013 | text_journal_set_to: "%{label} mis à %{value}" |
|
1012 | 1014 | text_journal_deleted: "%{label} %{old} supprimé" |
|
1013 | 1015 | text_journal_added: "%{label} %{value} ajouté" |
|
1014 | 1016 | enumeration_system_activity: Activité système |
|
1015 | 1017 | label_board_sticky: Sticky |
|
1016 | 1018 | label_board_locked: Verrouillé |
|
1017 | 1019 | error_unable_delete_issue_status: Impossible de supprimer le statut de demande |
|
1018 | 1020 | error_can_not_delete_custom_field: Impossible de supprimer le champ personnalisé |
|
1019 | 1021 | error_unable_to_connect: Connexion impossible (%{value}) |
|
1020 | 1022 | error_can_not_remove_role: Ce rôle est utilisé et ne peut pas être supprimé. |
|
1021 | 1023 | error_can_not_delete_tracker: Ce tracker contient des demandes et ne peut pas être supprimé. |
|
1022 | 1024 | field_principal: Principal |
|
1023 | 1025 | notice_failed_to_save_members: "Erreur lors de la sauvegarde des membres: %{errors}." |
|
1024 | 1026 | text_zoom_out: Zoom arrière |
|
1025 | 1027 | text_zoom_in: Zoom avant |
|
1026 | 1028 | notice_unable_delete_time_entry: Impossible de supprimer le temps passé. |
|
1027 | 1029 | label_overall_spent_time: Temps passé global |
|
1028 | 1030 | field_time_entries: Temps passé |
|
1029 | 1031 | project_module_gantt: Gantt |
|
1030 | 1032 | project_module_calendar: Calendrier |
|
1031 | 1033 | button_edit_associated_wikipage: "Modifier la page wiki associée: %{page_title}" |
|
1032 | 1034 | text_are_you_sure_with_children: Supprimer la demande et toutes ses sous-demandes ? |
|
1033 | 1035 | field_text: Champ texte |
|
1034 | 1036 | label_user_mail_option_only_owner: Seulement pour ce que j'ai créé |
|
1035 | 1037 | setting_default_notification_option: Option de notification par défaut |
|
1036 | 1038 | label_user_mail_option_only_my_events: Seulement pour ce que je surveille |
|
1037 | 1039 | label_user_mail_option_only_assigned: Seulement pour ce qui m'est assigné |
|
1038 | 1040 | label_user_mail_option_none: Aucune notification |
|
1039 | 1041 | field_member_of_group: Groupe de l'assigné |
|
1040 | 1042 | field_assigned_to_role: Rôle de l'assigné |
|
1041 | 1043 | setting_emails_header: En-tête des emails |
|
1042 | 1044 | label_bulk_edit_selected_time_entries: Modifier les temps passés sélectionnés |
|
1043 | 1045 | text_time_entries_destroy_confirmation: "Etes-vous sûr de vouloir supprimer les temps passés sélectionnés ?" |
|
1044 | 1046 | field_scm_path_encoding: Encodage des chemins |
|
1045 | 1047 | text_scm_path_encoding_note: "Défaut : UTF-8" |
|
1046 | 1048 | field_path_to_repository: Chemin du dépôt |
|
1047 | 1049 | field_root_directory: Répertoire racine |
|
1048 | 1050 | field_cvs_module: Module |
|
1049 | 1051 | field_cvsroot: CVSROOT |
|
1050 | 1052 | text_mercurial_repository_note: "Dépôt local (exemples : /hgrepo, c:\\hgrepo)" |
|
1051 | 1053 | text_scm_command: Commande |
|
1052 | 1054 | text_scm_command_version: Version |
|
1053 | 1055 | label_git_report_last_commit: Afficher le dernier commit des fichiers et répertoires |
|
1054 | 1056 | text_scm_config: Vous pouvez configurer les commandes des SCM dans config/configuration.yml. Redémarrer l'application après modification. |
|
1055 | 1057 | text_scm_command_not_available: Ce SCM n'est pas disponible. Vérifier les paramètres dans la section administration. |
|
1056 | 1058 | label_diff: diff |
|
1057 | 1059 | text_git_repository_note: Repository is bare and local (e.g. /gitrepo, c:\gitrepo) |
|
1058 | 1060 | description_query_sort_criteria_direction: Ordre de tri |
|
1059 | 1061 | description_project_scope: Périmètre de recherche |
|
1060 | 1062 | description_filter: Filtre |
|
1061 | 1063 | description_user_mail_notification: Option de notification |
|
1062 | 1064 | description_date_from: Date de début |
|
1063 | 1065 | description_message_content: Contenu du message |
|
1064 | 1066 | description_available_columns: Colonnes disponibles |
|
1065 | 1067 | description_all_columns: Toutes les colonnes |
|
1066 | 1068 | description_date_range_interval: Choisir une période |
|
1067 | 1069 | description_issue_category_reassign: Choisir une catégorie |
|
1068 | 1070 | description_search: Champ de recherche |
|
1069 | 1071 | description_notes: Notes |
|
1070 | 1072 | description_date_range_list: Choisir une période prédéfinie |
|
1071 | 1073 | description_choose_project: Projets |
|
1072 | 1074 | description_date_to: Date de fin |
|
1073 | 1075 | description_query_sort_criteria_attribute: Critère de tri |
|
1074 | 1076 | description_wiki_subpages_reassign: Choisir une nouvelle page parent |
|
1075 | 1077 | description_selected_columns: Colonnes sélectionnées |
|
1076 | 1078 | label_parent_revision: Parent |
|
1077 | 1079 | label_child_revision: Enfant |
|
1078 | 1080 | error_scm_annotate_big_text_file: Cette entrée ne peut pas être annotée car elle excède la taille maximale. |
|
1079 | 1081 | setting_repositories_encodings: Encodages des fichiers et des dépôts |
|
1080 | 1082 | label_search_for_watchers: Rechercher des observateurs |
|
1081 | 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 | 1 | /* Redmine - project management software |
|
2 | 2 | Copyright (C) 2006-2012 Jean-Philippe Lang */ |
|
3 | 3 | |
|
4 | 4 | function checkAll(id, checked) { |
|
5 | 5 | if (checked) { |
|
6 | 6 | $('#'+id).find('input[type=checkbox]').attr('checked', true); |
|
7 | 7 | } else { |
|
8 | 8 | $('#'+id).find('input[type=checkbox]').removeAttr('checked'); |
|
9 | 9 | } |
|
10 | 10 | } |
|
11 | 11 | |
|
12 | 12 | function toggleCheckboxesBySelector(selector) { |
|
13 | 13 | var all_checked = true; |
|
14 | 14 | $(selector).each(function(index) { |
|
15 | 15 | if (!$(this).is(':checked')) { all_checked = false; } |
|
16 | 16 | }); |
|
17 | 17 | $(selector).attr('checked', !all_checked) |
|
18 | 18 | } |
|
19 | 19 | |
|
20 | 20 | function showAndScrollTo(id, focus) { |
|
21 | 21 | $('#'+id).show(); |
|
22 | 22 | if (focus!=null) { |
|
23 | 23 | $('#'+focus).focus(); |
|
24 | 24 | } |
|
25 | 25 | $('html, body').animate({scrollTop: $('#'+id).offset().top}, 100); |
|
26 | 26 | } |
|
27 | 27 | |
|
28 | 28 | function toggleRowGroup(el) { |
|
29 | 29 | var tr = $(el).parents('tr').first(); |
|
30 | 30 | var n = tr.next(); |
|
31 | 31 | tr.toggleClass('open'); |
|
32 | 32 | while (n.length && !n.hasClass('group')) { |
|
33 | 33 | n.toggle(); |
|
34 | 34 | n = n.next('tr'); |
|
35 | 35 | } |
|
36 | 36 | } |
|
37 | 37 | |
|
38 | 38 | function collapseAllRowGroups(el) { |
|
39 | 39 | var tbody = $(el).parents('tbody').first(); |
|
40 | 40 | tbody.children('tr').each(function(index) { |
|
41 | 41 | if ($(this).hasClass('group')) { |
|
42 | 42 | $(this).removeClass('open'); |
|
43 | 43 | } else { |
|
44 | 44 | $(this).hide(); |
|
45 | 45 | } |
|
46 | 46 | }); |
|
47 | 47 | } |
|
48 | 48 | |
|
49 | 49 | function expandAllRowGroups(el) { |
|
50 | 50 | var tbody = $(el).parents('tbody').first(); |
|
51 | 51 | tbody.children('tr').each(function(index) { |
|
52 | 52 | if ($(this).hasClass('group')) { |
|
53 | 53 | $(this).addClass('open'); |
|
54 | 54 | } else { |
|
55 | 55 | $(this).show(); |
|
56 | 56 | } |
|
57 | 57 | }); |
|
58 | 58 | } |
|
59 | 59 | |
|
60 | 60 | function toggleAllRowGroups(el) { |
|
61 | 61 | var tr = $(el).parents('tr').first(); |
|
62 | 62 | if (tr.hasClass('open')) { |
|
63 | 63 | collapseAllRowGroups(el); |
|
64 | 64 | } else { |
|
65 | 65 | expandAllRowGroups(el); |
|
66 | 66 | } |
|
67 | 67 | } |
|
68 | 68 | |
|
69 | 69 | function toggleFieldset(el) { |
|
70 | 70 | var fieldset = $(el).parents('fieldset').first(); |
|
71 | 71 | fieldset.toggleClass('collapsed'); |
|
72 | 72 | fieldset.children('div').toggle(); |
|
73 | 73 | } |
|
74 | 74 | |
|
75 | 75 | function hideFieldset(el) { |
|
76 | 76 | var fieldset = $(el).parents('fieldset').first(); |
|
77 | 77 | fieldset.toggleClass('collapsed'); |
|
78 | 78 | fieldset.children('div').hide(); |
|
79 | 79 | } |
|
80 | 80 | |
|
81 | 81 | function initFilters(){ |
|
82 | 82 | $('#add_filter_select').change(function(){ |
|
83 | 83 | addFilter($(this).val(), '', []); |
|
84 | 84 | }); |
|
85 | 85 | $('#filters-table td.field input[type=checkbox]').each(function(){ |
|
86 | 86 | toggleFilter($(this).val()); |
|
87 | 87 | }); |
|
88 | 88 | $('#filters-table td.field input[type=checkbox]').live('click',function(){ |
|
89 | 89 | toggleFilter($(this).val()); |
|
90 | 90 | }); |
|
91 | 91 | $('#filters-table .toggle-multiselect').live('click',function(){ |
|
92 | 92 | toggleMultiSelect($(this).siblings('select')); |
|
93 | 93 | }); |
|
94 | 94 | $('#filters-table input[type=text]').live('keypress', function(e){ |
|
95 | 95 | if (e.keyCode == 13) submit_query_form("query_form"); |
|
96 | 96 | }); |
|
97 | 97 | } |
|
98 | 98 | |
|
99 | 99 | function addFilter(field, operator, values) { |
|
100 | 100 | var fieldId = field.replace('.', '_'); |
|
101 | 101 | var tr = $('#tr_'+fieldId); |
|
102 | 102 | if (tr.length > 0) { |
|
103 | 103 | tr.show(); |
|
104 | 104 | } else { |
|
105 | 105 | buildFilterRow(field, operator, values); |
|
106 | 106 | } |
|
107 | 107 | $('#cb_'+fieldId).attr('checked', true); |
|
108 | 108 | toggleFilter(field); |
|
109 | 109 | $('#add_filter_select').val('').children('option').each(function(){ |
|
110 | 110 | if ($(this).attr('value') == field) { |
|
111 | 111 | $(this).attr('disabled', true); |
|
112 | 112 | } |
|
113 | 113 | }); |
|
114 | 114 | } |
|
115 | 115 | |
|
116 | 116 | function buildFilterRow(field, operator, values) { |
|
117 | 117 | var fieldId = field.replace('.', '_'); |
|
118 | 118 | var filterTable = $("#filters-table"); |
|
119 | 119 | var filterOptions = availableFilters[field]; |
|
120 | 120 | var operators = operatorByType[filterOptions['type']]; |
|
121 | 121 | var filterValues = filterOptions['values']; |
|
122 | 122 | var i, select; |
|
123 | 123 | |
|
124 | 124 | var tr = $('<tr class="filter">').attr('id', 'tr_'+fieldId).html( |
|
125 | 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 | 126 | '<td class="operator"><select id="operators_'+fieldId+'" name="op['+field+']"></td>' + |
|
127 | 127 | '<td class="values"></td>' |
|
128 | 128 | ); |
|
129 | 129 | filterTable.append(tr); |
|
130 | 130 | |
|
131 | 131 | select = tr.find('td.operator select'); |
|
132 | 132 | for (i=0;i<operators.length;i++){ |
|
133 | 133 | var option = $('<option>').val(operators[i]).text(operatorLabels[operators[i]]); |
|
134 | 134 | if (operators[i] == operator) {option.attr('selected', true)}; |
|
135 | 135 | select.append(option); |
|
136 | 136 | } |
|
137 | 137 | select.change(function(){toggleOperator(field)}); |
|
138 | 138 | |
|
139 | 139 | switch (filterOptions['type']){ |
|
140 | 140 | case "list": |
|
141 | 141 | case "list_optional": |
|
142 | 142 | case "list_status": |
|
143 | 143 | case "list_subprojects": |
|
144 | 144 | tr.find('td.values').append( |
|
145 | 145 | '<span style="display:none;"><select class="value" id="values_'+fieldId+'_1" name="v['+field+'][]"></select>' + |
|
146 | 146 | ' <span class="toggle-multiselect"> </span></span>' |
|
147 | 147 | ); |
|
148 | 148 | select = tr.find('td.values select'); |
|
149 | 149 | if (values.length > 1) {select.attr('multiple', true)}; |
|
150 | 150 | for (i=0;i<filterValues.length;i++){ |
|
151 | 151 | var filterValue = filterValues[i]; |
|
152 | 152 | var option = $('<option>'); |
|
153 | 153 | if ($.isArray(filterValue)) { |
|
154 | 154 | option.val(filterValue[1]).text(filterValue[0]); |
|
155 | 155 | if ($.inArray(filterValue[1], values) > -1) {option.attr('selected', true);} |
|
156 | 156 | } else { |
|
157 | 157 | option.val(filterValue).text(filterValue); |
|
158 | 158 | if ($.inArray(filterValue, values) > -1) {option.attr('selected', true);} |
|
159 | 159 | } |
|
160 | 160 | select.append(option); |
|
161 | 161 | } |
|
162 | 162 | break; |
|
163 | 163 | case "date": |
|
164 | 164 | case "date_past": |
|
165 | 165 | tr.find('td.values').append( |
|
166 | 166 | '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="10" class="value date_value" /></span>' + |
|
167 | 167 | ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="10" class="value date_value" /></span>' + |
|
168 | 168 | ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="3" class="value" /> '+labelDayPlural+'</span>' |
|
169 | 169 | ); |
|
170 | 170 | $('#values_'+fieldId+'_1').val(values[0]).datepicker(datepickerOptions); |
|
171 | 171 | $('#values_'+fieldId+'_2').val(values[1]).datepicker(datepickerOptions); |
|
172 | 172 | $('#values_'+fieldId).val(values[0]); |
|
173 | 173 | break; |
|
174 | 174 | case "string": |
|
175 | 175 | case "text": |
|
176 | 176 | tr.find('td.values').append( |
|
177 | 177 | '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'" size="30" class="value" /></span>' |
|
178 | 178 | ); |
|
179 | 179 | $('#values_'+fieldId).val(values[0]); |
|
180 | 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 | 195 | case "integer": |
|
182 | 196 | case "float": |
|
183 | 197 | tr.find('td.values').append( |
|
184 | 198 | '<span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_1" size="6" class="value" /></span>' + |
|
185 | 199 | ' <span style="display:none;"><input type="text" name="v['+field+'][]" id="values_'+fieldId+'_2" size="6" class="value" /></span>' |
|
186 | 200 | ); |
|
187 | 201 | $('#values_'+fieldId+'_1').val(values[0]); |
|
188 | 202 | $('#values_'+fieldId+'_2').val(values[1]); |
|
189 | 203 | break; |
|
190 | 204 | } |
|
191 | 205 | } |
|
192 | 206 | |
|
193 | 207 | function toggleFilter(field) { |
|
194 | 208 | var fieldId = field.replace('.', '_'); |
|
195 | 209 | if ($('#cb_' + fieldId).is(':checked')) { |
|
196 | 210 | $("#operators_" + fieldId).show().removeAttr('disabled'); |
|
197 | 211 | toggleOperator(field); |
|
198 | 212 | } else { |
|
199 | 213 | $("#operators_" + fieldId).hide().attr('disabled', true); |
|
200 | 214 | enableValues(field, []); |
|
201 | 215 | } |
|
202 | 216 | } |
|
203 | 217 | |
|
204 | 218 | function enableValues(field, indexes) { |
|
205 | 219 | var fieldId = field.replace('.', '_'); |
|
206 | 220 | $('#tr_'+fieldId+' td.values .value').each(function(index) { |
|
207 | 221 | if ($.inArray(index, indexes) >= 0) { |
|
208 | 222 | $(this).removeAttr('disabled'); |
|
209 | 223 | $(this).parents('span').first().show(); |
|
210 | 224 | } else { |
|
211 | 225 | $(this).val(''); |
|
212 | 226 | $(this).attr('disabled', true); |
|
213 | 227 | $(this).parents('span').first().hide(); |
|
214 | 228 | } |
|
215 | 229 | |
|
216 | 230 | if ($(this).hasClass('group')) { |
|
217 | 231 | $(this).addClass('open'); |
|
218 | 232 | } else { |
|
219 | 233 | $(this).show(); |
|
220 | 234 | } |
|
221 | 235 | }); |
|
222 | 236 | } |
|
223 | 237 | |
|
224 | 238 | function toggleOperator(field) { |
|
225 | 239 | var fieldId = field.replace('.', '_'); |
|
226 | 240 | var operator = $("#operators_" + fieldId); |
|
227 | 241 | switch (operator.val()) { |
|
228 | 242 | case "!*": |
|
229 | 243 | case "*": |
|
230 | 244 | case "t": |
|
231 | 245 | case "w": |
|
232 | 246 | case "o": |
|
233 | 247 | case "c": |
|
234 | 248 | enableValues(field, []); |
|
235 | 249 | break; |
|
236 | 250 | case "><": |
|
237 | 251 | enableValues(field, [0,1]); |
|
238 | 252 | break; |
|
239 | 253 | case "<t+": |
|
240 | 254 | case ">t+": |
|
241 | 255 | case "t+": |
|
242 | 256 | case ">t-": |
|
243 | 257 | case "<t-": |
|
244 | 258 | case "t-": |
|
245 | 259 | enableValues(field, [2]); |
|
246 | 260 | break; |
|
261 | case "=p": | |
|
262 | case "=!p": | |
|
263 | enableValues(field, [1]); | |
|
264 | break; | |
|
247 | 265 | default: |
|
248 | 266 | enableValues(field, [0]); |
|
249 | 267 | break; |
|
250 | 268 | } |
|
251 | 269 | } |
|
252 | 270 | |
|
253 | 271 | function toggleMultiSelect(el) { |
|
254 | 272 | if (el.attr('multiple')) { |
|
255 | 273 | el.removeAttr('multiple'); |
|
256 | 274 | } else { |
|
257 | 275 | el.attr('multiple', true); |
|
258 | 276 | } |
|
259 | 277 | } |
|
260 | 278 | |
|
261 | 279 | function submit_query_form(id) { |
|
262 | 280 | selectAllOptions("selected_columns"); |
|
263 | 281 | $('#'+id).submit(); |
|
264 | 282 | } |
|
265 | 283 | |
|
266 | 284 | var fileFieldCount = 1; |
|
267 | 285 | function addFileField() { |
|
268 | 286 | var fields = $('#attachments_fields'); |
|
269 | 287 | if (fields.children().length >= 10) return false; |
|
270 | 288 | fileFieldCount++; |
|
271 | 289 | var s = fields.children('span').first().clone(); |
|
272 | 290 | s.children('input.file').attr('name', "attachments[" + fileFieldCount + "][file]").val(''); |
|
273 | 291 | s.children('input.description').attr('name', "attachments[" + fileFieldCount + "][description]").val(''); |
|
274 | 292 | fields.append(s); |
|
275 | 293 | } |
|
276 | 294 | |
|
277 | 295 | function removeFileField(el) { |
|
278 | 296 | var fields = $('#attachments_fields'); |
|
279 | 297 | var s = $(el).parents('span').first(); |
|
280 | 298 | if (fields.children().length > 1) { |
|
281 | 299 | s.remove(); |
|
282 | 300 | } else { |
|
283 | 301 | s.children('input.file').val(''); |
|
284 | 302 | s.children('input.description').val(''); |
|
285 | 303 | } |
|
286 | 304 | } |
|
287 | 305 | |
|
288 | 306 | function checkFileSize(el, maxSize, message) { |
|
289 | 307 | var files = el.files; |
|
290 | 308 | if (files) { |
|
291 | 309 | for (var i=0; i<files.length; i++) { |
|
292 | 310 | if (files[i].size > maxSize) { |
|
293 | 311 | alert(message); |
|
294 | 312 | el.value = ""; |
|
295 | 313 | } |
|
296 | 314 | } |
|
297 | 315 | } |
|
298 | 316 | } |
|
299 | 317 | |
|
300 | 318 | function showTab(name) { |
|
301 | 319 | $('div#content .tab-content').hide(); |
|
302 | 320 | $('div.tabs a').removeClass('selected'); |
|
303 | 321 | $('#tab-content-' + name).show(); |
|
304 | 322 | $('#tab-' + name).addClass('selected'); |
|
305 | 323 | return false; |
|
306 | 324 | } |
|
307 | 325 | |
|
308 | 326 | function moveTabRight(el) { |
|
309 | 327 | var lis = $(el).parents('div.tabs').first().find('ul').children(); |
|
310 | 328 | var tabsWidth = 0; |
|
311 | 329 | var i = 0; |
|
312 | 330 | lis.each(function(){ |
|
313 | 331 | if ($(this).is(':visible')) { |
|
314 | 332 | tabsWidth += $(this).width() + 6; |
|
315 | 333 | } |
|
316 | 334 | }); |
|
317 | 335 | if (tabsWidth < $(el).parents('div.tabs').first().width() - 60) { return; } |
|
318 | 336 | while (i<lis.length && !lis.eq(i).is(':visible')) { i++; } |
|
319 | 337 | lis.eq(i).hide(); |
|
320 | 338 | } |
|
321 | 339 | |
|
322 | 340 | function moveTabLeft(el) { |
|
323 | 341 | var lis = $(el).parents('div.tabs').first().find('ul').children(); |
|
324 | 342 | var i = 0; |
|
325 | 343 | while (i<lis.length && !lis.eq(i).is(':visible')) { i++; } |
|
326 | 344 | if (i>0) { |
|
327 | 345 | lis.eq(i-1).show(); |
|
328 | 346 | } |
|
329 | 347 | } |
|
330 | 348 | |
|
331 | 349 | function displayTabsButtons() { |
|
332 | 350 | var lis; |
|
333 | 351 | var tabsWidth = 0; |
|
334 | 352 | var el; |
|
335 | 353 | $('div.tabs').each(function() { |
|
336 | 354 | el = $(this); |
|
337 | 355 | lis = el.find('ul').children(); |
|
338 | 356 | lis.each(function(){ |
|
339 | 357 | if ($(this).is(':visible')) { |
|
340 | 358 | tabsWidth += $(this).width() + 6; |
|
341 | 359 | } |
|
342 | 360 | }); |
|
343 | 361 | if ((tabsWidth < el.width() - 60) && (lis.first().is(':visible'))) { |
|
344 | 362 | el.find('div.tabs-buttons').hide(); |
|
345 | 363 | } else { |
|
346 | 364 | el.find('div.tabs-buttons').show(); |
|
347 | 365 | } |
|
348 | 366 | }); |
|
349 | 367 | } |
|
350 | 368 | |
|
351 | 369 | function setPredecessorFieldsVisibility() { |
|
352 | 370 | var relationType = $('#relation_relation_type'); |
|
353 | 371 | if (relationType.val() == "precedes" || relationType.val() == "follows") { |
|
354 | 372 | $('#predecessor_fields').show(); |
|
355 | 373 | } else { |
|
356 | 374 | $('#predecessor_fields').hide(); |
|
357 | 375 | } |
|
358 | 376 | } |
|
359 | 377 | |
|
360 | 378 | function showModal(id, width) { |
|
361 | 379 | var el = $('#'+id).first(); |
|
362 | 380 | if (el.length == 0 || el.is(':visible')) {return;} |
|
363 | 381 | var title = el.find('h3.title').text(); |
|
364 | 382 | el.dialog({ |
|
365 | 383 | width: width, |
|
366 | 384 | modal: true, |
|
367 | 385 | resizable: false, |
|
368 | 386 | dialogClass: 'modal', |
|
369 | 387 | title: title |
|
370 | 388 | }); |
|
371 | 389 | el.find("input[type=text], input[type=submit]").first().focus(); |
|
372 | 390 | } |
|
373 | 391 | |
|
374 | 392 | function hideModal(el) { |
|
375 | 393 | var modal; |
|
376 | 394 | if (el) { |
|
377 | 395 | modal = $(el).parents('.ui-dialog-content'); |
|
378 | 396 | } else { |
|
379 | 397 | modal = $('#ajax-modal'); |
|
380 | 398 | } |
|
381 | 399 | modal.dialog("close"); |
|
382 | 400 | } |
|
383 | 401 | |
|
384 | 402 | function submitPreview(url, form, target) { |
|
385 | 403 | $.ajax({ |
|
386 | 404 | url: url, |
|
387 | 405 | type: 'post', |
|
388 | 406 | data: $('#'+form).serialize(), |
|
389 | 407 | success: function(data){ |
|
390 | 408 | $('#'+target).html(data); |
|
391 | 409 | } |
|
392 | 410 | }); |
|
393 | 411 | } |
|
394 | 412 | |
|
395 | 413 | function collapseScmEntry(id) { |
|
396 | 414 | $('.'+id).each(function() { |
|
397 | 415 | if ($(this).hasClass('open')) { |
|
398 | 416 | collapseScmEntry($(this).attr('id')); |
|
399 | 417 | } |
|
400 | 418 | $(this).hide(); |
|
401 | 419 | }); |
|
402 | 420 | $('#'+id).removeClass('open'); |
|
403 | 421 | } |
|
404 | 422 | |
|
405 | 423 | function expandScmEntry(id) { |
|
406 | 424 | $('.'+id).each(function() { |
|
407 | 425 | $(this).show(); |
|
408 | 426 | if ($(this).hasClass('loaded') && !$(this).hasClass('collapsed')) { |
|
409 | 427 | expandScmEntry($(this).attr('id')); |
|
410 | 428 | } |
|
411 | 429 | }); |
|
412 | 430 | $('#'+id).addClass('open'); |
|
413 | 431 | } |
|
414 | 432 | |
|
415 | 433 | function scmEntryClick(id, url) { |
|
416 | 434 | el = $('#'+id); |
|
417 | 435 | if (el.hasClass('open')) { |
|
418 | 436 | collapseScmEntry(id); |
|
419 | 437 | el.addClass('collapsed'); |
|
420 | 438 | return false; |
|
421 | 439 | } else if (el.hasClass('loaded')) { |
|
422 | 440 | expandScmEntry(id); |
|
423 | 441 | el.removeClass('collapsed'); |
|
424 | 442 | return false; |
|
425 | 443 | } |
|
426 | 444 | if (el.hasClass('loading')) { |
|
427 | 445 | return false; |
|
428 | 446 | } |
|
429 | 447 | el.addClass('loading'); |
|
430 | 448 | $.ajax({ |
|
431 | 449 | url: url, |
|
432 | 450 | success: function(data){ |
|
433 | 451 | el.after(data); |
|
434 | 452 | el.addClass('open').addClass('loaded').removeClass('loading'); |
|
435 | 453 | } |
|
436 | 454 | }); |
|
437 | 455 | return true; |
|
438 | 456 | } |
|
439 | 457 | |
|
440 | 458 | function randomKey(size) { |
|
441 | 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 | 460 | var key = ''; |
|
443 | 461 | for (i = 0; i < size; i++) { |
|
444 | 462 | key += chars[Math.floor(Math.random() * chars.length)]; |
|
445 | 463 | } |
|
446 | 464 | return key; |
|
447 | 465 | } |
|
448 | 466 | |
|
449 | 467 | // Can't use Rails' remote select because we need the form data |
|
450 | 468 | function updateIssueFrom(url) { |
|
451 | 469 | $.ajax({ |
|
452 | 470 | url: url, |
|
453 | 471 | type: 'post', |
|
454 | 472 | data: $('#issue-form').serialize() |
|
455 | 473 | }); |
|
456 | 474 | } |
|
457 | 475 | |
|
458 | 476 | function updateBulkEditFrom(url) { |
|
459 | 477 | $.ajax({ |
|
460 | 478 | url: url, |
|
461 | 479 | type: 'post', |
|
462 | 480 | data: $('#bulk_edit_form').serialize() |
|
463 | 481 | }); |
|
464 | 482 | } |
|
465 | 483 | |
|
466 | 484 | function observeAutocompleteField(fieldId, url) { |
|
467 | 485 | $('#'+fieldId).autocomplete({ |
|
468 | 486 | source: url, |
|
469 | 487 | minLength: 2, |
|
470 | 488 | }); |
|
471 | 489 | } |
|
472 | 490 | |
|
473 | 491 | function observeSearchfield(fieldId, targetId, url) { |
|
474 | 492 | $('#'+fieldId).each(function() { |
|
475 | 493 | var $this = $(this); |
|
476 | 494 | $this.attr('data-value-was', $this.val()); |
|
477 | 495 | var check = function() { |
|
478 | 496 | var val = $this.val(); |
|
479 | 497 | if ($this.attr('data-value-was') != val){ |
|
480 | 498 | $this.attr('data-value-was', val); |
|
481 | 499 | if (val != '') { |
|
482 | 500 | $.ajax({ |
|
483 | 501 | url: url, |
|
484 | 502 | type: 'get', |
|
485 | 503 | data: {q: $this.val()}, |
|
486 | 504 | success: function(data){ $('#'+targetId).html(data); }, |
|
487 | 505 | beforeSend: function(){ $this.addClass('ajax-loading'); }, |
|
488 | 506 | complete: function(){ $this.removeClass('ajax-loading'); } |
|
489 | 507 | }); |
|
490 | 508 | } |
|
491 | 509 | } |
|
492 | 510 | }; |
|
493 | 511 | var reset = function() { |
|
494 | 512 | if (timer) { |
|
495 | 513 | clearInterval(timer); |
|
496 | 514 | timer = setInterval(check, 300); |
|
497 | 515 | } |
|
498 | 516 | }; |
|
499 | 517 | var timer = setInterval(check, 300); |
|
500 | 518 | $this.bind('keyup click mousemove', reset); |
|
501 | 519 | }); |
|
502 | 520 | } |
|
503 | 521 | |
|
504 | 522 | function observeProjectModules() { |
|
505 | 523 | var f = function() { |
|
506 | 524 | /* Hides trackers and issues custom fields on the new project form when issue_tracking module is disabled */ |
|
507 | 525 | if ($('#project_enabled_module_names_issue_tracking').attr('checked')) { |
|
508 | 526 | $('#project_trackers').show(); |
|
509 | 527 | }else{ |
|
510 | 528 | $('#project_trackers').hide(); |
|
511 | 529 | } |
|
512 | 530 | }; |
|
513 | 531 | |
|
514 | 532 | $(window).load(f); |
|
515 | 533 | $('#project_enabled_module_names_issue_tracking').change(f); |
|
516 | 534 | } |
|
517 | 535 | |
|
518 | 536 | function initMyPageSortable(list, url) { |
|
519 | 537 | $('#list-'+list).sortable({ |
|
520 | 538 | connectWith: '.block-receiver', |
|
521 | 539 | tolerance: 'pointer', |
|
522 | 540 | update: function(){ |
|
523 | 541 | $.ajax({ |
|
524 | 542 | url: url, |
|
525 | 543 | type: 'post', |
|
526 | 544 | data: {'blocks': $.map($('#list-'+list).children(), function(el){return $(el).attr('id');})} |
|
527 | 545 | }); |
|
528 | 546 | } |
|
529 | 547 | }); |
|
530 | 548 | $("#list-top, #list-left, #list-right").disableSelection(); |
|
531 | 549 | } |
|
532 | 550 | |
|
533 | 551 | var warnLeavingUnsavedMessage; |
|
534 | 552 | function warnLeavingUnsaved(message) { |
|
535 | 553 | warnLeavingUnsavedMessage = message; |
|
536 | 554 | |
|
537 | 555 | $('form').submit(function(){ |
|
538 | 556 | $('textarea').removeData('changed'); |
|
539 | 557 | }); |
|
540 | 558 | $('textarea').change(function(){ |
|
541 | 559 | $(this).data('changed', 'changed'); |
|
542 | 560 | }); |
|
543 | 561 | window.onbeforeunload = function(){ |
|
544 | 562 | var warn = false; |
|
545 | 563 | $('textarea').blur().each(function(){ |
|
546 | 564 | if ($(this).data('changed')) { |
|
547 | 565 | warn = true; |
|
548 | 566 | } |
|
549 | 567 | }); |
|
550 | 568 | if (warn) {return warnLeavingUnsavedMessage;} |
|
551 | 569 | }; |
|
552 | 570 | }; |
|
553 | 571 | |
|
554 | 572 | $(document).ready(function(){ |
|
555 | 573 | $('#ajax-indicator').bind('ajaxSend', function(){ |
|
556 | 574 | if ($('.ajax-loading').length == 0) { |
|
557 | 575 | $('#ajax-indicator').show(); |
|
558 | 576 | } |
|
559 | 577 | }); |
|
560 | 578 | $('#ajax-indicator').bind('ajaxStop', function(){ |
|
561 | 579 | $('#ajax-indicator').hide(); |
|
562 | 580 | }); |
|
563 | 581 | }); |
|
564 | 582 | |
|
565 | 583 | function hideOnLoad() { |
|
566 | 584 | $('.hol').hide(); |
|
567 | 585 | } |
|
568 | 586 | |
|
569 | 587 | function addFormObserversForDoubleSubmit() { |
|
570 | 588 | $('form[method=post]').each(function() { |
|
571 | 589 | if (!$(this).hasClass('multiple-submit')) { |
|
572 | 590 | $(this).submit(function(form_submission) { |
|
573 | 591 | if ($(form_submission.target).attr('data-submitted')) { |
|
574 | 592 | form_submission.preventDefault(); |
|
575 | 593 | } else { |
|
576 | 594 | $(form_submission.target).attr('data-submitted', true); |
|
577 | 595 | } |
|
578 | 596 | }); |
|
579 | 597 | } |
|
580 | 598 | }); |
|
581 | 599 | } |
|
582 | 600 | |
|
583 | 601 | $(document).ready(hideOnLoad); |
|
584 | 602 | $(document).ready(addFormObserversForDoubleSubmit); |
@@ -1,1127 +1,1130 | |||
|
1 | 1 | html {overflow-y:scroll;} |
|
2 | 2 | body { font-family: Verdana, sans-serif; font-size: 12px; color:#484848; margin: 0; padding: 0; min-width: 900px; } |
|
3 | 3 | |
|
4 | 4 | h1, h2, h3, h4 {font-family: "Trebuchet MS", Verdana, sans-serif;padding: 2px 10px 1px 0px;margin: 0 0 10px 0;} |
|
5 | 5 | #content h1, h2, h3, h4 {color: #555;} |
|
6 | 6 | h2, .wiki h1 {font-size: 20px;} |
|
7 | 7 | h3, .wiki h2 {font-size: 16px;} |
|
8 | 8 | h4, .wiki h3 {font-size: 13px;} |
|
9 | 9 | h4 {border-bottom: 1px dotted #bbb;} |
|
10 | 10 | |
|
11 | 11 | /***** Layout *****/ |
|
12 | 12 | #wrapper {background: white;} |
|
13 | 13 | |
|
14 | 14 | #top-menu {background: #3E5B76; color: #fff; height:1.8em; font-size: 0.8em; padding: 2px 2px 0px 6px;} |
|
15 | 15 | #top-menu ul {margin: 0; padding: 0;} |
|
16 | 16 | #top-menu li { |
|
17 | 17 | float:left; |
|
18 | 18 | list-style-type:none; |
|
19 | 19 | margin: 0px 0px 0px 0px; |
|
20 | 20 | padding: 0px 0px 0px 0px; |
|
21 | 21 | white-space:nowrap; |
|
22 | 22 | } |
|
23 | 23 | #top-menu a {color: #fff; margin-right: 8px; font-weight: bold;} |
|
24 | 24 | #top-menu #loggedas { float: right; margin-right: 0.5em; color: #fff; } |
|
25 | 25 | |
|
26 | 26 | #account {float:right;} |
|
27 | 27 | |
|
28 | 28 | #header {height:5.3em;margin:0;background-color:#628DB6;color:#f8f8f8; padding: 4px 8px 0px 6px; position:relative;} |
|
29 | 29 | #header a {color:#f8f8f8;} |
|
30 | 30 | #header h1 a.ancestor { font-size: 80%; } |
|
31 | 31 | #quick-search {float:right;} |
|
32 | 32 | |
|
33 | 33 | #main-menu {position: absolute; bottom: 0px; left:6px; margin-right: -500px;} |
|
34 | 34 | #main-menu ul {margin: 0; padding: 0;} |
|
35 | 35 | #main-menu li { |
|
36 | 36 | float:left; |
|
37 | 37 | list-style-type:none; |
|
38 | 38 | margin: 0px 2px 0px 0px; |
|
39 | 39 | padding: 0px 0px 0px 0px; |
|
40 | 40 | white-space:nowrap; |
|
41 | 41 | } |
|
42 | 42 | #main-menu li a { |
|
43 | 43 | display: block; |
|
44 | 44 | color: #fff; |
|
45 | 45 | text-decoration: none; |
|
46 | 46 | font-weight: bold; |
|
47 | 47 | margin: 0; |
|
48 | 48 | padding: 4px 10px 4px 10px; |
|
49 | 49 | } |
|
50 | 50 | #main-menu li a:hover {background:#759FCF; color:#fff;} |
|
51 | 51 | #main-menu li a.selected, #main-menu li a.selected:hover {background:#fff; color:#555;} |
|
52 | 52 | |
|
53 | 53 | #admin-menu ul {margin: 0; padding: 0;} |
|
54 | 54 | #admin-menu li {margin: 0; padding: 0 0 6px 0; list-style-type:none;} |
|
55 | 55 | |
|
56 | 56 | #admin-menu a { background-position: 0% 40%; background-repeat: no-repeat; padding-left: 20px; padding-top: 2px; padding-bottom: 3px;} |
|
57 | 57 | #admin-menu a.projects { background-image: url(../images/projects.png); } |
|
58 | 58 | #admin-menu a.users { background-image: url(../images/user.png); } |
|
59 | 59 | #admin-menu a.groups { background-image: url(../images/group.png); } |
|
60 | 60 | #admin-menu a.roles { background-image: url(../images/database_key.png); } |
|
61 | 61 | #admin-menu a.trackers { background-image: url(../images/ticket.png); } |
|
62 | 62 | #admin-menu a.issue_statuses { background-image: url(../images/ticket_edit.png); } |
|
63 | 63 | #admin-menu a.workflows { background-image: url(../images/ticket_go.png); } |
|
64 | 64 | #admin-menu a.custom_fields { background-image: url(../images/textfield.png); } |
|
65 | 65 | #admin-menu a.enumerations { background-image: url(../images/text_list_bullets.png); } |
|
66 | 66 | #admin-menu a.settings { background-image: url(../images/changeset.png); } |
|
67 | 67 | #admin-menu a.plugins { background-image: url(../images/plugin.png); } |
|
68 | 68 | #admin-menu a.info { background-image: url(../images/help.png); } |
|
69 | 69 | #admin-menu a.server_authentication { background-image: url(../images/server_key.png); } |
|
70 | 70 | |
|
71 | 71 | #main {background-color:#EEEEEE;} |
|
72 | 72 | |
|
73 | 73 | #sidebar{ float: right; width: 22%; position: relative; z-index: 9; padding: 0; margin: 0;} |
|
74 | 74 | * html #sidebar{ width: 22%; } |
|
75 | 75 | #sidebar h3{ font-size: 14px; margin-top:14px; color: #666; } |
|
76 | 76 | #sidebar hr{ width: 100%; margin: 0 auto; height: 1px; background: #ccc; border: 0; } |
|
77 | 77 | * html #sidebar hr{ width: 95%; position: relative; left: -6px; color: #ccc; } |
|
78 | 78 | #sidebar .contextual { margin-right: 1em; } |
|
79 | 79 | |
|
80 | 80 | #content { width: 75%; background-color: #fff; margin: 0px; border-right: 1px solid #ddd; padding: 6px 10px 10px 10px; z-index: 10; } |
|
81 | 81 | * html #content{ width: 75%; padding-left: 0; margin-top: 0px; padding: 6px 10px 10px 10px;} |
|
82 | 82 | html>body #content { min-height: 600px; } |
|
83 | 83 | * html body #content { height: 600px; } /* IE */ |
|
84 | 84 | |
|
85 | 85 | #main.nosidebar #sidebar{ display: none; } |
|
86 | 86 | #main.nosidebar #content{ width: auto; border-right: 0; } |
|
87 | 87 | |
|
88 | 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 | 90 | #login-form table {margin-top:5em; padding:1em; margin-left: auto; margin-right: auto; border: 2px solid #FDBF3B; background-color:#FFEBC1; } |
|
91 | 91 | #login-form table td {padding: 6px;} |
|
92 | 92 | #login-form label {font-weight: bold;} |
|
93 | 93 | #login-form input#username, #login-form input#password { width: 300px; } |
|
94 | 94 | |
|
95 | 95 | div.modal { border-radius:5px; background:#fff; z-index:50; padding:4px;} |
|
96 | 96 | div.modal h3.title {display:none;} |
|
97 | 97 | div.modal p.buttons {text-align:right; margin-bottom:0;} |
|
98 | 98 | |
|
99 | 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 | 101 | .clear:after{ content: "."; display: block; height: 0; clear: both; visibility: hidden; } |
|
102 | 102 | |
|
103 | 103 | /***** Links *****/ |
|
104 | 104 | a, a:link, a:visited{ color: #169; text-decoration: none; } |
|
105 | 105 | a:hover, a:active{ color: #c61a1a; text-decoration: underline;} |
|
106 | 106 | a img{ border: 0; } |
|
107 | 107 | |
|
108 | 108 | a.issue.closed, a.issue.closed:link, a.issue.closed:visited { color: #999; text-decoration: line-through; } |
|
109 | 109 | a.project.closed, a.project.closed:link, a.project.closed:visited { color: #999; } |
|
110 | 110 | |
|
111 | 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 | 112 | #sidebar a.selected:hover {text-decoration:none;} |
|
113 | 113 | #admin-menu a {line-height:1.7em;} |
|
114 | 114 | #admin-menu a.selected {padding-left: 20px !important; background-position: 2px 40%;} |
|
115 | 115 | |
|
116 | 116 | a.collapsible {padding-left: 12px; background: url(../images/arrow_expanded.png) no-repeat -3px 40%;} |
|
117 | 117 | a.collapsible.collapsed {background: url(../images/arrow_collapsed.png) no-repeat -5px 40%;} |
|
118 | 118 | |
|
119 | 119 | a#toggle-completed-versions {color:#999;} |
|
120 | 120 | /***** Tables *****/ |
|
121 | 121 | table.list { border: 1px solid #e4e4e4; border-collapse: collapse; width: 100%; margin-bottom: 4px; } |
|
122 | 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 | 124 | table.list td.id { width: 2%; text-align: center;} |
|
125 | 125 | table.list td.checkbox { width: 15px; padding: 2px 0 0 0; } |
|
126 | 126 | table.list td.checkbox input {padding:0px;} |
|
127 | 127 | table.list td.buttons { width: 15%; white-space:nowrap; text-align: right; } |
|
128 | 128 | table.list td.buttons a { padding-right: 0.6em; } |
|
129 | 129 | table.list caption { text-align: left; padding: 0.5em 0.5em 0.5em 0; } |
|
130 | 130 | |
|
131 | 131 | tr.project td.name a { white-space:nowrap; } |
|
132 | 132 | tr.project.closed, tr.project.archived { color: #aaa; } |
|
133 | 133 | tr.project.closed a, tr.project.archived a { color: #aaa; } |
|
134 | 134 | |
|
135 | 135 | tr.project.idnt td.name span {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} |
|
136 | 136 | tr.project.idnt-1 td.name {padding-left: 0.5em;} |
|
137 | 137 | tr.project.idnt-2 td.name {padding-left: 2em;} |
|
138 | 138 | tr.project.idnt-3 td.name {padding-left: 3.5em;} |
|
139 | 139 | tr.project.idnt-4 td.name {padding-left: 5em;} |
|
140 | 140 | tr.project.idnt-5 td.name {padding-left: 6.5em;} |
|
141 | 141 | tr.project.idnt-6 td.name {padding-left: 8em;} |
|
142 | 142 | tr.project.idnt-7 td.name {padding-left: 9.5em;} |
|
143 | 143 | tr.project.idnt-8 td.name {padding-left: 11em;} |
|
144 | 144 | tr.project.idnt-9 td.name {padding-left: 12.5em;} |
|
145 | 145 | |
|
146 | 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; } | |
|
148 | tr.issue td.subject { text-align: left; } | |
|
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, tr.issue td.relations { text-align: left; } | |
|
149 | 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 | 152 | tr.issue.idnt td.subject a {background: url(../images/bullet_arrow_right.png) no-repeat 0 50%; padding-left: 16px;} |
|
152 | 153 | tr.issue.idnt-1 td.subject {padding-left: 0.5em;} |
|
153 | 154 | tr.issue.idnt-2 td.subject {padding-left: 2em;} |
|
154 | 155 | tr.issue.idnt-3 td.subject {padding-left: 3.5em;} |
|
155 | 156 | tr.issue.idnt-4 td.subject {padding-left: 5em;} |
|
156 | 157 | tr.issue.idnt-5 td.subject {padding-left: 6.5em;} |
|
157 | 158 | tr.issue.idnt-6 td.subject {padding-left: 8em;} |
|
158 | 159 | tr.issue.idnt-7 td.subject {padding-left: 9.5em;} |
|
159 | 160 | tr.issue.idnt-8 td.subject {padding-left: 11em;} |
|
160 | 161 | tr.issue.idnt-9 td.subject {padding-left: 12.5em;} |
|
161 | 162 | |
|
162 | 163 | tr.entry { border: 1px solid #f8f8f8; } |
|
163 | 164 | tr.entry td { white-space: nowrap; } |
|
164 | 165 | tr.entry td.filename { width: 30%; } |
|
165 | 166 | tr.entry td.filename_no_report { width: 70%; } |
|
166 | 167 | tr.entry td.size { text-align: right; font-size: 90%; } |
|
167 | 168 | tr.entry td.revision, tr.entry td.author { text-align: center; } |
|
168 | 169 | tr.entry td.age { text-align: right; } |
|
169 | 170 | tr.entry.file td.filename a { margin-left: 16px; } |
|
170 | 171 | tr.entry.file td.filename_no_report a { margin-left: 16px; } |
|
171 | 172 | |
|
172 | 173 | tr span.expander {background-image: url(../images/bullet_toggle_plus.png); padding-left: 8px; margin-left: 0; cursor: pointer;} |
|
173 | 174 | tr.open span.expander {background-image: url(../images/bullet_toggle_minus.png);} |
|
174 | 175 | |
|
175 | 176 | tr.changeset { height: 20px } |
|
176 | 177 | tr.changeset ul, ol { margin-top: 0px; margin-bottom: 0px; } |
|
177 | 178 | tr.changeset td.revision_graph { width: 15%; background-color: #fffffb; } |
|
178 | 179 | tr.changeset td.author { text-align: center; width: 15%; white-space:nowrap;} |
|
179 | 180 | tr.changeset td.committed_on { text-align: center; width: 15%; white-space:nowrap;} |
|
180 | 181 | |
|
181 | 182 | table.files tr.file td { text-align: center; } |
|
182 | 183 | table.files tr.file td.filename { text-align: left; padding-left: 24px; } |
|
183 | 184 | table.files tr.file td.digest { font-size: 80%; } |
|
184 | 185 | |
|
185 | 186 | table.members td.roles, table.memberships td.roles { width: 45%; } |
|
186 | 187 | |
|
187 | 188 | tr.message { height: 2.6em; } |
|
188 | 189 | tr.message td.subject { padding-left: 20px; } |
|
189 | 190 | tr.message td.created_on { white-space: nowrap; } |
|
190 | 191 | tr.message td.last_message { font-size: 80%; white-space: nowrap; } |
|
191 | 192 | tr.message.locked td.subject { background: url(../images/locked.png) no-repeat 0 1px; } |
|
192 | 193 | tr.message.sticky td.subject { background: url(../images/bullet_go.png) no-repeat 0 1px; font-weight: bold; } |
|
193 | 194 | |
|
194 | 195 | tr.version.closed, tr.version.closed a { color: #999; } |
|
195 | 196 | tr.version td.name { padding-left: 20px; } |
|
196 | 197 | tr.version.shared td.name { background: url(../images/link.png) no-repeat 0% 70%; } |
|
197 | 198 | tr.version td.date, tr.version td.status, tr.version td.sharing { text-align: center; white-space:nowrap; } |
|
198 | 199 | |
|
199 | 200 | tr.user td { width:13%; } |
|
200 | 201 | tr.user td.email { width:18%; } |
|
201 | 202 | tr.user td { white-space: nowrap; } |
|
202 | 203 | tr.user.locked, tr.user.registered { color: #aaa; } |
|
203 | 204 | tr.user.locked a, tr.user.registered a { color: #aaa; } |
|
204 | 205 | |
|
205 | 206 | table.permissions td.role {color:#999;font-size:90%;font-weight:normal !important;text-align:center;vertical-align:bottom;} |
|
206 | 207 | |
|
207 | 208 | tr.wiki-page-version td.updated_on, tr.wiki-page-version td.author {text-align:center;} |
|
208 | 209 | |
|
209 | 210 | tr.time-entry { text-align: center; white-space: nowrap; } |
|
210 | 211 | tr.time-entry td.subject, tr.time-entry td.comments { text-align: left; white-space: normal; } |
|
211 | 212 | td.hours { text-align: right; font-weight: bold; padding-right: 0.5em; } |
|
212 | 213 | td.hours .hours-dec { font-size: 0.9em; } |
|
213 | 214 | |
|
214 | 215 | table.plugins td { vertical-align: middle; } |
|
215 | 216 | table.plugins td.configure { text-align: right; padding-right: 1em; } |
|
216 | 217 | table.plugins span.name { font-weight: bold; display: block; margin-bottom: 6px; } |
|
217 | 218 | table.plugins span.description { display: block; font-size: 0.9em; } |
|
218 | 219 | table.plugins span.url { display: block; font-size: 0.9em; } |
|
219 | 220 | |
|
220 | 221 | table.list tbody tr.group td { padding: 0.8em 0 0.5em 0.3em; font-weight: bold; border-bottom: 1px solid #ccc; } |
|
221 | 222 | table.list tbody tr.group span.count { color: #aaa; font-size: 80%; } |
|
222 | 223 | tr.group a.toggle-all { color: #aaa; font-size: 80%; font-weight: normal; display:none;} |
|
223 | 224 | tr.group:hover a.toggle-all { display:inline;} |
|
224 | 225 | a.toggle-all:hover {text-decoration:none;} |
|
225 | 226 | |
|
226 | 227 | table.list tbody tr:hover { background-color:#ffffdd; } |
|
227 | 228 | table.list tbody tr.group:hover { background-color:inherit; } |
|
228 | 229 | table td {padding:2px;} |
|
229 | 230 | table p {margin:0;} |
|
230 | 231 | .odd {background-color:#f6f7f8;} |
|
231 | 232 | .even {background-color: #fff;} |
|
232 | 233 | |
|
233 | 234 | a.sort { padding-right: 16px; background-position: 100% 50%; background-repeat: no-repeat; } |
|
234 | 235 | a.sort.asc { background-image: url(../images/sort_asc.png); } |
|
235 | 236 | a.sort.desc { background-image: url(../images/sort_desc.png); } |
|
236 | 237 | |
|
237 | 238 | table.attributes { width: 100% } |
|
238 | 239 | table.attributes th { vertical-align: top; text-align: left; } |
|
239 | 240 | table.attributes td { vertical-align: top; } |
|
240 | 241 | |
|
241 | 242 | table.boards a.board, h3.comments { background: url(../images/comment.png) no-repeat 0% 50%; padding-left: 20px; } |
|
242 | 243 | table.boards td.topic-count, table.boards td.message-count {text-align:center;} |
|
243 | 244 | table.boards td.last-message {font-size:80%;} |
|
244 | 245 | |
|
245 | 246 | table.messages td.author, table.messages td.created_on, table.messages td.reply-count {text-align:center;} |
|
246 | 247 | |
|
247 | 248 | table.query-columns { |
|
248 | 249 | border-collapse: collapse; |
|
249 | 250 | border: 0; |
|
250 | 251 | } |
|
251 | 252 | |
|
252 | 253 | table.query-columns td.buttons { |
|
253 | 254 | vertical-align: middle; |
|
254 | 255 | text-align: center; |
|
255 | 256 | } |
|
256 | 257 | |
|
257 | 258 | td.center {text-align:center;} |
|
258 | 259 | |
|
259 | 260 | h3.version { background: url(../images/package.png) no-repeat 0% 50%; padding-left: 20px; } |
|
260 | 261 | |
|
261 | 262 | div.issues h3 { background: url(../images/ticket.png) no-repeat 0% 50%; padding-left: 20px; } |
|
262 | 263 | div.members h3 { background: url(../images/group.png) no-repeat 0% 50%; padding-left: 20px; } |
|
263 | 264 | div.news h3 { background: url(../images/news.png) no-repeat 0% 50%; padding-left: 20px; } |
|
264 | 265 | div.projects h3 { background: url(../images/projects.png) no-repeat 0% 50%; padding-left: 20px; } |
|
265 | 266 | |
|
266 | 267 | #watchers ul {margin: 0; padding: 0;} |
|
267 | 268 | #watchers li {list-style-type:none;margin: 0px 2px 0px 0px; padding: 0px 0px 0px 0px;} |
|
268 | 269 | #watchers select {width: 95%; display: block;} |
|
269 | 270 | #watchers a.delete {opacity: 0.4;} |
|
270 | 271 | #watchers a.delete:hover {opacity: 1;} |
|
271 | 272 | #watchers img.gravatar {margin: 0 4px 2px 0;} |
|
272 | 273 | |
|
273 | 274 | span#watchers_inputs {overflow:auto; display:block;} |
|
274 | 275 | span.search_for_watchers {display:block;} |
|
275 | 276 | span.search_for_watchers, span.add_attachment {font-size:80%; line-height:2.5em;} |
|
276 | 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 | 280 | .highlight { background-color: #FCFD8D;} |
|
280 | 281 | .highlight.token-1 { background-color: #faa;} |
|
281 | 282 | .highlight.token-2 { background-color: #afa;} |
|
282 | 283 | .highlight.token-3 { background-color: #aaf;} |
|
283 | 284 | |
|
284 | 285 | .box{ |
|
285 | 286 | padding:6px; |
|
286 | 287 | margin-bottom: 10px; |
|
287 | 288 | background-color:#f6f6f6; |
|
288 | 289 | color:#505050; |
|
289 | 290 | line-height:1.5em; |
|
290 | 291 | border: 1px solid #e4e4e4; |
|
291 | 292 | } |
|
292 | 293 | |
|
293 | 294 | div.square { |
|
294 | 295 | border: 1px solid #999; |
|
295 | 296 | float: left; |
|
296 | 297 | margin: .3em .4em 0 .4em; |
|
297 | 298 | overflow: hidden; |
|
298 | 299 | width: .6em; height: .6em; |
|
299 | 300 | } |
|
300 | 301 | .contextual {float:right; white-space: nowrap; line-height:1.4em;margin-top:5px; padding-left: 10px; font-size:0.9em;} |
|
301 | 302 | .contextual input, .contextual select {font-size:0.9em;} |
|
302 | 303 | .message .contextual { margin-top: 0; } |
|
303 | 304 | |
|
304 | 305 | .splitcontent {overflow:auto;} |
|
305 | 306 | .splitcontentleft{float:left; width:49%;} |
|
306 | 307 | .splitcontentright{float:right; width:49%;} |
|
307 | 308 | form {display: inline;} |
|
308 | 309 | input, select {vertical-align: middle; margin-top: 1px; margin-bottom: 1px;} |
|
309 | 310 | fieldset {border: 1px solid #e4e4e4; margin:0;} |
|
310 | 311 | legend {color: #484848;} |
|
311 | 312 | hr { width: 100%; height: 1px; background: #ccc; border: 0;} |
|
312 | 313 | blockquote { font-style: italic; border-left: 3px solid #e0e0e0; padding-left: 0.6em; margin-left: 2.4em;} |
|
313 | 314 | blockquote blockquote { margin-left: 0;} |
|
314 | 315 | acronym { border-bottom: 1px dotted; cursor: help; } |
|
315 | 316 | textarea.wiki-edit { width: 99%; } |
|
316 | 317 | li p {margin-top: 0;} |
|
317 | 318 | div.issue {background:#ffffdd; padding:6px; margin-bottom:6px;border: 1px solid #d7d7d7;} |
|
318 | 319 | p.breadcrumb { font-size: 0.9em; margin: 4px 0 4px 0;} |
|
319 | 320 | p.subtitle { font-size: 0.9em; margin: -6px 0 12px 0; font-style: italic; } |
|
320 | 321 | p.footnote { font-size: 0.9em; margin-top: 0px; margin-bottom: 0px; } |
|
321 | 322 | |
|
322 | 323 | div.issue div.subject div div { padding-left: 16px; } |
|
323 | 324 | div.issue div.subject p {margin: 0; margin-bottom: 0.1em; font-size: 90%; color: #999;} |
|
324 | 325 | div.issue div.subject>div>p { margin-top: 0.5em; } |
|
325 | 326 | div.issue div.subject h3 {margin: 0; margin-bottom: 0.1em;} |
|
326 | 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 | 328 | div.issue .next-prev-links {color:#999;} |
|
328 | 329 | div.issue table.attributes th {width:22%;} |
|
329 | 330 | div.issue table.attributes td {width:28%;} |
|
330 | 331 | |
|
331 | 332 | #issue_tree table.issues, #relations table.issues { border: 0; } |
|
332 | 333 | #issue_tree td.checkbox, #relations td.checkbox {display:none;} |
|
333 | 334 | #relations td.buttons {padding:0;} |
|
334 | 335 | |
|
335 | 336 | fieldset.collapsible { border-width: 1px 0 0 0; font-size: 0.9em; } |
|
336 | 337 | fieldset.collapsible legend { padding-left: 16px; background: url(../images/arrow_expanded.png) no-repeat 0% 40%; cursor:pointer; } |
|
337 | 338 | fieldset.collapsible.collapsed legend { background-image: url(../images/arrow_collapsed.png); } |
|
338 | 339 | |
|
339 | 340 | fieldset#date-range p { margin: 2px 0 2px 0; } |
|
340 | 341 | fieldset#filters table { border-collapse: collapse; } |
|
341 | 342 | fieldset#filters table td { padding: 0; vertical-align: middle; } |
|
342 | 343 | fieldset#filters tr.filter { height: 2.1em; } |
|
343 |
fieldset#filters td.field { width:2 |
|
|
344 |
fieldset#filters td.operator { width:1 |
|
|
344 | fieldset#filters td.field { width:230px; } | |
|
345 | fieldset#filters td.operator { width:180px; } | |
|
346 | fieldset#filters td.operator select {max-width:170px;} | |
|
345 | 347 | fieldset#filters td.values { white-space:nowrap; } |
|
346 | 348 | fieldset#filters td.values select {min-width:130px;} |
|
347 | 349 | fieldset#filters td.values input {height:1em;} |
|
348 | 350 | fieldset#filters td.add-filter { text-align: right; vertical-align: top; } |
|
351 | ||
|
349 | 352 | .toggle-multiselect {background: url(../images/bullet_toggle_plus.png) no-repeat 0% 40%; padding-left:8px; margin-left:0; cursor:pointer;} |
|
350 | 353 | .buttons { font-size: 0.9em; margin-bottom: 1.4em; margin-top: 1em; } |
|
351 | 354 | |
|
352 | 355 | div#issue-changesets {float:right; width:45%; margin-left: 1em; margin-bottom: 1em; background: #fff; padding-left: 1em; font-size: 90%;} |
|
353 | 356 | div#issue-changesets div.changeset { padding: 4px;} |
|
354 | 357 | div#issue-changesets div.changeset { border-bottom: 1px solid #ddd; } |
|
355 | 358 | div#issue-changesets p { margin-top: 0; margin-bottom: 1em;} |
|
356 | 359 | |
|
357 | 360 | .journal ul.details img {margin:0 0 -3px 4px;} |
|
358 | 361 | |
|
359 | 362 | div#activity dl, #search-results { margin-left: 2em; } |
|
360 | 363 | div#activity dd, #search-results dd { margin-bottom: 1em; padding-left: 18px; font-size: 0.9em; } |
|
361 | 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 | 365 | div#activity dt.me .time { border-bottom: 1px solid #999; } |
|
363 | 366 | div#activity dt .time { color: #777; font-size: 80%; } |
|
364 | 367 | div#activity dd .description, #search-results dd .description { font-style: italic; } |
|
365 | 368 | div#activity span.project:after, #search-results span.project:after { content: " -"; } |
|
366 | 369 | div#activity dd span.description, #search-results dd span.description { display:block; color: #808080; } |
|
367 | 370 | |
|
368 | 371 | #search-results dd { margin-bottom: 1em; padding-left: 20px; margin-left:0px; } |
|
369 | 372 | |
|
370 | 373 | div#search-results-counts {float:right;} |
|
371 | 374 | div#search-results-counts ul { margin-top: 0.5em; } |
|
372 | 375 | div#search-results-counts li { list-style-type:none; float: left; margin-left: 1em; } |
|
373 | 376 | |
|
374 | 377 | dt.issue { background-image: url(../images/ticket.png); } |
|
375 | 378 | dt.issue-edit { background-image: url(../images/ticket_edit.png); } |
|
376 | 379 | dt.issue-closed { background-image: url(../images/ticket_checked.png); } |
|
377 | 380 | dt.issue-note { background-image: url(../images/ticket_note.png); } |
|
378 | 381 | dt.changeset { background-image: url(../images/changeset.png); } |
|
379 | 382 | dt.news { background-image: url(../images/news.png); } |
|
380 | 383 | dt.message { background-image: url(../images/message.png); } |
|
381 | 384 | dt.reply { background-image: url(../images/comments.png); } |
|
382 | 385 | dt.wiki-page { background-image: url(../images/wiki_edit.png); } |
|
383 | 386 | dt.attachment { background-image: url(../images/attachment.png); } |
|
384 | 387 | dt.document { background-image: url(../images/document.png); } |
|
385 | 388 | dt.project { background-image: url(../images/projects.png); } |
|
386 | 389 | dt.time-entry { background-image: url(../images/time.png); } |
|
387 | 390 | |
|
388 | 391 | #search-results dt.issue.closed { background-image: url(../images/ticket_checked.png); } |
|
389 | 392 | |
|
390 | 393 | div#roadmap .related-issues { margin-bottom: 1em; } |
|
391 | 394 | div#roadmap .related-issues td.checkbox { display: none; } |
|
392 | 395 | div#roadmap .wiki h1:first-child { display: none; } |
|
393 | 396 | div#roadmap .wiki h1 { font-size: 120%; } |
|
394 | 397 | div#roadmap .wiki h2 { font-size: 110%; } |
|
395 | 398 | body.controller-versions.action-show div#roadmap .related-issues {width:70%;} |
|
396 | 399 | |
|
397 | 400 | div#version-summary { float:right; width:28%; margin-left: 16px; margin-bottom: 16px; background-color: #fff; } |
|
398 | 401 | div#version-summary fieldset { margin-bottom: 1em; } |
|
399 | 402 | div#version-summary fieldset.time-tracking table { width:100%; } |
|
400 | 403 | div#version-summary th, div#version-summary td.total-hours { text-align: right; } |
|
401 | 404 | |
|
402 | 405 | table#time-report td.hours, table#time-report th.period, table#time-report th.total { text-align: right; padding-right: 0.5em; } |
|
403 | 406 | table#time-report tbody tr.subtotal { font-style: italic; color:#777;} |
|
404 | 407 | table#time-report tbody tr.subtotal td.hours { color:#b0b0b0; } |
|
405 | 408 | table#time-report tbody tr.total { font-weight: bold; background-color:#EEEEEE; border-top:1px solid #e4e4e4;} |
|
406 | 409 | table#time-report .hours-dec { font-size: 0.9em; } |
|
407 | 410 | |
|
408 | 411 | div.wiki-page .contextual a {opacity: 0.4} |
|
409 | 412 | div.wiki-page .contextual a:hover {opacity: 1} |
|
410 | 413 | |
|
411 | 414 | form .attributes select { width: 60%; } |
|
412 | 415 | input#issue_subject { width: 99%; } |
|
413 | 416 | select#issue_done_ratio { width: 95px; } |
|
414 | 417 | |
|
415 | 418 | ul.projects {margin:0; padding-left:1em;} |
|
416 | 419 | ul.projects ul {padding-left:1.6em;} |
|
417 | 420 | ul.projects.root {margin:0; padding:0;} |
|
418 | 421 | ul.projects li {list-style-type:none;} |
|
419 | 422 | |
|
420 | 423 | #projects-index ul.projects ul.projects { border-left: 3px solid #e0e0e0; padding-left:1em;} |
|
421 | 424 | #projects-index ul.projects li.root {margin-bottom: 1em;} |
|
422 | 425 | #projects-index ul.projects li.child {margin-top: 1em;} |
|
423 | 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 | 427 | .my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; } |
|
425 | 428 | |
|
426 | 429 | #notified-projects ul, #tracker_project_ids ul {max-height:250px; overflow-y:auto;} |
|
427 | 430 | |
|
428 | 431 | #related-issues li img {vertical-align:middle;} |
|
429 | 432 | |
|
430 | 433 | ul.properties {padding:0; font-size: 0.9em; color: #777;} |
|
431 | 434 | ul.properties li {list-style-type:none;} |
|
432 | 435 | ul.properties li span {font-style:italic;} |
|
433 | 436 | |
|
434 | 437 | .total-hours { font-size: 110%; font-weight: bold; } |
|
435 | 438 | .total-hours span.hours-int { font-size: 120%; } |
|
436 | 439 | |
|
437 | 440 | .autoscroll {overflow-x: auto; padding:1px; margin-bottom: 1.2em;} |
|
438 | 441 | #user_login, #user_firstname, #user_lastname, #user_mail, #my_account_form select, #user_form select, #user_identity_url { width: 90%; } |
|
439 | 442 | |
|
440 | 443 | #workflow_copy_form select { width: 200px; } |
|
441 | 444 | table.transitions td.enabled {background: #bfb;} |
|
442 | 445 | table.fields_permissions select {font-size:90%} |
|
443 | 446 | table.fields_permissions td.readonly {background:#ddd;} |
|
444 | 447 | table.fields_permissions td.required {background:#d88;} |
|
445 | 448 | |
|
446 | 449 | textarea#custom_field_possible_values {width: 99%} |
|
447 | 450 | input#content_comments {width: 99%} |
|
448 | 451 | |
|
449 | 452 | .pagination {font-size: 90%} |
|
450 | 453 | p.pagination {margin-top:8px;} |
|
451 | 454 | |
|
452 | 455 | /***** Tabular forms ******/ |
|
453 | 456 | .tabular p{ |
|
454 | 457 | margin: 0; |
|
455 | 458 | padding: 3px 0 3px 0; |
|
456 | 459 | padding-left: 180px; /* width of left column containing the label elements */ |
|
457 | 460 | min-height: 1.8em; |
|
458 | 461 | clear:left; |
|
459 | 462 | } |
|
460 | 463 | |
|
461 | 464 | html>body .tabular p {overflow:hidden;} |
|
462 | 465 | |
|
463 | 466 | .tabular label{ |
|
464 | 467 | font-weight: bold; |
|
465 | 468 | float: left; |
|
466 | 469 | text-align: right; |
|
467 | 470 | /* width of left column */ |
|
468 | 471 | margin-left: -180px; |
|
469 | 472 | /* width of labels. Should be smaller than left column to create some right margin */ |
|
470 | 473 | width: 175px; |
|
471 | 474 | } |
|
472 | 475 | |
|
473 | 476 | .tabular label.floating{ |
|
474 | 477 | font-weight: normal; |
|
475 | 478 | margin-left: 0px; |
|
476 | 479 | text-align: left; |
|
477 | 480 | width: 270px; |
|
478 | 481 | } |
|
479 | 482 | |
|
480 | 483 | .tabular label.block{ |
|
481 | 484 | font-weight: normal; |
|
482 | 485 | margin-left: 0px !important; |
|
483 | 486 | text-align: left; |
|
484 | 487 | float: none; |
|
485 | 488 | display: block; |
|
486 | 489 | width: auto; |
|
487 | 490 | } |
|
488 | 491 | |
|
489 | 492 | .tabular label.inline{ |
|
490 | 493 | float:none; |
|
491 | 494 | margin-left: 5px !important; |
|
492 | 495 | width: auto; |
|
493 | 496 | } |
|
494 | 497 | |
|
495 | 498 | label.no-css { |
|
496 | 499 | font-weight: inherit; |
|
497 | 500 | float:none; |
|
498 | 501 | text-align:left; |
|
499 | 502 | margin-left:0px; |
|
500 | 503 | width:auto; |
|
501 | 504 | } |
|
502 | 505 | input#time_entry_comments { width: 90%;} |
|
503 | 506 | |
|
504 | 507 | #preview fieldset {margin-top: 1em; background: url(../images/draft.png)} |
|
505 | 508 | |
|
506 | 509 | .tabular.settings p{ padding-left: 300px; } |
|
507 | 510 | .tabular.settings label{ margin-left: -300px; width: 295px; } |
|
508 | 511 | .tabular.settings textarea { width: 99%; } |
|
509 | 512 | |
|
510 | 513 | .settings.enabled_scm table {width:100%} |
|
511 | 514 | .settings.enabled_scm td.scm_name{ font-weight: bold; } |
|
512 | 515 | |
|
513 | 516 | fieldset.settings label { display: block; } |
|
514 | 517 | fieldset#notified_events .parent { padding-left: 20px; } |
|
515 | 518 | |
|
516 | 519 | span.required {color: #bb0000;} |
|
517 | 520 | .summary {font-style: italic;} |
|
518 | 521 | |
|
519 | 522 | #attachments_fields input.description {margin-left: 8px; width:340px;} |
|
520 | 523 | #attachments_fields span {display:block; white-space:nowrap;} |
|
521 | 524 | #attachments_fields img {vertical-align: middle;} |
|
522 | 525 | |
|
523 | 526 | div.attachments { margin-top: 12px; } |
|
524 | 527 | div.attachments p { margin:4px 0 2px 0; } |
|
525 | 528 | div.attachments img { vertical-align: middle; } |
|
526 | 529 | div.attachments span.author { font-size: 0.9em; color: #888; } |
|
527 | 530 | |
|
528 | 531 | div.thumbnails {margin-top:0.6em;} |
|
529 | 532 | div.thumbnails div {background:#fff;border:2px solid #ddd;display:inline-block;margin-right:2px;} |
|
530 | 533 | div.thumbnails img {margin: 3px;} |
|
531 | 534 | |
|
532 | 535 | p.other-formats { text-align: right; font-size:0.9em; color: #666; } |
|
533 | 536 | .other-formats span + span:before { content: "| "; } |
|
534 | 537 | |
|
535 | 538 | a.atom { background: url(../images/feed.png) no-repeat 1px 50%; padding: 2px 0px 3px 16px; } |
|
536 | 539 | |
|
537 | 540 | em.info {font-style:normal;font-size:90%;color:#888;display:block;} |
|
538 | 541 | em.info.error {padding-left:20px; background:url(../images/exclamation.png) no-repeat 0 50%;} |
|
539 | 542 | |
|
540 | 543 | textarea.text_cf {width:90%;} |
|
541 | 544 | |
|
542 | 545 | /* Project members tab */ |
|
543 | 546 | div#tab-content-members .splitcontentleft, div#tab-content-memberships .splitcontentleft, div#tab-content-users .splitcontentleft { width: 64% } |
|
544 | 547 | div#tab-content-members .splitcontentright, div#tab-content-memberships .splitcontentright, div#tab-content-users .splitcontentright { width: 34% } |
|
545 | 548 | div#tab-content-members fieldset, div#tab-content-memberships fieldset, div#tab-content-users fieldset { padding:1em; margin-bottom: 1em; } |
|
546 | 549 | div#tab-content-members fieldset legend, div#tab-content-memberships fieldset legend, div#tab-content-users fieldset legend { font-weight: bold; } |
|
547 | 550 | div#tab-content-members fieldset label, div#tab-content-memberships fieldset label, div#tab-content-users fieldset label { display: block; } |
|
548 | 551 | div#tab-content-members fieldset div, div#tab-content-users fieldset div { max-height: 400px; overflow:auto; } |
|
549 | 552 | |
|
550 | 553 | #users_for_watcher {height: 200px; overflow:auto;} |
|
551 | 554 | #users_for_watcher label {display: block;} |
|
552 | 555 | |
|
553 | 556 | table.members td.group { padding-left: 20px; background: url(../images/group.png) no-repeat 0% 50%; } |
|
554 | 557 | |
|
555 | 558 | input#principal_search, input#user_search {width:100%} |
|
556 | 559 | input#principal_search, input#user_search { |
|
557 | 560 | background: url(../images/magnifier.png) no-repeat 2px 50%; padding-left:20px; |
|
558 | 561 | border:1px solid #9EB1C2; border-radius:3px; height:1.5em; width:95%; |
|
559 | 562 | } |
|
560 | 563 | input#principal_search.ajax-loading, input#user_search.ajax-loading { |
|
561 | 564 | background-image: url(../images/loading.gif); |
|
562 | 565 | } |
|
563 | 566 | |
|
564 | 567 | * html div#tab-content-members fieldset div { height: 450px; } |
|
565 | 568 | |
|
566 | 569 | /***** Flash & error messages ****/ |
|
567 | 570 | #errorExplanation, div.flash, .nodata, .warning, .conflict { |
|
568 | 571 | padding: 4px 4px 4px 30px; |
|
569 | 572 | margin-bottom: 12px; |
|
570 | 573 | font-size: 1.1em; |
|
571 | 574 | border: 2px solid; |
|
572 | 575 | } |
|
573 | 576 | |
|
574 | 577 | div.flash {margin-top: 8px;} |
|
575 | 578 | |
|
576 | 579 | div.flash.error, #errorExplanation { |
|
577 | 580 | background: url(../images/exclamation.png) 8px 50% no-repeat; |
|
578 | 581 | background-color: #ffe3e3; |
|
579 | 582 | border-color: #dd0000; |
|
580 | 583 | color: #880000; |
|
581 | 584 | } |
|
582 | 585 | |
|
583 | 586 | div.flash.notice { |
|
584 | 587 | background: url(../images/true.png) 8px 5px no-repeat; |
|
585 | 588 | background-color: #dfffdf; |
|
586 | 589 | border-color: #9fcf9f; |
|
587 | 590 | color: #005f00; |
|
588 | 591 | } |
|
589 | 592 | |
|
590 | 593 | div.flash.warning, .conflict { |
|
591 | 594 | background: url(../images/warning.png) 8px 5px no-repeat; |
|
592 | 595 | background-color: #FFEBC1; |
|
593 | 596 | border-color: #FDBF3B; |
|
594 | 597 | color: #A6750C; |
|
595 | 598 | text-align: left; |
|
596 | 599 | } |
|
597 | 600 | |
|
598 | 601 | .nodata, .warning { |
|
599 | 602 | text-align: center; |
|
600 | 603 | background-color: #FFEBC1; |
|
601 | 604 | border-color: #FDBF3B; |
|
602 | 605 | color: #A6750C; |
|
603 | 606 | } |
|
604 | 607 | |
|
605 | 608 | #errorExplanation ul { font-size: 0.9em;} |
|
606 | 609 | #errorExplanation h2, #errorExplanation p { display: none; } |
|
607 | 610 | |
|
608 | 611 | .conflict-details {font-size:80%;} |
|
609 | 612 | |
|
610 | 613 | /***** Ajax indicator ******/ |
|
611 | 614 | #ajax-indicator { |
|
612 | 615 | position: absolute; /* fixed not supported by IE */ |
|
613 | 616 | background-color:#eee; |
|
614 | 617 | border: 1px solid #bbb; |
|
615 | 618 | top:35%; |
|
616 | 619 | left:40%; |
|
617 | 620 | width:20%; |
|
618 | 621 | font-weight:bold; |
|
619 | 622 | text-align:center; |
|
620 | 623 | padding:0.6em; |
|
621 | 624 | z-index:100; |
|
622 | 625 | opacity: 0.5; |
|
623 | 626 | } |
|
624 | 627 | |
|
625 | 628 | html>body #ajax-indicator { position: fixed; } |
|
626 | 629 | |
|
627 | 630 | #ajax-indicator span { |
|
628 | 631 | background-position: 0% 40%; |
|
629 | 632 | background-repeat: no-repeat; |
|
630 | 633 | background-image: url(../images/loading.gif); |
|
631 | 634 | padding-left: 26px; |
|
632 | 635 | vertical-align: bottom; |
|
633 | 636 | } |
|
634 | 637 | |
|
635 | 638 | /***** Calendar *****/ |
|
636 | 639 | table.cal {border-collapse: collapse; width: 100%; margin: 0px 0 6px 0;border: 1px solid #d7d7d7;} |
|
637 | 640 | table.cal thead th {width: 14%; background-color:#EEEEEE; padding: 4px; } |
|
638 | 641 | table.cal thead th.week-number {width: auto;} |
|
639 | 642 | table.cal tbody tr {height: 100px;} |
|
640 | 643 | table.cal td {border: 1px solid #d7d7d7; vertical-align: top; font-size: 0.9em;} |
|
641 | 644 | table.cal td.week-number { background-color:#EEEEEE; padding: 4px; border:none; font-size: 1em;} |
|
642 | 645 | table.cal td p.day-num {font-size: 1.1em; text-align:right;} |
|
643 | 646 | table.cal td.odd p.day-num {color: #bbb;} |
|
644 | 647 | table.cal td.today {background:#ffffdd;} |
|
645 | 648 | table.cal td.today p.day-num {font-weight: bold;} |
|
646 | 649 | table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;} |
|
647 | 650 | table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;} |
|
648 | 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 | 652 | p.cal.legend span {display:block;} |
|
650 | 653 | |
|
651 | 654 | /***** Tooltips ******/ |
|
652 | 655 | .tooltip{position:relative;z-index:24;} |
|
653 | 656 | .tooltip:hover{z-index:25;color:#000;} |
|
654 | 657 | .tooltip span.tip{display: none; text-align:left;} |
|
655 | 658 | |
|
656 | 659 | div.tooltip:hover span.tip{ |
|
657 | 660 | display:block; |
|
658 | 661 | position:absolute; |
|
659 | 662 | top:12px; left:24px; width:270px; |
|
660 | 663 | border:1px solid #555; |
|
661 | 664 | background-color:#fff; |
|
662 | 665 | padding: 4px; |
|
663 | 666 | font-size: 0.8em; |
|
664 | 667 | color:#505050; |
|
665 | 668 | } |
|
666 | 669 | |
|
667 | 670 | img.ui-datepicker-trigger { |
|
668 | 671 | cursor: pointer; |
|
669 | 672 | vertical-align: middle; |
|
670 | 673 | margin-left: 4px; |
|
671 | 674 | } |
|
672 | 675 | |
|
673 | 676 | /***** Progress bar *****/ |
|
674 | 677 | table.progress { |
|
675 | 678 | border-collapse: collapse; |
|
676 | 679 | border-spacing: 0pt; |
|
677 | 680 | empty-cells: show; |
|
678 | 681 | text-align: center; |
|
679 | 682 | float:left; |
|
680 | 683 | margin: 1px 6px 1px 0px; |
|
681 | 684 | } |
|
682 | 685 | |
|
683 | 686 | table.progress td { height: 1em; } |
|
684 | 687 | table.progress td.closed { background: #BAE0BA none repeat scroll 0%; } |
|
685 | 688 | table.progress td.done { background: #D3EDD3 none repeat scroll 0%; } |
|
686 | 689 | table.progress td.todo { background: #eee none repeat scroll 0%; } |
|
687 | 690 | p.pourcent {font-size: 80%;} |
|
688 | 691 | p.progress-info {clear: left; font-size: 80%; margin-top:-4px; color:#777;} |
|
689 | 692 | |
|
690 | 693 | #roadmap table.progress td { height: 1.2em; } |
|
691 | 694 | /***** Tabs *****/ |
|
692 | 695 | #content .tabs {height: 2.6em; margin-bottom:1.2em; position:relative; overflow:hidden;} |
|
693 | 696 | #content .tabs ul {margin:0; position:absolute; bottom:0; padding-left:0.5em; width: 2000px; border-bottom: 1px solid #bbbbbb;} |
|
694 | 697 | #content .tabs ul li { |
|
695 | 698 | float:left; |
|
696 | 699 | list-style-type:none; |
|
697 | 700 | white-space:nowrap; |
|
698 | 701 | margin-right:4px; |
|
699 | 702 | background:#fff; |
|
700 | 703 | position:relative; |
|
701 | 704 | margin-bottom:-1px; |
|
702 | 705 | } |
|
703 | 706 | #content .tabs ul li a{ |
|
704 | 707 | display:block; |
|
705 | 708 | font-size: 0.9em; |
|
706 | 709 | text-decoration:none; |
|
707 | 710 | line-height:1.3em; |
|
708 | 711 | padding:4px 6px 4px 6px; |
|
709 | 712 | border: 1px solid #ccc; |
|
710 | 713 | border-bottom: 1px solid #bbbbbb; |
|
711 | 714 | background-color: #f6f6f6; |
|
712 | 715 | color:#999; |
|
713 | 716 | font-weight:bold; |
|
714 | 717 | border-top-left-radius:3px; |
|
715 | 718 | border-top-right-radius:3px; |
|
716 | 719 | } |
|
717 | 720 | |
|
718 | 721 | #content .tabs ul li a:hover { |
|
719 | 722 | background-color: #ffffdd; |
|
720 | 723 | text-decoration:none; |
|
721 | 724 | } |
|
722 | 725 | |
|
723 | 726 | #content .tabs ul li a.selected { |
|
724 | 727 | background-color: #fff; |
|
725 | 728 | border: 1px solid #bbbbbb; |
|
726 | 729 | border-bottom: 1px solid #fff; |
|
727 | 730 | color:#444; |
|
728 | 731 | } |
|
729 | 732 | |
|
730 | 733 | #content .tabs ul li a.selected:hover {background-color: #fff;} |
|
731 | 734 | |
|
732 | 735 | div.tabs-buttons { position:absolute; right: 0; width: 48px; height: 24px; background: white; bottom: 0; border-bottom: 1px solid #bbbbbb; } |
|
733 | 736 | |
|
734 | 737 | button.tab-left, button.tab-right { |
|
735 | 738 | font-size: 0.9em; |
|
736 | 739 | cursor: pointer; |
|
737 | 740 | height:24px; |
|
738 | 741 | border: 1px solid #ccc; |
|
739 | 742 | border-bottom: 1px solid #bbbbbb; |
|
740 | 743 | position:absolute; |
|
741 | 744 | padding:4px; |
|
742 | 745 | width: 20px; |
|
743 | 746 | bottom: -1px; |
|
744 | 747 | } |
|
745 | 748 | |
|
746 | 749 | button.tab-left { |
|
747 | 750 | right: 20px; |
|
748 | 751 | background: #eeeeee url(../images/bullet_arrow_left.png) no-repeat 50% 50%; |
|
749 | 752 | border-top-left-radius:3px; |
|
750 | 753 | } |
|
751 | 754 | |
|
752 | 755 | button.tab-right { |
|
753 | 756 | right: 0; |
|
754 | 757 | background: #eeeeee url(../images/bullet_arrow_right.png) no-repeat 50% 50%; |
|
755 | 758 | border-top-right-radius:3px; |
|
756 | 759 | } |
|
757 | 760 | |
|
758 | 761 | /***** Diff *****/ |
|
759 | 762 | .diff_out { background: #fcc; } |
|
760 | 763 | .diff_out span { background: #faa; } |
|
761 | 764 | .diff_in { background: #cfc; } |
|
762 | 765 | .diff_in span { background: #afa; } |
|
763 | 766 | |
|
764 | 767 | .text-diff { |
|
765 | 768 | padding: 1em; |
|
766 | 769 | background-color:#f6f6f6; |
|
767 | 770 | color:#505050; |
|
768 | 771 | border: 1px solid #e4e4e4; |
|
769 | 772 | } |
|
770 | 773 | |
|
771 | 774 | /***** Wiki *****/ |
|
772 | 775 | div.wiki table { |
|
773 | 776 | border-collapse: collapse; |
|
774 | 777 | margin-bottom: 1em; |
|
775 | 778 | } |
|
776 | 779 | |
|
777 | 780 | div.wiki table, div.wiki td, div.wiki th { |
|
778 | 781 | border: 1px solid #bbb; |
|
779 | 782 | padding: 4px; |
|
780 | 783 | } |
|
781 | 784 | |
|
782 | 785 | div.wiki .noborder, div.wiki .noborder td, div.wiki .noborder th {border:0;} |
|
783 | 786 | |
|
784 | 787 | div.wiki .external { |
|
785 | 788 | background-position: 0% 60%; |
|
786 | 789 | background-repeat: no-repeat; |
|
787 | 790 | padding-left: 12px; |
|
788 | 791 | background-image: url(../images/external.png); |
|
789 | 792 | } |
|
790 | 793 | |
|
791 | 794 | div.wiki a.new {color: #b73535;} |
|
792 | 795 | |
|
793 | 796 | div.wiki ul, div.wiki ol {margin-bottom:1em;} |
|
794 | 797 | |
|
795 | 798 | div.wiki pre { |
|
796 | 799 | margin: 1em 1em 1em 1.6em; |
|
797 | 800 | padding: 8px; |
|
798 | 801 | background-color: #fafafa; |
|
799 | 802 | border: 1px solid #e2e2e2; |
|
800 | 803 | width:auto; |
|
801 | 804 | overflow-x: auto; |
|
802 | 805 | overflow-y: hidden; |
|
803 | 806 | } |
|
804 | 807 | |
|
805 | 808 | div.wiki ul.toc { |
|
806 | 809 | background-color: #ffffdd; |
|
807 | 810 | border: 1px solid #e4e4e4; |
|
808 | 811 | padding: 4px; |
|
809 | 812 | line-height: 1.2em; |
|
810 | 813 | margin-bottom: 12px; |
|
811 | 814 | margin-right: 12px; |
|
812 | 815 | margin-left: 0; |
|
813 | 816 | display: table |
|
814 | 817 | } |
|
815 | 818 | * html div.wiki ul.toc { width: 50%; } /* IE6 doesn't autosize div */ |
|
816 | 819 | |
|
817 | 820 | div.wiki ul.toc.right { float: right; margin-left: 12px; margin-right: 0; width: auto; } |
|
818 | 821 | div.wiki ul.toc.left { float: left; margin-right: 12px; margin-left: 0; width: auto; } |
|
819 | 822 | div.wiki ul.toc ul { margin: 0; padding: 0; } |
|
820 | 823 | div.wiki ul.toc li {list-style-type:none; margin: 0; font-size:12px;} |
|
821 | 824 | div.wiki ul.toc li li {margin-left: 1.5em; font-size:10px;} |
|
822 | 825 | div.wiki ul.toc a { |
|
823 | 826 | font-size: 0.9em; |
|
824 | 827 | font-weight: normal; |
|
825 | 828 | text-decoration: none; |
|
826 | 829 | color: #606060; |
|
827 | 830 | } |
|
828 | 831 | div.wiki ul.toc a:hover { color: #c61a1a; text-decoration: underline;} |
|
829 | 832 | |
|
830 | 833 | a.wiki-anchor { display: none; margin-left: 6px; text-decoration: none; } |
|
831 | 834 | a.wiki-anchor:hover { color: #aaa !important; text-decoration: none; } |
|
832 | 835 | h1:hover a.wiki-anchor, h2:hover a.wiki-anchor, h3:hover a.wiki-anchor { display: inline; color: #ddd; } |
|
833 | 836 | |
|
834 | 837 | div.wiki img { vertical-align: middle; } |
|
835 | 838 | |
|
836 | 839 | /***** My page layout *****/ |
|
837 | 840 | .block-receiver { |
|
838 | 841 | border:1px dashed #c0c0c0; |
|
839 | 842 | margin-bottom: 20px; |
|
840 | 843 | padding: 15px 0 15px 0; |
|
841 | 844 | } |
|
842 | 845 | |
|
843 | 846 | .mypage-box { |
|
844 | 847 | margin:0 0 20px 0; |
|
845 | 848 | color:#505050; |
|
846 | 849 | line-height:1.5em; |
|
847 | 850 | } |
|
848 | 851 | |
|
849 | 852 | .handle {cursor: move;} |
|
850 | 853 | |
|
851 | 854 | a.close-icon { |
|
852 | 855 | display:block; |
|
853 | 856 | margin-top:3px; |
|
854 | 857 | overflow:hidden; |
|
855 | 858 | width:12px; |
|
856 | 859 | height:12px; |
|
857 | 860 | background-repeat: no-repeat; |
|
858 | 861 | cursor:pointer; |
|
859 | 862 | background-image:url('../images/close.png'); |
|
860 | 863 | } |
|
861 | 864 | a.close-icon:hover {background-image:url('../images/close_hl.png');} |
|
862 | 865 | |
|
863 | 866 | /***** Gantt chart *****/ |
|
864 | 867 | .gantt_hdr { |
|
865 | 868 | position:absolute; |
|
866 | 869 | top:0; |
|
867 | 870 | height:16px; |
|
868 | 871 | border-top: 1px solid #c0c0c0; |
|
869 | 872 | border-bottom: 1px solid #c0c0c0; |
|
870 | 873 | border-right: 1px solid #c0c0c0; |
|
871 | 874 | text-align: center; |
|
872 | 875 | overflow: hidden; |
|
873 | 876 | } |
|
874 | 877 | |
|
875 | 878 | .gantt_subjects { font-size: 0.8em; } |
|
876 | 879 | .gantt_subjects div { line-height:16px;height:16px;overflow:hidden;white-space:nowrap;text-overflow: ellipsis; } |
|
877 | 880 | |
|
878 | 881 | .task { |
|
879 | 882 | position: absolute; |
|
880 | 883 | height:8px; |
|
881 | 884 | font-size:0.8em; |
|
882 | 885 | color:#888; |
|
883 | 886 | padding:0; |
|
884 | 887 | margin:0; |
|
885 | 888 | line-height:16px; |
|
886 | 889 | white-space:nowrap; |
|
887 | 890 | } |
|
888 | 891 | |
|
889 | 892 | .task.label {width:100%;} |
|
890 | 893 | .task.label.project, .task.label.version { font-weight: bold; } |
|
891 | 894 | |
|
892 | 895 | .task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; } |
|
893 | 896 | .task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; } |
|
894 | 897 | .task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; } |
|
895 | 898 | |
|
896 | 899 | .task_todo.parent { background: #888; border: 1px solid #888; height: 3px;} |
|
897 | 900 | .task_late.parent, .task_done.parent { height: 3px;} |
|
898 | 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 | 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 | 904 | .version.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} |
|
902 | 905 | .version.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} |
|
903 | 906 | .version.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} |
|
904 | 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 | 909 | .project.task_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;} |
|
907 | 910 | .project.task_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;} |
|
908 | 911 | .project.task_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;} |
|
909 | 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 | 914 | .version-behind-schedule a, .issue-behind-schedule a {color: #f66914;} |
|
912 | 915 | .version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;} |
|
913 | 916 | |
|
914 | 917 | /***** Icons *****/ |
|
915 | 918 | .icon { |
|
916 | 919 | background-position: 0% 50%; |
|
917 | 920 | background-repeat: no-repeat; |
|
918 | 921 | padding-left: 20px; |
|
919 | 922 | padding-top: 2px; |
|
920 | 923 | padding-bottom: 3px; |
|
921 | 924 | } |
|
922 | 925 | |
|
923 | 926 | .icon-add { background-image: url(../images/add.png); } |
|
924 | 927 | .icon-edit { background-image: url(../images/edit.png); } |
|
925 | 928 | .icon-copy { background-image: url(../images/copy.png); } |
|
926 | 929 | .icon-duplicate { background-image: url(../images/duplicate.png); } |
|
927 | 930 | .icon-del { background-image: url(../images/delete.png); } |
|
928 | 931 | .icon-move { background-image: url(../images/move.png); } |
|
929 | 932 | .icon-save { background-image: url(../images/save.png); } |
|
930 | 933 | .icon-cancel { background-image: url(../images/cancel.png); } |
|
931 | 934 | .icon-multiple { background-image: url(../images/table_multiple.png); } |
|
932 | 935 | .icon-folder { background-image: url(../images/folder.png); } |
|
933 | 936 | .open .icon-folder { background-image: url(../images/folder_open.png); } |
|
934 | 937 | .icon-package { background-image: url(../images/package.png); } |
|
935 | 938 | .icon-user { background-image: url(../images/user.png); } |
|
936 | 939 | .icon-projects { background-image: url(../images/projects.png); } |
|
937 | 940 | .icon-help { background-image: url(../images/help.png); } |
|
938 | 941 | .icon-attachment { background-image: url(../images/attachment.png); } |
|
939 | 942 | .icon-history { background-image: url(../images/history.png); } |
|
940 | 943 | .icon-time { background-image: url(../images/time.png); } |
|
941 | 944 | .icon-time-add { background-image: url(../images/time_add.png); } |
|
942 | 945 | .icon-stats { background-image: url(../images/stats.png); } |
|
943 | 946 | .icon-warning { background-image: url(../images/warning.png); } |
|
944 | 947 | .icon-fav { background-image: url(../images/fav.png); } |
|
945 | 948 | .icon-fav-off { background-image: url(../images/fav_off.png); } |
|
946 | 949 | .icon-reload { background-image: url(../images/reload.png); } |
|
947 | 950 | .icon-lock { background-image: url(../images/locked.png); } |
|
948 | 951 | .icon-unlock { background-image: url(../images/unlock.png); } |
|
949 | 952 | .icon-checked { background-image: url(../images/true.png); } |
|
950 | 953 | .icon-details { background-image: url(../images/zoom_in.png); } |
|
951 | 954 | .icon-report { background-image: url(../images/report.png); } |
|
952 | 955 | .icon-comment { background-image: url(../images/comment.png); } |
|
953 | 956 | .icon-summary { background-image: url(../images/lightning.png); } |
|
954 | 957 | .icon-server-authentication { background-image: url(../images/server_key.png); } |
|
955 | 958 | .icon-issue { background-image: url(../images/ticket.png); } |
|
956 | 959 | .icon-zoom-in { background-image: url(../images/zoom_in.png); } |
|
957 | 960 | .icon-zoom-out { background-image: url(../images/zoom_out.png); } |
|
958 | 961 | .icon-passwd { background-image: url(../images/textfield_key.png); } |
|
959 | 962 | .icon-test { background-image: url(../images/bullet_go.png); } |
|
960 | 963 | |
|
961 | 964 | .icon-file { background-image: url(../images/files/default.png); } |
|
962 | 965 | .icon-file.text-plain { background-image: url(../images/files/text.png); } |
|
963 | 966 | .icon-file.text-x-c { background-image: url(../images/files/c.png); } |
|
964 | 967 | .icon-file.text-x-csharp { background-image: url(../images/files/csharp.png); } |
|
965 | 968 | .icon-file.text-x-java { background-image: url(../images/files/java.png); } |
|
966 | 969 | .icon-file.text-x-javascript { background-image: url(../images/files/js.png); } |
|
967 | 970 | .icon-file.text-x-php { background-image: url(../images/files/php.png); } |
|
968 | 971 | .icon-file.text-x-ruby { background-image: url(../images/files/ruby.png); } |
|
969 | 972 | .icon-file.text-xml { background-image: url(../images/files/xml.png); } |
|
970 | 973 | .icon-file.text-css { background-image: url(../images/files/css.png); } |
|
971 | 974 | .icon-file.text-html { background-image: url(../images/files/html.png); } |
|
972 | 975 | .icon-file.image-gif { background-image: url(../images/files/image.png); } |
|
973 | 976 | .icon-file.image-jpeg { background-image: url(../images/files/image.png); } |
|
974 | 977 | .icon-file.image-png { background-image: url(../images/files/image.png); } |
|
975 | 978 | .icon-file.image-tiff { background-image: url(../images/files/image.png); } |
|
976 | 979 | .icon-file.application-pdf { background-image: url(../images/files/pdf.png); } |
|
977 | 980 | .icon-file.application-zip { background-image: url(../images/files/zip.png); } |
|
978 | 981 | .icon-file.application-x-gzip { background-image: url(../images/files/zip.png); } |
|
979 | 982 | |
|
980 | 983 | img.gravatar { |
|
981 | 984 | padding: 2px; |
|
982 | 985 | border: solid 1px #d5d5d5; |
|
983 | 986 | background: #fff; |
|
984 | 987 | vertical-align: middle; |
|
985 | 988 | } |
|
986 | 989 | |
|
987 | 990 | div.issue img.gravatar { |
|
988 | 991 | float: left; |
|
989 | 992 | margin: 0 6px 0 0; |
|
990 | 993 | padding: 5px; |
|
991 | 994 | } |
|
992 | 995 | |
|
993 | 996 | div.issue table img.gravatar { |
|
994 | 997 | height: 14px; |
|
995 | 998 | width: 14px; |
|
996 | 999 | padding: 2px; |
|
997 | 1000 | float: left; |
|
998 | 1001 | margin: 0 0.5em 0 0; |
|
999 | 1002 | } |
|
1000 | 1003 | |
|
1001 | 1004 | h2 img.gravatar {margin: -2px 4px -4px 0;} |
|
1002 | 1005 | h3 img.gravatar {margin: -4px 4px -4px 0;} |
|
1003 | 1006 | h4 img.gravatar {margin: -6px 4px -4px 0;} |
|
1004 | 1007 | td.username img.gravatar {margin: 0 0.5em 0 0; vertical-align: top;} |
|
1005 | 1008 | #activity dt img.gravatar {float: left; margin: 0 1em 1em 0;} |
|
1006 | 1009 | /* Used on 12px Gravatar img tags without the icon background */ |
|
1007 | 1010 | .icon-gravatar {float: left; margin-right: 4px;} |
|
1008 | 1011 | |
|
1009 | 1012 | #activity dt, .journal {clear: left;} |
|
1010 | 1013 | |
|
1011 | 1014 | .journal-link {float: right;} |
|
1012 | 1015 | |
|
1013 | 1016 | h2 img { vertical-align:middle; } |
|
1014 | 1017 | |
|
1015 | 1018 | .hascontextmenu { cursor: context-menu; } |
|
1016 | 1019 | |
|
1017 | 1020 | /************* CodeRay styles *************/ |
|
1018 | 1021 | .syntaxhl div {display: inline;} |
|
1019 | 1022 | .syntaxhl .line-numbers {padding: 2px 4px 2px 4px; background-color: #eee; margin:0px 5px 0px 0px;} |
|
1020 | 1023 | .syntaxhl .code pre { overflow: auto } |
|
1021 | 1024 | .syntaxhl .debug { color: white !important; background: blue !important; } |
|
1022 | 1025 | |
|
1023 | 1026 | .syntaxhl .annotation { color:#007 } |
|
1024 | 1027 | .syntaxhl .attribute-name { color:#b48 } |
|
1025 | 1028 | .syntaxhl .attribute-value { color:#700 } |
|
1026 | 1029 | .syntaxhl .binary { color:#509 } |
|
1027 | 1030 | .syntaxhl .char .content { color:#D20 } |
|
1028 | 1031 | .syntaxhl .char .delimiter { color:#710 } |
|
1029 | 1032 | .syntaxhl .char { color:#D20 } |
|
1030 | 1033 | .syntaxhl .class { color:#258; font-weight:bold } |
|
1031 | 1034 | .syntaxhl .class-variable { color:#369 } |
|
1032 | 1035 | .syntaxhl .color { color:#0A0 } |
|
1033 | 1036 | .syntaxhl .comment { color:#385 } |
|
1034 | 1037 | .syntaxhl .comment .char { color:#385 } |
|
1035 | 1038 | .syntaxhl .comment .delimiter { color:#385 } |
|
1036 | 1039 | .syntaxhl .complex { color:#A08 } |
|
1037 | 1040 | .syntaxhl .constant { color:#258; font-weight:bold } |
|
1038 | 1041 | .syntaxhl .decorator { color:#B0B } |
|
1039 | 1042 | .syntaxhl .definition { color:#099; font-weight:bold } |
|
1040 | 1043 | .syntaxhl .delimiter { color:black } |
|
1041 | 1044 | .syntaxhl .directive { color:#088; font-weight:bold } |
|
1042 | 1045 | .syntaxhl .doc { color:#970 } |
|
1043 | 1046 | .syntaxhl .doc-string { color:#D42; font-weight:bold } |
|
1044 | 1047 | .syntaxhl .doctype { color:#34b } |
|
1045 | 1048 | .syntaxhl .entity { color:#800; font-weight:bold } |
|
1046 | 1049 | .syntaxhl .error { color:#F00; background-color:#FAA } |
|
1047 | 1050 | .syntaxhl .escape { color:#666 } |
|
1048 | 1051 | .syntaxhl .exception { color:#C00; font-weight:bold } |
|
1049 | 1052 | .syntaxhl .float { color:#06D } |
|
1050 | 1053 | .syntaxhl .function { color:#06B; font-weight:bold } |
|
1051 | 1054 | .syntaxhl .global-variable { color:#d70 } |
|
1052 | 1055 | .syntaxhl .hex { color:#02b } |
|
1053 | 1056 | .syntaxhl .imaginary { color:#f00 } |
|
1054 | 1057 | .syntaxhl .include { color:#B44; font-weight:bold } |
|
1055 | 1058 | .syntaxhl .inline { background-color: hsla(0,0%,0%,0.07); color: black } |
|
1056 | 1059 | .syntaxhl .inline-delimiter { font-weight: bold; color: #666 } |
|
1057 | 1060 | .syntaxhl .instance-variable { color:#33B } |
|
1058 | 1061 | .syntaxhl .integer { color:#06D } |
|
1059 | 1062 | .syntaxhl .key .char { color: #60f } |
|
1060 | 1063 | .syntaxhl .key .delimiter { color: #404 } |
|
1061 | 1064 | .syntaxhl .key { color: #606 } |
|
1062 | 1065 | .syntaxhl .keyword { color:#939; font-weight:bold } |
|
1063 | 1066 | .syntaxhl .label { color:#970; font-weight:bold } |
|
1064 | 1067 | .syntaxhl .local-variable { color:#963 } |
|
1065 | 1068 | .syntaxhl .namespace { color:#707; font-weight:bold } |
|
1066 | 1069 | .syntaxhl .octal { color:#40E } |
|
1067 | 1070 | .syntaxhl .operator { } |
|
1068 | 1071 | .syntaxhl .predefined { color:#369; font-weight:bold } |
|
1069 | 1072 | .syntaxhl .predefined-constant { color:#069 } |
|
1070 | 1073 | .syntaxhl .predefined-type { color:#0a5; font-weight:bold } |
|
1071 | 1074 | .syntaxhl .preprocessor { color:#579 } |
|
1072 | 1075 | .syntaxhl .pseudo-class { color:#00C; font-weight:bold } |
|
1073 | 1076 | .syntaxhl .regexp .content { color:#808 } |
|
1074 | 1077 | .syntaxhl .regexp .delimiter { color:#404 } |
|
1075 | 1078 | .syntaxhl .regexp .modifier { color:#C2C } |
|
1076 | 1079 | .syntaxhl .regexp { background-color:hsla(300,100%,50%,0.06); } |
|
1077 | 1080 | .syntaxhl .reserved { color:#080; font-weight:bold } |
|
1078 | 1081 | .syntaxhl .shell .content { color:#2B2 } |
|
1079 | 1082 | .syntaxhl .shell .delimiter { color:#161 } |
|
1080 | 1083 | .syntaxhl .shell { background-color:hsla(120,100%,50%,0.06); } |
|
1081 | 1084 | .syntaxhl .string .char { color: #46a } |
|
1082 | 1085 | .syntaxhl .string .content { color: #46a } |
|
1083 | 1086 | .syntaxhl .string .delimiter { color: #46a } |
|
1084 | 1087 | .syntaxhl .string .modifier { color: #46a } |
|
1085 | 1088 | .syntaxhl .symbol .content { color:#d33 } |
|
1086 | 1089 | .syntaxhl .symbol .delimiter { color:#d33 } |
|
1087 | 1090 | .syntaxhl .symbol { color:#d33 } |
|
1088 | 1091 | .syntaxhl .tag { color:#070 } |
|
1089 | 1092 | .syntaxhl .type { color:#339; font-weight:bold } |
|
1090 | 1093 | .syntaxhl .value { color: #088; } |
|
1091 | 1094 | .syntaxhl .variable { color:#037 } |
|
1092 | 1095 | |
|
1093 | 1096 | .syntaxhl .insert { background: hsla(120,100%,50%,0.12) } |
|
1094 | 1097 | .syntaxhl .delete { background: hsla(0,100%,50%,0.12) } |
|
1095 | 1098 | .syntaxhl .change { color: #bbf; background: #007; } |
|
1096 | 1099 | .syntaxhl .head { color: #f8f; background: #505 } |
|
1097 | 1100 | .syntaxhl .head .filename { color: white; } |
|
1098 | 1101 | |
|
1099 | 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 | 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 | 1105 | .syntaxhl .insert .insert { color: #0c0; background:transparent; font-weight:bold } |
|
1103 | 1106 | .syntaxhl .delete .delete { color: #c00; background:transparent; font-weight:bold } |
|
1104 | 1107 | .syntaxhl .change .change { color: #88f } |
|
1105 | 1108 | .syntaxhl .head .head { color: #f4f } |
|
1106 | 1109 | |
|
1107 | 1110 | /***** Media print specific styles *****/ |
|
1108 | 1111 | @media print { |
|
1109 | 1112 | #top-menu, #header, #main-menu, #sidebar, #footer, .contextual, .other-formats { display:none; } |
|
1110 | 1113 | #main { background: #fff; } |
|
1111 | 1114 | #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;} |
|
1112 | 1115 | #wiki_add_attachment { display:none; } |
|
1113 | 1116 | .hide-when-print { display: none; } |
|
1114 | 1117 | .autoscroll {overflow-x: visible;} |
|
1115 | 1118 | table.list {margin-top:0.5em;} |
|
1116 | 1119 | table.list th, table.list td {border: 1px solid #aaa;} |
|
1117 | 1120 | } |
|
1118 | 1121 | |
|
1119 | 1122 | /* Accessibility specific styles */ |
|
1120 | 1123 | .hidden-for-sighted { |
|
1121 | 1124 | position:absolute; |
|
1122 | 1125 | left:-10000px; |
|
1123 | 1126 | top:auto; |
|
1124 | 1127 | width:1px; |
|
1125 | 1128 | height:1px; |
|
1126 | 1129 | overflow:hidden; |
|
1127 | 1130 | } |
|
1 | NO CONTENT: modified file | |
The requested commit or file is too big and content was truncated. Show full diff |
|
1 | 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