##// END OF EJS Templates
Trackers controller refactoring....
Jean-Philippe Lang -
r2462:589320337db0
parent child
Show More
@@ -1,83 +1,67
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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 TrackersController < ApplicationController
19 19 before_filter :require_admin
20 20
21 21 def index
22 22 list
23 23 render :action => 'list' unless request.xhr?
24 24 end
25
26 # GETs should be safe (see http://www.w3.org/2001/tag/doc/whenToUseGet.html)
27 verify :method => :post, :only => [ :destroy, :move ], :redirect_to => { :action => :list }
25
26 verify :method => :post, :only => :destroy, :redirect_to => { :action => :list }
28 27
29 28 def list
30 29 @tracker_pages, @trackers = paginate :trackers, :per_page => 10, :order => 'position'
31 30 render :action => "list", :layout => false if request.xhr?
32 31 end
33 32
34 33 def new
35 34 @tracker = Tracker.new(params[:tracker])
36 35 if request.post? and @tracker.save
37 36 # workflow copy
38 37 if !params[:copy_workflow_from].blank? && (copy_from = Tracker.find_by_id(params[:copy_workflow_from]))
39 38 @tracker.workflows.copy(copy_from)
40 39 end
41 40 flash[:notice] = l(:notice_successful_create)
42 41 redirect_to :action => 'list'
43 42 return
44 43 end
45 44 @trackers = Tracker.find :all, :order => 'position'
46 45 @projects = Project.find(:all)
47 46 end
48 47
49 48 def edit
50 49 @tracker = Tracker.find(params[:id])
51 50 if request.post? and @tracker.update_attributes(params[:tracker])
52 51 flash[:notice] = l(:notice_successful_update)
53 52 redirect_to :action => 'list'
54 53 return
55 54 end
56 55 @projects = Project.find(:all)
57 56 end
58
59 def move
60 @tracker = Tracker.find(params[:id])
61 case params[:position]
62 when 'highest'
63 @tracker.move_to_top
64 when 'higher'
65 @tracker.move_higher
66 when 'lower'
67 @tracker.move_lower
68 when 'lowest'
69 @tracker.move_to_bottom
70 end if params[:position]
71 redirect_to :action => 'list'
72 end
73 57
74 58 def destroy
75 59 @tracker = Tracker.find(params[:id])
76 60 unless @tracker.issues.empty?
77 61 flash[:error] = "This tracker contains issues and can\'t be deleted."
78 62 else
79 63 @tracker.destroy
80 64 end
81 65 redirect_to :action => 'list'
82 66 end
83 67 end
@@ -1,648 +1,655
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 require 'coderay'
19 19 require 'coderay/helpers/file_type'
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 def current_role
32 32 @current_role ||= User.current.role_for_project(@project)
33 33 end
34 34
35 35 # Return true if user is authorized for controller/action, otherwise false
36 36 def authorize_for(controller, action)
37 37 User.current.allowed_to?({:controller => controller, :action => action}, @project)
38 38 end
39 39
40 40 # Display a link if user is authorized
41 41 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 42 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 43 end
44 44
45 45 # Display a link to remote if user is authorized
46 46 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
47 47 url = options[:url] || {}
48 48 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
49 49 end
50 50
51 51 # Display a link to user's account page
52 52 def link_to_user(user, options={})
53 53 (user && !user.anonymous?) ? link_to(user.name(options[:format]), :controller => 'account', :action => 'show', :id => user) : 'Anonymous'
54 54 end
55 55
56 56 def link_to_issue(issue, options={})
57 57 options[:class] ||= ''
58 58 options[:class] << ' issue'
59 59 options[:class] << ' closed' if issue.closed?
60 60 link_to "#{issue.tracker.name} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue}, options
61 61 end
62 62
63 63 # Generates a link to an attachment.
64 64 # Options:
65 65 # * :text - Link text (default to attachment filename)
66 66 # * :download - Force download (default: false)
67 67 def link_to_attachment(attachment, options={})
68 68 text = options.delete(:text) || attachment.filename
69 69 action = options.delete(:download) ? 'download' : 'show'
70 70
71 71 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
72 72 end
73 73
74 74 def toggle_link(name, id, options={})
75 75 onclick = "Element.toggle('#{id}'); "
76 76 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
77 77 onclick << "return false;"
78 78 link_to(name, "#", :onclick => onclick)
79 79 end
80 80
81 81 def image_to_function(name, function, html_options = {})
82 82 html_options.symbolize_keys!
83 83 tag(:input, html_options.merge({
84 84 :type => "image", :src => image_path(name),
85 85 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
86 86 }))
87 87 end
88 88
89 89 def prompt_to_remote(name, text, param, url, html_options = {})
90 90 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
91 91 link_to name, {}, html_options
92 92 end
93 93
94 94 def format_activity_title(text)
95 95 h(truncate_single_line(text, :length => 100))
96 96 end
97 97
98 98 def format_activity_day(date)
99 99 date == Date.today ? l(:label_today).titleize : format_date(date)
100 100 end
101 101
102 102 def format_activity_description(text)
103 103 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
104 104 end
105 105
106 106 def due_date_distance_in_words(date)
107 107 if date
108 108 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
109 109 end
110 110 end
111 111
112 112 def render_page_hierarchy(pages, node=nil)
113 113 content = ''
114 114 if pages[node]
115 115 content << "<ul class=\"pages-hierarchy\">\n"
116 116 pages[node].each do |page|
117 117 content << "<li>"
118 118 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
119 119 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
120 120 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
121 121 content << "</li>\n"
122 122 end
123 123 content << "</ul>\n"
124 124 end
125 125 content
126 126 end
127 127
128 128 # Renders flash messages
129 129 def render_flash_messages
130 130 s = ''
131 131 flash.each do |k,v|
132 132 s << content_tag('div', v, :class => "flash #{k}")
133 133 end
134 134 s
135 135 end
136 136
137 137 # Renders the project quick-jump box
138 138 def render_project_jump_box
139 139 # Retrieve them now to avoid a COUNT query
140 140 projects = User.current.projects.all
141 141 if projects.any?
142 142 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
143 143 "<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
144 144 '<option disabled="disabled">---</option>'
145 145 s << project_tree_options_for_select(projects) do |p|
146 146 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
147 147 end
148 148 s << '</select>'
149 149 s
150 150 end
151 151 end
152 152
153 153 def project_tree_options_for_select(projects, options = {})
154 154 s = ''
155 155 project_tree(projects) do |project, level|
156 156 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
157 157 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
158 158 tag_options.merge!(yield(project)) if block_given?
159 159 s << content_tag('option', name_prefix + h(project), tag_options)
160 160 end
161 161 s
162 162 end
163 163
164 164 # Yields the given block for each project with its level in the tree
165 165 def project_tree(projects, &block)
166 166 ancestors = []
167 167 projects.sort_by(&:lft).each do |project|
168 168 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
169 169 ancestors.pop
170 170 end
171 171 yield project, ancestors.size
172 172 ancestors << project
173 173 end
174 174 end
175 175
176 176 def project_nested_ul(projects, &block)
177 177 s = ''
178 178 if projects.any?
179 179 ancestors = []
180 180 projects.sort_by(&:lft).each do |project|
181 181 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
182 182 s << "<ul>\n"
183 183 else
184 184 ancestors.pop
185 185 s << "</li>"
186 186 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
187 187 ancestors.pop
188 188 s << "</ul></li>\n"
189 189 end
190 190 end
191 191 s << "<li>"
192 192 s << yield(project).to_s
193 193 ancestors << project
194 194 end
195 195 s << ("</li></ul>\n" * ancestors.size)
196 196 end
197 197 s
198 198 end
199 199
200 200 # Truncates and returns the string as a single line
201 201 def truncate_single_line(string, *args)
202 202 truncate(string, *args).gsub(%r{[\r\n]+}m, ' ')
203 203 end
204 204
205 205 def html_hours(text)
206 206 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
207 207 end
208 208
209 209 def authoring(created, author, options={})
210 210 time_tag = @project.nil? ? content_tag('acronym', distance_of_time_in_words(Time.now, created), :title => format_time(created)) :
211 211 link_to(distance_of_time_in_words(Time.now, created),
212 212 {:controller => 'projects', :action => 'activity', :id => @project, :from => created.to_date},
213 213 :title => format_time(created))
214 214 author_tag = (author.is_a?(User) && !author.anonymous?) ? link_to(h(author), :controller => 'account', :action => 'show', :id => author) : h(author || 'Anonymous')
215 215 l(options[:label] || :label_added_time_by, :author => author_tag, :age => time_tag)
216 216 end
217 217
218 218 def syntax_highlight(name, content)
219 219 type = CodeRay::FileType[name]
220 220 type ? CodeRay.scan(content, type).html : h(content)
221 221 end
222 222
223 223 def to_path_param(path)
224 224 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
225 225 end
226 226
227 227 def pagination_links_full(paginator, count=nil, options={})
228 228 page_param = options.delete(:page_param) || :page
229 229 url_param = params.dup
230 230 # don't reuse query params if filters are present
231 231 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
232 232
233 233 html = ''
234 234 if paginator.current.previous
235 235 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
236 236 end
237 237
238 238 html << (pagination_links_each(paginator, options) do |n|
239 239 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
240 240 end || '')
241 241
242 242 if paginator.current.next
243 243 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
244 244 end
245 245
246 246 unless count.nil?
247 247 html << [
248 248 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
249 249 per_page_links(paginator.items_per_page)
250 250 ].compact.join(' | ')
251 251 end
252 252
253 253 html
254 254 end
255 255
256 256 def per_page_links(selected=nil)
257 257 url_param = params.dup
258 258 url_param.clear if url_param.has_key?(:set_filter)
259 259
260 260 links = Setting.per_page_options_array.collect do |n|
261 261 n == selected ? n : link_to_remote(n, {:update => "content",
262 262 :url => params.dup.merge(:per_page => n),
263 263 :method => :get},
264 264 {:href => url_for(url_param.merge(:per_page => n))})
265 265 end
266 266 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
267 267 end
268
269 def reorder_links(name, url)
270 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
271 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
272 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
273 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
274 end
268 275
269 276 def breadcrumb(*args)
270 277 elements = args.flatten
271 278 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
272 279 end
273 280
274 281 def other_formats_links(&block)
275 282 concat('<p class="other-formats">' + l(:label_export_to))
276 283 yield Redmine::Views::OtherFormatsBuilder.new(self)
277 284 concat('</p>')
278 285 end
279 286
280 287 def page_header_title
281 288 if @project.nil? || @project.new_record?
282 289 h(Setting.app_title)
283 290 else
284 291 b = []
285 292 ancestors = (@project.root? ? [] : @project.ancestors.visible)
286 293 if ancestors.any?
287 294 root = ancestors.shift
288 295 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
289 296 if ancestors.size > 2
290 297 b << '&#8230;'
291 298 ancestors = ancestors[-2, 2]
292 299 end
293 300 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
294 301 end
295 302 b << h(@project)
296 303 b.join(' &#187; ')
297 304 end
298 305 end
299 306
300 307 def html_title(*args)
301 308 if args.empty?
302 309 title = []
303 310 title << @project.name if @project
304 311 title += @html_title if @html_title
305 312 title << Setting.app_title
306 313 title.compact.join(' - ')
307 314 else
308 315 @html_title ||= []
309 316 @html_title += args
310 317 end
311 318 end
312 319
313 320 def accesskey(s)
314 321 Redmine::AccessKeys.key_for s
315 322 end
316 323
317 324 # Formats text according to system settings.
318 325 # 2 ways to call this method:
319 326 # * with a String: textilizable(text, options)
320 327 # * with an object and one of its attribute: textilizable(issue, :description, options)
321 328 def textilizable(*args)
322 329 options = args.last.is_a?(Hash) ? args.pop : {}
323 330 case args.size
324 331 when 1
325 332 obj = options[:object]
326 333 text = args.shift
327 334 when 2
328 335 obj = args.shift
329 336 text = obj.send(args.shift).to_s
330 337 else
331 338 raise ArgumentError, 'invalid arguments to textilizable'
332 339 end
333 340 return '' if text.blank?
334 341
335 342 only_path = options.delete(:only_path) == false ? false : true
336 343
337 344 # when using an image link, try to use an attachment, if possible
338 345 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
339 346
340 347 if attachments
341 348 attachments = attachments.sort_by(&:created_on).reverse
342 349 text = text.gsub(/!((\<|\=|\>)?(\([^\)]+\))?(\[[^\]]+\])?(\{[^\}]+\})?)(\S+\.(bmp|gif|jpg|jpeg|png))!/i) do |m|
343 350 style = $1
344 351 filename = $6.downcase
345 352 # search for the picture in attachments
346 353 if found = attachments.detect { |att| att.filename.downcase == filename }
347 354 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
348 355 desc = found.description.to_s.gsub(/^([^\(\)]*).*$/, "\\1")
349 356 alt = desc.blank? ? nil : "(#{desc})"
350 357 "!#{style}#{image_url}#{alt}!"
351 358 else
352 359 m
353 360 end
354 361 end
355 362 end
356 363
357 364 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text) { |macro, args| exec_macro(macro, obj, args) }
358 365
359 366 # different methods for formatting wiki links
360 367 case options[:wiki_links]
361 368 when :local
362 369 # used for local links to html files
363 370 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
364 371 when :anchor
365 372 # used for single-file wiki export
366 373 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
367 374 else
368 375 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
369 376 end
370 377
371 378 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
372 379
373 380 # Wiki links
374 381 #
375 382 # Examples:
376 383 # [[mypage]]
377 384 # [[mypage|mytext]]
378 385 # wiki links can refer other project wikis, using project name or identifier:
379 386 # [[project:]] -> wiki starting page
380 387 # [[project:|mytext]]
381 388 # [[project:mypage]]
382 389 # [[project:mypage|mytext]]
383 390 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
384 391 link_project = project
385 392 esc, all, page, title = $1, $2, $3, $5
386 393 if esc.nil?
387 394 if page =~ /^([^\:]+)\:(.*)$/
388 395 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
389 396 page = $2
390 397 title ||= $1 if page.blank?
391 398 end
392 399
393 400 if link_project && link_project.wiki
394 401 # extract anchor
395 402 anchor = nil
396 403 if page =~ /^(.+?)\#(.+)$/
397 404 page, anchor = $1, $2
398 405 end
399 406 # check if page exists
400 407 wiki_page = link_project.wiki.find_page(page)
401 408 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
402 409 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
403 410 else
404 411 # project or wiki doesn't exist
405 412 all
406 413 end
407 414 else
408 415 all
409 416 end
410 417 end
411 418
412 419 # Redmine links
413 420 #
414 421 # Examples:
415 422 # Issues:
416 423 # #52 -> Link to issue #52
417 424 # Changesets:
418 425 # r52 -> Link to revision 52
419 426 # commit:a85130f -> Link to scmid starting with a85130f
420 427 # Documents:
421 428 # document#17 -> Link to document with id 17
422 429 # document:Greetings -> Link to the document with title "Greetings"
423 430 # document:"Some document" -> Link to the document with title "Some document"
424 431 # Versions:
425 432 # version#3 -> Link to version with id 3
426 433 # version:1.0.0 -> Link to version named "1.0.0"
427 434 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
428 435 # Attachments:
429 436 # attachment:file.zip -> Link to the attachment of the current object named file.zip
430 437 # Source files:
431 438 # source:some/file -> Link to the file located at /some/file in the project's repository
432 439 # source:some/file@52 -> Link to the file's revision 52
433 440 # source:some/file#L120 -> Link to line 120 of the file
434 441 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
435 442 # export:some/file -> Force the download of the file
436 443 # Forum messages:
437 444 # message#1218 -> Link to message with id 1218
438 445 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|\s|<|$)}) do |m|
439 446 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
440 447 link = nil
441 448 if esc.nil?
442 449 if prefix.nil? && sep == 'r'
443 450 if project && (changeset = project.changesets.find_by_revision(oid))
444 451 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
445 452 :class => 'changeset',
446 453 :title => truncate_single_line(changeset.comments, :length => 100))
447 454 end
448 455 elsif sep == '#'
449 456 oid = oid.to_i
450 457 case prefix
451 458 when nil
452 459 if issue = Issue.find_by_id(oid, :include => [:project, :status], :conditions => Project.visible_by(User.current))
453 460 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
454 461 :class => (issue.closed? ? 'issue closed' : 'issue'),
455 462 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
456 463 link = content_tag('del', link) if issue.closed?
457 464 end
458 465 when 'document'
459 466 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
460 467 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
461 468 :class => 'document'
462 469 end
463 470 when 'version'
464 471 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
465 472 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
466 473 :class => 'version'
467 474 end
468 475 when 'message'
469 476 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
470 477 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
471 478 :controller => 'messages',
472 479 :action => 'show',
473 480 :board_id => message.board,
474 481 :id => message.root,
475 482 :anchor => (message.parent ? "message-#{message.id}" : nil)},
476 483 :class => 'message'
477 484 end
478 485 end
479 486 elsif sep == ':'
480 487 # removes the double quotes if any
481 488 name = oid.gsub(%r{^"(.*)"$}, "\\1")
482 489 case prefix
483 490 when 'document'
484 491 if project && document = project.documents.find_by_title(name)
485 492 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
486 493 :class => 'document'
487 494 end
488 495 when 'version'
489 496 if project && version = project.versions.find_by_name(name)
490 497 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
491 498 :class => 'version'
492 499 end
493 500 when 'commit'
494 501 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
495 502 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
496 503 :class => 'changeset',
497 504 :title => truncate_single_line(changeset.comments, :length => 100)
498 505 end
499 506 when 'source', 'export'
500 507 if project && project.repository
501 508 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
502 509 path, rev, anchor = $1, $3, $5
503 510 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
504 511 :path => to_path_param(path),
505 512 :rev => rev,
506 513 :anchor => anchor,
507 514 :format => (prefix == 'export' ? 'raw' : nil)},
508 515 :class => (prefix == 'export' ? 'source download' : 'source')
509 516 end
510 517 when 'attachment'
511 518 if attachments && attachment = attachments.detect {|a| a.filename == name }
512 519 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
513 520 :class => 'attachment'
514 521 end
515 522 end
516 523 end
517 524 end
518 525 leading + (link || "#{prefix}#{sep}#{oid}")
519 526 end
520 527
521 528 text
522 529 end
523 530
524 531 # Same as Rails' simple_format helper without using paragraphs
525 532 def simple_format_without_paragraph(text)
526 533 text.to_s.
527 534 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
528 535 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
529 536 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
530 537 end
531 538
532 539 def lang_options_for_select(blank=true)
533 540 (blank ? [["(auto)", ""]] : []) +
534 541 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
535 542 end
536 543
537 544 def label_tag_for(name, option_tags = nil, options = {})
538 545 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
539 546 content_tag("label", label_text)
540 547 end
541 548
542 549 def labelled_tabular_form_for(name, object, options, &proc)
543 550 options[:html] ||= {}
544 551 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
545 552 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
546 553 end
547 554
548 555 def back_url_hidden_field_tag
549 556 back_url = params[:back_url] || request.env['HTTP_REFERER']
550 557 back_url = CGI.unescape(back_url.to_s)
551 558 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
552 559 end
553 560
554 561 def check_all_links(form_name)
555 562 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
556 563 " | " +
557 564 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
558 565 end
559 566
560 567 def progress_bar(pcts, options={})
561 568 pcts = [pcts, pcts] unless pcts.is_a?(Array)
562 569 pcts[1] = pcts[1] - pcts[0]
563 570 pcts << (100 - pcts[1] - pcts[0])
564 571 width = options[:width] || '100px;'
565 572 legend = options[:legend] || ''
566 573 content_tag('table',
567 574 content_tag('tr',
568 575 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0].floor}%;", :class => 'closed') : '') +
569 576 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1].floor}%;", :class => 'done') : '') +
570 577 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2].floor}%;", :class => 'todo') : '')
571 578 ), :class => 'progress', :style => "width: #{width};") +
572 579 content_tag('p', legend, :class => 'pourcent')
573 580 end
574 581
575 582 def context_menu_link(name, url, options={})
576 583 options[:class] ||= ''
577 584 if options.delete(:selected)
578 585 options[:class] << ' icon-checked disabled'
579 586 options[:disabled] = true
580 587 end
581 588 if options.delete(:disabled)
582 589 options.delete(:method)
583 590 options.delete(:confirm)
584 591 options.delete(:onclick)
585 592 options[:class] << ' disabled'
586 593 url = '#'
587 594 end
588 595 link_to name, url, options
589 596 end
590 597
591 598 def calendar_for(field_id)
592 599 include_calendar_headers_tags
593 600 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
594 601 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
595 602 end
596 603
597 604 def include_calendar_headers_tags
598 605 unless @calendar_headers_tags_included
599 606 @calendar_headers_tags_included = true
600 607 content_for :header_tags do
601 608 javascript_include_tag('calendar/calendar') +
602 609 javascript_include_tag("calendar/lang/calendar-#{current_language}.js") +
603 610 javascript_include_tag('calendar/calendar-setup') +
604 611 stylesheet_link_tag('calendar')
605 612 end
606 613 end
607 614 end
608 615
609 616 def content_for(name, content = nil, &block)
610 617 @has_content ||= {}
611 618 @has_content[name] = true
612 619 super(name, content, &block)
613 620 end
614 621
615 622 def has_content?(name)
616 623 (@has_content && @has_content[name]) || false
617 624 end
618 625
619 626 # Returns the avatar image tag for the given +user+ if avatars are enabled
620 627 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
621 628 def avatar(user, options = { })
622 629 if Setting.gravatar_enabled?
623 630 email = nil
624 631 if user.respond_to?(:mail)
625 632 email = user.mail
626 633 elsif user.to_s =~ %r{<(.+?)>}
627 634 email = $1
628 635 end
629 636 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
630 637 end
631 638 end
632 639
633 640 private
634 641
635 642 def wiki_helper
636 643 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
637 644 extend helper
638 645 return self
639 646 end
640 647
641 648 def link_to_remote_content_update(text, url_params)
642 649 link_to_remote(text,
643 650 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
644 651 {:href => url_for(:params => url_params)}
645 652 )
646 653 end
647 654
648 655 end
@@ -1,35 +1,30
1 1 <div class="contextual">
2 2 <%= link_to l(:label_tracker_new), {:action => 'new'}, :class => 'icon icon-add' %>
3 3 </div>
4 4
5 5 <h2><%=l(:label_tracker_plural)%></h2>
6 6
7 7 <table class="list">
8 8 <thead><tr>
9 9 <th><%=l(:label_tracker)%></th>
10 10 <th></th>
11 11 <th><%=l(:button_sort)%></th>
12 12 <th></th>
13 13 </tr></thead>
14 14 <tbody>
15 15 <% for tracker in @trackers %>
16 16 <tr class="<%= cycle("odd", "even") %>">
17 17 <td><%= link_to tracker.name, :action => 'edit', :id => tracker %></td>
18 18 <td align="center"><% unless tracker.workflows.count > 0 %><span class="icon icon-warning"><%= l(:text_tracker_no_workflow) %> (<%= link_to l(:button_edit), {:controller => 'workflows', :action => 'edit', :tracker_id => tracker} %>)</span><% end %></td>
19 <td align="center" style="width:15%;">
20 <%= link_to image_tag('2uparrow.png', :alt => l(:label_sort_highest)), {:action => 'move', :id => tracker, :position => 'highest'}, :method => :post, :title => l(:label_sort_highest) %>
21 <%= link_to image_tag('1uparrow.png', :alt => l(:label_sort_higher)), {:action => 'move', :id => tracker, :position => 'higher'}, :method => :post, :title => l(:label_sort_higher) %> -
22 <%= link_to image_tag('1downarrow.png', :alt => l(:label_sort_lower)), {:action => 'move', :id => tracker, :position => 'lower'}, :method => :post, :title => l(:label_sort_lower) %>
23 <%= link_to image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), {:action => 'move', :id => tracker, :position => 'lowest'}, :method => :post, :title => l(:label_sort_lowest) %>
24 </td>
19 <td align="center" style="width:15%;"><%= reorder_links('tracker', {:action => 'edit', :id => tracker}) %></td>
25 20 <td align="center" style="width:10%;">
26 21 <%= button_to l(:button_delete), { :action => 'destroy', :id => tracker }, :confirm => l(:text_are_you_sure), :class => "button-small" %>
27 22 </td>
28 23 </tr>
29 24 <% end %>
30 25 </tbody>
31 26 </table>
32 27
33 28 <p class="pagination"><%= pagination_links_full @tracker_pages %></p>
34 29
35 30 <% html_title(l(:label_tracker_plural)) -%>
@@ -1,68 +1,119
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2009 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 require File.dirname(__FILE__) + '/../test_helper'
19 19 require 'trackers_controller'
20 20
21 21 # Re-raise errors caught by the controller.
22 22 class TrackersController; def rescue_action(e) raise e end; end
23 23
24 24 class TrackersControllerTest < Test::Unit::TestCase
25 fixtures :trackers, :projects, :projects_trackers, :users
25 fixtures :trackers, :projects, :projects_trackers, :users, :issues
26 26
27 27 def setup
28 28 @controller = TrackersController.new
29 29 @request = ActionController::TestRequest.new
30 30 @response = ActionController::TestResponse.new
31 31 User.current = nil
32 32 @request.session[:user_id] = 1 # admin
33 33 end
34 34
35 def test_index
36 get :index
37 assert_response :success
38 assert_template 'list'
39 end
40
41 def test_get_new
42 get :new
43 assert_response :success
44 assert_template 'new'
45 end
46
47 def test_post_new
48 post :new, :tracker => { :name => 'New tracker', :project_ids => ['1', '', ''] }
49 assert_redirected_to '/trackers/list'
50 tracker = Tracker.find_by_name('New tracker')
51 assert_equal [1], tracker.project_ids.sort
52 assert_equal 0, tracker.workflows.count
53 end
54
55 def test_post_new_with_workflow_copy
56 post :new, :tracker => { :name => 'New tracker' }, :copy_workflow_from => 1
57 assert_redirected_to '/trackers/list'
58 tracker = Tracker.find_by_name('New tracker')
59 assert_equal 0, tracker.projects.count
60 assert_equal Tracker.find(1).workflows.count, tracker.workflows.count
61 end
62
35 63 def test_get_edit
36 64 Tracker.find(1).project_ids = [1, 3]
37 65
38 66 get :edit, :id => 1
39 67 assert_response :success
40 68 assert_template 'edit'
41 69
42 70 assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
43 71 :value => '1',
44 72 :checked => 'checked' }
45 73
46 74 assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
47 75 :value => '2',
48 76 :checked => nil }
49 77
50 78 assert_tag :input, :attributes => { :name => 'tracker[project_ids][]',
51 79 :value => '',
52 80 :type => 'hidden'}
53 81 end
54 82
55 83 def test_post_edit
56 84 post :edit, :id => 1, :tracker => { :name => 'Renamed',
57 85 :project_ids => ['1', '2', ''] }
58 86 assert_redirected_to '/trackers/list'
59 87 assert_equal [1, 2], Tracker.find(1).project_ids.sort
60 88 end
61 89
62 90 def test_post_edit_without_projects
63 91 post :edit, :id => 1, :tracker => { :name => 'Renamed',
64 92 :project_ids => [''] }
65 93 assert_redirected_to '/trackers/list'
66 94 assert Tracker.find(1).project_ids.empty?
67 95 end
96
97 def test_move_lower
98 tracker = Tracker.find_by_position(1)
99 post :edit, :id => 1, :tracker => { :move_to => 'lower' }
100 assert_equal 2, tracker.reload.position
101 end
102
103 def test_destroy
104 tracker = Tracker.create!(:name => 'Destroyable')
105 assert_difference 'Tracker.count', -1 do
106 post :destroy, :id => tracker.id
107 end
108 assert_redirected_to '/trackers/list'
109 assert_nil flash[:error]
110 end
111
112 def test_destroy_tracker_in_use
113 assert_no_difference 'Tracker.count' do
114 post :destroy, :id => 1
115 end
116 assert_redirected_to '/trackers/list'
117 assert_not_nil flash[:error]
118 end
68 119 end
@@ -1,256 +1,270
1 1 module ActiveRecord
2 2 module Acts #:nodoc:
3 3 module List #:nodoc:
4 4 def self.included(base)
5 5 base.extend(ClassMethods)
6 6 end
7 7
8 8 # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9 9 # The class that has this specified needs to have a +position+ column defined as an integer on
10 10 # the mapped database table.
11 11 #
12 12 # Todo list example:
13 13 #
14 14 # class TodoList < ActiveRecord::Base
15 15 # has_many :todo_items, :order => "position"
16 16 # end
17 17 #
18 18 # class TodoItem < ActiveRecord::Base
19 19 # belongs_to :todo_list
20 20 # acts_as_list :scope => :todo_list
21 21 # end
22 22 #
23 23 # todo_list.first.move_to_bottom
24 24 # todo_list.last.move_higher
25 25 module ClassMethods
26 26 # Configuration options are:
27 27 #
28 28 # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29 29 # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30 30 # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31 31 # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32 32 # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33 33 def acts_as_list(options = {})
34 34 configuration = { :column => "position", :scope => "1 = 1" }
35 35 configuration.update(options) if options.is_a?(Hash)
36 36
37 37 configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
38 38
39 39 if configuration[:scope].is_a?(Symbol)
40 40 scope_condition_method = %(
41 41 def scope_condition
42 42 if #{configuration[:scope].to_s}.nil?
43 43 "#{configuration[:scope].to_s} IS NULL"
44 44 else
45 45 "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
46 46 end
47 47 end
48 48 )
49 49 else
50 50 scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
51 51 end
52 52
53 53 class_eval <<-EOV
54 54 include ActiveRecord::Acts::List::InstanceMethods
55 55
56 56 def acts_as_list_class
57 57 ::#{self.name}
58 58 end
59 59
60 60 def position_column
61 61 '#{configuration[:column]}'
62 62 end
63 63
64 64 #{scope_condition_method}
65 65
66 66 before_destroy :remove_from_list
67 67 before_create :add_to_list_bottom
68 68 EOV
69 69 end
70 70 end
71 71
72 72 # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
73 73 # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
74 74 # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
75 75 # the first in the list of all chapters.
76 76 module InstanceMethods
77 77 # Insert the item at the given position (defaults to the top position of 1).
78 78 def insert_at(position = 1)
79 79 insert_at_position(position)
80 80 end
81 81
82 82 # Swap positions with the next lower item, if one exists.
83 83 def move_lower
84 84 return unless lower_item
85 85
86 86 acts_as_list_class.transaction do
87 87 lower_item.decrement_position
88 88 increment_position
89 89 end
90 90 end
91 91
92 92 # Swap positions with the next higher item, if one exists.
93 93 def move_higher
94 94 return unless higher_item
95 95
96 96 acts_as_list_class.transaction do
97 97 higher_item.increment_position
98 98 decrement_position
99 99 end
100 100 end
101 101
102 102 # Move to the bottom of the list. If the item is already in the list, the items below it have their
103 103 # position adjusted accordingly.
104 104 def move_to_bottom
105 105 return unless in_list?
106 106 acts_as_list_class.transaction do
107 107 decrement_positions_on_lower_items
108 108 assume_bottom_position
109 109 end
110 110 end
111 111
112 112 # Move to the top of the list. If the item is already in the list, the items above it have their
113 113 # position adjusted accordingly.
114 114 def move_to_top
115 115 return unless in_list?
116 116 acts_as_list_class.transaction do
117 117 increment_positions_on_higher_items
118 118 assume_top_position
119 119 end
120 120 end
121
122 # Move to the given position
123 def move_to=(pos)
124 case pos.to_s
125 when 'highest'
126 move_to_top
127 when 'higher'
128 move_higher
129 when 'lower'
130 move_lower
131 when 'lowest'
132 move_to_bottom
133 end
134 end
121 135
122 136 # Removes the item from the list.
123 137 def remove_from_list
124 138 if in_list?
125 139 decrement_positions_on_lower_items
126 140 update_attribute position_column, nil
127 141 end
128 142 end
129 143
130 144 # Increase the position of this item without adjusting the rest of the list.
131 145 def increment_position
132 146 return unless in_list?
133 147 update_attribute position_column, self.send(position_column).to_i + 1
134 148 end
135 149
136 150 # Decrease the position of this item without adjusting the rest of the list.
137 151 def decrement_position
138 152 return unless in_list?
139 153 update_attribute position_column, self.send(position_column).to_i - 1
140 154 end
141 155
142 156 # Return +true+ if this object is the first in the list.
143 157 def first?
144 158 return false unless in_list?
145 159 self.send(position_column) == 1
146 160 end
147 161
148 162 # Return +true+ if this object is the last in the list.
149 163 def last?
150 164 return false unless in_list?
151 165 self.send(position_column) == bottom_position_in_list
152 166 end
153 167
154 168 # Return the next higher item in the list.
155 169 def higher_item
156 170 return nil unless in_list?
157 171 acts_as_list_class.find(:first, :conditions =>
158 172 "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
159 173 )
160 174 end
161 175
162 176 # Return the next lower item in the list.
163 177 def lower_item
164 178 return nil unless in_list?
165 179 acts_as_list_class.find(:first, :conditions =>
166 180 "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
167 181 )
168 182 end
169 183
170 184 # Test if this record is in a list
171 185 def in_list?
172 186 !send(position_column).nil?
173 187 end
174 188
175 189 private
176 190 def add_to_list_top
177 191 increment_positions_on_all_items
178 192 end
179 193
180 194 def add_to_list_bottom
181 195 self[position_column] = bottom_position_in_list.to_i + 1
182 196 end
183 197
184 198 # Overwrite this method to define the scope of the list changes
185 199 def scope_condition() "1" end
186 200
187 201 # Returns the bottom position number in the list.
188 202 # bottom_position_in_list # => 2
189 203 def bottom_position_in_list(except = nil)
190 204 item = bottom_item(except)
191 205 item ? item.send(position_column) : 0
192 206 end
193 207
194 208 # Returns the bottom item
195 209 def bottom_item(except = nil)
196 210 conditions = scope_condition
197 211 conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
198 212 acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
199 213 end
200 214
201 215 # Forces item to assume the bottom position in the list.
202 216 def assume_bottom_position
203 217 update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
204 218 end
205 219
206 220 # Forces item to assume the top position in the list.
207 221 def assume_top_position
208 222 update_attribute(position_column, 1)
209 223 end
210 224
211 225 # This has the effect of moving all the higher items up one.
212 226 def decrement_positions_on_higher_items(position)
213 227 acts_as_list_class.update_all(
214 228 "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
215 229 )
216 230 end
217 231
218 232 # This has the effect of moving all the lower items up one.
219 233 def decrement_positions_on_lower_items
220 234 return unless in_list?
221 235 acts_as_list_class.update_all(
222 236 "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
223 237 )
224 238 end
225 239
226 240 # This has the effect of moving all the higher items down one.
227 241 def increment_positions_on_higher_items
228 242 return unless in_list?
229 243 acts_as_list_class.update_all(
230 244 "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
231 245 )
232 246 end
233 247
234 248 # This has the effect of moving all the lower items down one.
235 249 def increment_positions_on_lower_items(position)
236 250 acts_as_list_class.update_all(
237 251 "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
238 252 )
239 253 end
240 254
241 255 # Increments position (<tt>position_column</tt>) of all items in the list.
242 256 def increment_positions_on_all_items
243 257 acts_as_list_class.update_all(
244 258 "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
245 259 )
246 260 end
247 261
248 262 def insert_at_position(position)
249 263 remove_from_list
250 264 increment_positions_on_lower_items(position)
251 265 self.update_attribute(position_column, position)
252 266 end
253 267 end
254 268 end
255 269 end
256 270 end
General Comments 0
You need to be logged in to leave comments. Login now