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