##// END OF EJS Templates
Adds pagination to forum messages (#4664)....
Jean-Philippe Lang -
r3259:8fb29d4d211a
parent child
Show More
@@ -1,132 +1,146
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 class MessagesController < ApplicationController
18 class MessagesController < ApplicationController
19 menu_item :boards
19 menu_item :boards
20 default_search_scope :messages
20 default_search_scope :messages
21 before_filter :find_board, :only => [:new, :preview]
21 before_filter :find_board, :only => [:new, :preview]
22 before_filter :find_message, :except => [:new, :preview]
22 before_filter :find_message, :except => [:new, :preview]
23 before_filter :authorize, :except => [:preview, :edit, :destroy]
23 before_filter :authorize, :except => [:preview, :edit, :destroy]
24
24
25 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
25 verify :method => :post, :only => [ :reply, :destroy ], :redirect_to => { :action => :show }
26 verify :xhr => true, :only => :quote
26 verify :xhr => true, :only => :quote
27
27
28 helper :watchers
28 helper :watchers
29 helper :attachments
29 helper :attachments
30 include AttachmentsHelper
30 include AttachmentsHelper
31
31
32 REPLIES_PER_PAGE = 25 unless const_defined?(:REPLIES_PER_PAGE)
33
32 # Show a topic and its replies
34 # Show a topic and its replies
33 def show
35 def show
34 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}])
36 page = params[:page]
35 @replies.reverse! if User.current.wants_comments_in_reverse_order?
37 # Find the page of the requested reply
38 if params[:r] && page.nil?
39 offset = @topic.children.count(:conditions => ["#{Message.table_name}.id < ?", params[:r].to_i])
40 page = 1 + offset / REPLIES_PER_PAGE
41 end
42
43 @reply_count = @topic.children.count
44 @reply_pages = Paginator.new self, @reply_count, REPLIES_PER_PAGE, page
45 @replies = @topic.children.find(:all, :include => [:author, :attachments, {:board => :project}],
46 :order => "#{Message.table_name}.created_on ASC",
47 :limit => @reply_pages.items_per_page,
48 :offset => @reply_pages.current.offset)
49
36 @reply = Message.new(:subject => "RE: #{@message.subject}")
50 @reply = Message.new(:subject => "RE: #{@message.subject}")
37 render :action => "show", :layout => false if request.xhr?
51 render :action => "show", :layout => false if request.xhr?
38 end
52 end
39
53
40 # Create a new topic
54 # Create a new topic
41 def new
55 def new
42 @message = Message.new(params[:message])
56 @message = Message.new(params[:message])
43 @message.author = User.current
57 @message.author = User.current
44 @message.board = @board
58 @message.board = @board
45 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
59 if params[:message] && User.current.allowed_to?(:edit_messages, @project)
46 @message.locked = params[:message]['locked']
60 @message.locked = params[:message]['locked']
47 @message.sticky = params[:message]['sticky']
61 @message.sticky = params[:message]['sticky']
48 end
62 end
49 if request.post? && @message.save
63 if request.post? && @message.save
50 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
64 call_hook(:controller_messages_new_after_save, { :params => params, :message => @message})
51 attach_files(@message, params[:attachments])
65 attach_files(@message, params[:attachments])
52 redirect_to :action => 'show', :id => @message
66 redirect_to :action => 'show', :id => @message
53 end
67 end
54 end
68 end
55
69
56 # Reply to a topic
70 # Reply to a topic
57 def reply
71 def reply
58 @reply = Message.new(params[:reply])
72 @reply = Message.new(params[:reply])
59 @reply.author = User.current
73 @reply.author = User.current
60 @reply.board = @board
74 @reply.board = @board
61 @topic.children << @reply
75 @topic.children << @reply
62 if !@reply.new_record?
76 if !@reply.new_record?
63 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
77 call_hook(:controller_messages_reply_after_save, { :params => params, :message => @reply})
64 attach_files(@reply, params[:attachments])
78 attach_files(@reply, params[:attachments])
65 end
79 end
66 redirect_to :action => 'show', :id => @topic
80 redirect_to :action => 'show', :id => @topic, :r => @reply
67 end
81 end
68
82
69 # Edit a message
83 # Edit a message
70 def edit
84 def edit
71 (render_403; return false) unless @message.editable_by?(User.current)
85 (render_403; return false) unless @message.editable_by?(User.current)
72 if params[:message]
86 if params[:message]
73 @message.locked = params[:message]['locked']
87 @message.locked = params[:message]['locked']
74 @message.sticky = params[:message]['sticky']
88 @message.sticky = params[:message]['sticky']
75 end
89 end
76 if request.post? && @message.update_attributes(params[:message])
90 if request.post? && @message.update_attributes(params[:message])
77 attach_files(@message, params[:attachments])
91 attach_files(@message, params[:attachments])
78 flash[:notice] = l(:notice_successful_update)
92 flash[:notice] = l(:notice_successful_update)
79 @message.reload
93 @message.reload
80 redirect_to :action => 'show', :board_id => @message.board, :id => @message.root
94 redirect_to :action => 'show', :board_id => @message.board, :id => @message.root, :r => (@message.parent_id && @message.id)
81 end
95 end
82 end
96 end
83
97
84 # Delete a messages
98 # Delete a messages
85 def destroy
99 def destroy
86 (render_403; return false) unless @message.destroyable_by?(User.current)
100 (render_403; return false) unless @message.destroyable_by?(User.current)
87 @message.destroy
101 @message.destroy
88 redirect_to @message.parent.nil? ?
102 redirect_to @message.parent.nil? ?
89 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
103 { :controller => 'boards', :action => 'show', :project_id => @project, :id => @board } :
90 { :action => 'show', :id => @message.parent }
104 { :action => 'show', :id => @message.parent, :r => @message }
91 end
105 end
92
106
93 def quote
107 def quote
94 user = @message.author
108 user = @message.author
95 text = @message.content
109 text = @message.content
96 subject = @message.subject.gsub('"', '\"')
110 subject = @message.subject.gsub('"', '\"')
97 subject = "RE: #{subject}" unless subject.starts_with?('RE:')
111 subject = "RE: #{subject}" unless subject.starts_with?('RE:')
98 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
112 content = "#{ll(Setting.default_language, :text_user_wrote, user)}\\n> "
99 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
113 content << text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]').gsub('"', '\"').gsub(/(\r?\n|\r\n?)/, "\\n> ") + "\\n\\n"
100 render(:update) { |page|
114 render(:update) { |page|
101 page << "$('reply_subject').value = \"#{subject}\";"
115 page << "$('reply_subject').value = \"#{subject}\";"
102 page.<< "$('message_content').value = \"#{content}\";"
116 page.<< "$('message_content').value = \"#{content}\";"
103 page.show 'reply'
117 page.show 'reply'
104 page << "Form.Element.focus('message_content');"
118 page << "Form.Element.focus('message_content');"
105 page << "Element.scrollTo('reply');"
119 page << "Element.scrollTo('reply');"
106 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
120 page << "$('message_content').scrollTop = $('message_content').scrollHeight - $('message_content').clientHeight;"
107 }
121 }
108 end
122 end
109
123
110 def preview
124 def preview
111 message = @board.messages.find_by_id(params[:id])
125 message = @board.messages.find_by_id(params[:id])
112 @attachements = message.attachments if message
126 @attachements = message.attachments if message
113 @text = (params[:message] || params[:reply])[:content]
127 @text = (params[:message] || params[:reply])[:content]
114 render :partial => 'common/preview'
128 render :partial => 'common/preview'
115 end
129 end
116
130
117 private
131 private
118 def find_message
132 def find_message
119 find_board
133 find_board
120 @message = @board.messages.find(params[:id], :include => :parent)
134 @message = @board.messages.find(params[:id], :include => :parent)
121 @topic = @message.root
135 @topic = @message.root
122 rescue ActiveRecord::RecordNotFound
136 rescue ActiveRecord::RecordNotFound
123 render_404
137 render_404
124 end
138 end
125
139
126 def find_board
140 def find_board
127 @board = Board.find(params[:board_id], :include => :project)
141 @board = Board.find(params[:board_id], :include => :project)
128 @project = @board.project
142 @project = @board.project
129 rescue ActiveRecord::RecordNotFound
143 rescue ActiveRecord::RecordNotFound
130 render_404
144 render_404
131 end
145 end
132 end
146 end
@@ -1,733 +1,734
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 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
37 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
38 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
38 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
39 end
39 end
40
40
41 # Display a link to remote if user is authorized
41 # Display a link to remote if user is authorized
42 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
42 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
43 url = options[:url] || {}
43 url = options[:url] || {}
44 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
44 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
45 end
45 end
46
46
47 # Displays a link to user's account page if active
47 # Displays a link to user's account page if active
48 def link_to_user(user, options={})
48 def link_to_user(user, options={})
49 if user.is_a?(User)
49 if user.is_a?(User)
50 name = h(user.name(options[:format]))
50 name = h(user.name(options[:format]))
51 if user.active?
51 if user.active?
52 link_to name, :controller => 'users', :action => 'show', :id => user
52 link_to name, :controller => 'users', :action => 'show', :id => user
53 else
53 else
54 name
54 name
55 end
55 end
56 else
56 else
57 h(user.to_s)
57 h(user.to_s)
58 end
58 end
59 end
59 end
60
60
61 # Displays a link to +issue+ with its subject.
61 # Displays a link to +issue+ with its subject.
62 # Examples:
62 # Examples:
63 #
63 #
64 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue) # => Defect #6: This is the subject
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
66 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :subject => false) # => Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :project => true) # => Foo - Defect #6
68 #
68 #
69 def link_to_issue(issue, options={})
69 def link_to_issue(issue, options={})
70 title = nil
70 title = nil
71 subject = nil
71 subject = nil
72 if options[:subject] == false
72 if options[:subject] == false
73 title = truncate(issue.subject, :length => 60)
73 title = truncate(issue.subject, :length => 60)
74 else
74 else
75 subject = issue.subject
75 subject = issue.subject
76 if options[:truncate]
76 if options[:truncate]
77 subject = truncate(subject, :length => options[:truncate])
77 subject = truncate(subject, :length => options[:truncate])
78 end
78 end
79 end
79 end
80 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
80 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
81 :class => issue.css_classes,
81 :class => issue.css_classes,
82 :title => title
82 :title => title
83 s << ": #{h subject}" if subject
83 s << ": #{h subject}" if subject
84 s = "#{h issue.project} - " + s if options[:project]
84 s = "#{h issue.project} - " + s if options[:project]
85 s
85 s
86 end
86 end
87
87
88 # Generates a link to an attachment.
88 # Generates a link to an attachment.
89 # Options:
89 # Options:
90 # * :text - Link text (default to attachment filename)
90 # * :text - Link text (default to attachment filename)
91 # * :download - Force download (default: false)
91 # * :download - Force download (default: false)
92 def link_to_attachment(attachment, options={})
92 def link_to_attachment(attachment, options={})
93 text = options.delete(:text) || attachment.filename
93 text = options.delete(:text) || attachment.filename
94 action = options.delete(:download) ? 'download' : 'show'
94 action = options.delete(:download) ? 'download' : 'show'
95
95
96 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
96 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
97 end
97 end
98
98
99 # Generates a link to a SCM revision
99 # Generates a link to a SCM revision
100 # Options:
100 # Options:
101 # * :text - Link text (default to the formatted revision)
101 # * :text - Link text (default to the formatted revision)
102 def link_to_revision(revision, project, options={})
102 def link_to_revision(revision, project, options={})
103 text = options.delete(:text) || format_revision(revision)
103 text = options.delete(:text) || format_revision(revision)
104
104
105 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
105 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
106 end
106 end
107
107
108 def toggle_link(name, id, options={})
108 def toggle_link(name, id, options={})
109 onclick = "Element.toggle('#{id}'); "
109 onclick = "Element.toggle('#{id}'); "
110 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
110 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
111 onclick << "return false;"
111 onclick << "return false;"
112 link_to(name, "#", :onclick => onclick)
112 link_to(name, "#", :onclick => onclick)
113 end
113 end
114
114
115 def image_to_function(name, function, html_options = {})
115 def image_to_function(name, function, html_options = {})
116 html_options.symbolize_keys!
116 html_options.symbolize_keys!
117 tag(:input, html_options.merge({
117 tag(:input, html_options.merge({
118 :type => "image", :src => image_path(name),
118 :type => "image", :src => image_path(name),
119 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
119 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
120 }))
120 }))
121 end
121 end
122
122
123 def prompt_to_remote(name, text, param, url, html_options = {})
123 def prompt_to_remote(name, text, param, url, html_options = {})
124 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
124 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
125 link_to name, {}, html_options
125 link_to name, {}, html_options
126 end
126 end
127
127
128 def format_activity_title(text)
128 def format_activity_title(text)
129 h(truncate_single_line(text, :length => 100))
129 h(truncate_single_line(text, :length => 100))
130 end
130 end
131
131
132 def format_activity_day(date)
132 def format_activity_day(date)
133 date == Date.today ? l(:label_today).titleize : format_date(date)
133 date == Date.today ? l(:label_today).titleize : format_date(date)
134 end
134 end
135
135
136 def format_activity_description(text)
136 def format_activity_description(text)
137 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
137 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
138 end
138 end
139
139
140 def format_version_name(version)
140 def format_version_name(version)
141 if version.project == @project
141 if version.project == @project
142 h(version)
142 h(version)
143 else
143 else
144 h("#{version.project} - #{version}")
144 h("#{version.project} - #{version}")
145 end
145 end
146 end
146 end
147
147
148 def due_date_distance_in_words(date)
148 def due_date_distance_in_words(date)
149 if date
149 if date
150 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
150 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
151 end
151 end
152 end
152 end
153
153
154 def render_page_hierarchy(pages, node=nil)
154 def render_page_hierarchy(pages, node=nil)
155 content = ''
155 content = ''
156 if pages[node]
156 if pages[node]
157 content << "<ul class=\"pages-hierarchy\">\n"
157 content << "<ul class=\"pages-hierarchy\">\n"
158 pages[node].each do |page|
158 pages[node].each do |page|
159 content << "<li>"
159 content << "<li>"
160 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
160 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'index', :id => page.project, :page => page.title},
161 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
161 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
162 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
162 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
163 content << "</li>\n"
163 content << "</li>\n"
164 end
164 end
165 content << "</ul>\n"
165 content << "</ul>\n"
166 end
166 end
167 content
167 content
168 end
168 end
169
169
170 # Renders flash messages
170 # Renders flash messages
171 def render_flash_messages
171 def render_flash_messages
172 s = ''
172 s = ''
173 flash.each do |k,v|
173 flash.each do |k,v|
174 s << content_tag('div', v, :class => "flash #{k}")
174 s << content_tag('div', v, :class => "flash #{k}")
175 end
175 end
176 s
176 s
177 end
177 end
178
178
179 # Renders tabs and their content
179 # Renders tabs and their content
180 def render_tabs(tabs)
180 def render_tabs(tabs)
181 if tabs.any?
181 if tabs.any?
182 render :partial => 'common/tabs', :locals => {:tabs => tabs}
182 render :partial => 'common/tabs', :locals => {:tabs => tabs}
183 else
183 else
184 content_tag 'p', l(:label_no_data), :class => "nodata"
184 content_tag 'p', l(:label_no_data), :class => "nodata"
185 end
185 end
186 end
186 end
187
187
188 # Renders the project quick-jump box
188 # Renders the project quick-jump box
189 def render_project_jump_box
189 def render_project_jump_box
190 # Retrieve them now to avoid a COUNT query
190 # Retrieve them now to avoid a COUNT query
191 projects = User.current.projects.all
191 projects = User.current.projects.all
192 if projects.any?
192 if projects.any?
193 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
193 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
194 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
194 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
195 '<option value="" disabled="disabled">---</option>'
195 '<option value="" disabled="disabled">---</option>'
196 s << project_tree_options_for_select(projects, :selected => @project) do |p|
196 s << project_tree_options_for_select(projects, :selected => @project) do |p|
197 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
197 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
198 end
198 end
199 s << '</select>'
199 s << '</select>'
200 s
200 s
201 end
201 end
202 end
202 end
203
203
204 def project_tree_options_for_select(projects, options = {})
204 def project_tree_options_for_select(projects, options = {})
205 s = ''
205 s = ''
206 project_tree(projects) do |project, level|
206 project_tree(projects) do |project, level|
207 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
207 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
208 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
208 tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)}
209 tag_options.merge!(yield(project)) if block_given?
209 tag_options.merge!(yield(project)) if block_given?
210 s << content_tag('option', name_prefix + h(project), tag_options)
210 s << content_tag('option', name_prefix + h(project), tag_options)
211 end
211 end
212 s
212 s
213 end
213 end
214
214
215 # Yields the given block for each project with its level in the tree
215 # Yields the given block for each project with its level in the tree
216 def project_tree(projects, &block)
216 def project_tree(projects, &block)
217 ancestors = []
217 ancestors = []
218 projects.sort_by(&:lft).each do |project|
218 projects.sort_by(&:lft).each do |project|
219 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
219 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
220 ancestors.pop
220 ancestors.pop
221 end
221 end
222 yield project, ancestors.size
222 yield project, ancestors.size
223 ancestors << project
223 ancestors << project
224 end
224 end
225 end
225 end
226
226
227 def project_nested_ul(projects, &block)
227 def project_nested_ul(projects, &block)
228 s = ''
228 s = ''
229 if projects.any?
229 if projects.any?
230 ancestors = []
230 ancestors = []
231 projects.sort_by(&:lft).each do |project|
231 projects.sort_by(&:lft).each do |project|
232 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
232 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
233 s << "<ul>\n"
233 s << "<ul>\n"
234 else
234 else
235 ancestors.pop
235 ancestors.pop
236 s << "</li>"
236 s << "</li>"
237 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
237 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
238 ancestors.pop
238 ancestors.pop
239 s << "</ul></li>\n"
239 s << "</ul></li>\n"
240 end
240 end
241 end
241 end
242 s << "<li>"
242 s << "<li>"
243 s << yield(project).to_s
243 s << yield(project).to_s
244 ancestors << project
244 ancestors << project
245 end
245 end
246 s << ("</li></ul>\n" * ancestors.size)
246 s << ("</li></ul>\n" * ancestors.size)
247 end
247 end
248 s
248 s
249 end
249 end
250
250
251 def principals_check_box_tags(name, principals)
251 def principals_check_box_tags(name, principals)
252 s = ''
252 s = ''
253 principals.sort.each do |principal|
253 principals.sort.each do |principal|
254 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
254 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
255 end
255 end
256 s
256 s
257 end
257 end
258
258
259 # Truncates and returns the string as a single line
259 # Truncates and returns the string as a single line
260 def truncate_single_line(string, *args)
260 def truncate_single_line(string, *args)
261 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
261 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
262 end
262 end
263
263
264 def html_hours(text)
264 def html_hours(text)
265 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
265 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
266 end
266 end
267
267
268 def authoring(created, author, options={})
268 def authoring(created, author, options={})
269 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
269 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
270 end
270 end
271
271
272 def time_tag(time)
272 def time_tag(time)
273 text = distance_of_time_in_words(Time.now, time)
273 text = distance_of_time_in_words(Time.now, time)
274 if @project
274 if @project
275 link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
275 link_to(text, {:controller => 'projects', :action => 'activity', :id => @project, :from => time.to_date}, :title => format_time(time))
276 else
276 else
277 content_tag('acronym', text, :title => format_time(time))
277 content_tag('acronym', text, :title => format_time(time))
278 end
278 end
279 end
279 end
280
280
281 def syntax_highlight(name, content)
281 def syntax_highlight(name, content)
282 type = CodeRay::FileType[name]
282 type = CodeRay::FileType[name]
283 type ? CodeRay.scan(content, type).html : h(content)
283 type ? CodeRay.scan(content, type).html : h(content)
284 end
284 end
285
285
286 def to_path_param(path)
286 def to_path_param(path)
287 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
287 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
288 end
288 end
289
289
290 def pagination_links_full(paginator, count=nil, options={})
290 def pagination_links_full(paginator, count=nil, options={})
291 page_param = options.delete(:page_param) || :page
291 page_param = options.delete(:page_param) || :page
292 per_page_links = options.delete(:per_page_links)
292 url_param = params.dup
293 url_param = params.dup
293 # don't reuse query params if filters are present
294 # don't reuse query params if filters are present
294 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
295 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
295
296
296 html = ''
297 html = ''
297 if paginator.current.previous
298 if paginator.current.previous
298 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
299 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
299 end
300 end
300
301
301 html << (pagination_links_each(paginator, options) do |n|
302 html << (pagination_links_each(paginator, options) do |n|
302 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
303 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
303 end || '')
304 end || '')
304
305
305 if paginator.current.next
306 if paginator.current.next
306 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
307 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
307 end
308 end
308
309
309 unless count.nil?
310 unless count.nil?
310 html << [
311 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
311 " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})",
312 if per_page_links != false && links = per_page_links(paginator.items_per_page)
312 per_page_links(paginator.items_per_page)
313 html << " | #{links}"
313 ].compact.join(' | ')
314 end
314 end
315 end
315
316
316 html
317 html
317 end
318 end
318
319
319 def per_page_links(selected=nil)
320 def per_page_links(selected=nil)
320 url_param = params.dup
321 url_param = params.dup
321 url_param.clear if url_param.has_key?(:set_filter)
322 url_param.clear if url_param.has_key?(:set_filter)
322
323
323 links = Setting.per_page_options_array.collect do |n|
324 links = Setting.per_page_options_array.collect do |n|
324 n == selected ? n : link_to_remote(n, {:update => "content",
325 n == selected ? n : link_to_remote(n, {:update => "content",
325 :url => params.dup.merge(:per_page => n),
326 :url => params.dup.merge(:per_page => n),
326 :method => :get},
327 :method => :get},
327 {:href => url_for(url_param.merge(:per_page => n))})
328 {:href => url_for(url_param.merge(:per_page => n))})
328 end
329 end
329 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
330 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
330 end
331 end
331
332
332 def reorder_links(name, url)
333 def reorder_links(name, url)
333 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
334 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
334 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
335 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
335 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
336 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
336 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
337 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
337 end
338 end
338
339
339 def breadcrumb(*args)
340 def breadcrumb(*args)
340 elements = args.flatten
341 elements = args.flatten
341 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
342 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
342 end
343 end
343
344
344 def other_formats_links(&block)
345 def other_formats_links(&block)
345 concat('<p class="other-formats">' + l(:label_export_to))
346 concat('<p class="other-formats">' + l(:label_export_to))
346 yield Redmine::Views::OtherFormatsBuilder.new(self)
347 yield Redmine::Views::OtherFormatsBuilder.new(self)
347 concat('</p>')
348 concat('</p>')
348 end
349 end
349
350
350 def page_header_title
351 def page_header_title
351 if @project.nil? || @project.new_record?
352 if @project.nil? || @project.new_record?
352 h(Setting.app_title)
353 h(Setting.app_title)
353 else
354 else
354 b = []
355 b = []
355 ancestors = (@project.root? ? [] : @project.ancestors.visible)
356 ancestors = (@project.root? ? [] : @project.ancestors.visible)
356 if ancestors.any?
357 if ancestors.any?
357 root = ancestors.shift
358 root = ancestors.shift
358 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
359 b << link_to(h(root), {:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item}, :class => 'root')
359 if ancestors.size > 2
360 if ancestors.size > 2
360 b << '&#8230;'
361 b << '&#8230;'
361 ancestors = ancestors[-2, 2]
362 ancestors = ancestors[-2, 2]
362 end
363 end
363 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
364 b += ancestors.collect {|p| link_to(h(p), {:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item}, :class => 'ancestor') }
364 end
365 end
365 b << h(@project)
366 b << h(@project)
366 b.join(' &#187; ')
367 b.join(' &#187; ')
367 end
368 end
368 end
369 end
369
370
370 def html_title(*args)
371 def html_title(*args)
371 if args.empty?
372 if args.empty?
372 title = []
373 title = []
373 title << @project.name if @project
374 title << @project.name if @project
374 title += @html_title if @html_title
375 title += @html_title if @html_title
375 title << Setting.app_title
376 title << Setting.app_title
376 title.select {|t| !t.blank? }.join(' - ')
377 title.select {|t| !t.blank? }.join(' - ')
377 else
378 else
378 @html_title ||= []
379 @html_title ||= []
379 @html_title += args
380 @html_title += args
380 end
381 end
381 end
382 end
382
383
383 def accesskey(s)
384 def accesskey(s)
384 Redmine::AccessKeys.key_for s
385 Redmine::AccessKeys.key_for s
385 end
386 end
386
387
387 # Formats text according to system settings.
388 # Formats text according to system settings.
388 # 2 ways to call this method:
389 # 2 ways to call this method:
389 # * with a String: textilizable(text, options)
390 # * with a String: textilizable(text, options)
390 # * with an object and one of its attribute: textilizable(issue, :description, options)
391 # * with an object and one of its attribute: textilizable(issue, :description, options)
391 def textilizable(*args)
392 def textilizable(*args)
392 options = args.last.is_a?(Hash) ? args.pop : {}
393 options = args.last.is_a?(Hash) ? args.pop : {}
393 case args.size
394 case args.size
394 when 1
395 when 1
395 obj = options[:object]
396 obj = options[:object]
396 text = args.shift
397 text = args.shift
397 when 2
398 when 2
398 obj = args.shift
399 obj = args.shift
399 attr = args.shift
400 attr = args.shift
400 text = obj.send(attr).to_s
401 text = obj.send(attr).to_s
401 else
402 else
402 raise ArgumentError, 'invalid arguments to textilizable'
403 raise ArgumentError, 'invalid arguments to textilizable'
403 end
404 end
404 return '' if text.blank?
405 return '' if text.blank?
405
406
406 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
407 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
407
408
408 only_path = options.delete(:only_path) == false ? false : true
409 only_path = options.delete(:only_path) == false ? false : true
409
410
410 # when using an image link, try to use an attachment, if possible
411 # when using an image link, try to use an attachment, if possible
411 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
412 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
412
413
413 if attachments
414 if attachments
414 attachments = attachments.sort_by(&:created_on).reverse
415 attachments = attachments.sort_by(&:created_on).reverse
415 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
416 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
416 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
417 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
417
418
418 # search for the picture in attachments
419 # search for the picture in attachments
419 if found = attachments.detect { |att| att.filename.downcase == filename }
420 if found = attachments.detect { |att| att.filename.downcase == filename }
420 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
421 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
421 desc = found.description.to_s.gsub('"', '')
422 desc = found.description.to_s.gsub('"', '')
422 if !desc.blank? && alttext.blank?
423 if !desc.blank? && alttext.blank?
423 alt = " title=\"#{desc}\" alt=\"#{desc}\""
424 alt = " title=\"#{desc}\" alt=\"#{desc}\""
424 end
425 end
425 "src=\"#{image_url}\"#{alt}"
426 "src=\"#{image_url}\"#{alt}"
426 else
427 else
427 m
428 m
428 end
429 end
429 end
430 end
430 end
431 end
431
432
432
433
433 # different methods for formatting wiki links
434 # different methods for formatting wiki links
434 case options[:wiki_links]
435 case options[:wiki_links]
435 when :local
436 when :local
436 # used for local links to html files
437 # used for local links to html files
437 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
438 format_wiki_link = Proc.new {|project, title, anchor| "#{title}.html" }
438 when :anchor
439 when :anchor
439 # used for single-file wiki export
440 # used for single-file wiki export
440 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
441 format_wiki_link = Proc.new {|project, title, anchor| "##{title}" }
441 else
442 else
442 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
443 format_wiki_link = Proc.new {|project, title, anchor| url_for(:only_path => only_path, :controller => 'wiki', :action => 'index', :id => project, :page => title, :anchor => anchor) }
443 end
444 end
444
445
445 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
446 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
446
447
447 # Wiki links
448 # Wiki links
448 #
449 #
449 # Examples:
450 # Examples:
450 # [[mypage]]
451 # [[mypage]]
451 # [[mypage|mytext]]
452 # [[mypage|mytext]]
452 # wiki links can refer other project wikis, using project name or identifier:
453 # wiki links can refer other project wikis, using project name or identifier:
453 # [[project:]] -> wiki starting page
454 # [[project:]] -> wiki starting page
454 # [[project:|mytext]]
455 # [[project:|mytext]]
455 # [[project:mypage]]
456 # [[project:mypage]]
456 # [[project:mypage|mytext]]
457 # [[project:mypage|mytext]]
457 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
458 text = text.gsub(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
458 link_project = project
459 link_project = project
459 esc, all, page, title = $1, $2, $3, $5
460 esc, all, page, title = $1, $2, $3, $5
460 if esc.nil?
461 if esc.nil?
461 if page =~ /^([^\:]+)\:(.*)$/
462 if page =~ /^([^\:]+)\:(.*)$/
462 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
463 link_project = Project.find_by_name($1) || Project.find_by_identifier($1)
463 page = $2
464 page = $2
464 title ||= $1 if page.blank?
465 title ||= $1 if page.blank?
465 end
466 end
466
467
467 if link_project && link_project.wiki
468 if link_project && link_project.wiki
468 # extract anchor
469 # extract anchor
469 anchor = nil
470 anchor = nil
470 if page =~ /^(.+?)\#(.+)$/
471 if page =~ /^(.+?)\#(.+)$/
471 page, anchor = $1, $2
472 page, anchor = $1, $2
472 end
473 end
473 # check if page exists
474 # check if page exists
474 wiki_page = link_project.wiki.find_page(page)
475 wiki_page = link_project.wiki.find_page(page)
475 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
476 link_to((title || page), format_wiki_link.call(link_project, Wiki.titleize(page), anchor),
476 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
477 :class => ('wiki-page' + (wiki_page ? '' : ' new')))
477 else
478 else
478 # project or wiki doesn't exist
479 # project or wiki doesn't exist
479 all
480 all
480 end
481 end
481 else
482 else
482 all
483 all
483 end
484 end
484 end
485 end
485
486
486 # Redmine links
487 # Redmine links
487 #
488 #
488 # Examples:
489 # Examples:
489 # Issues:
490 # Issues:
490 # #52 -> Link to issue #52
491 # #52 -> Link to issue #52
491 # Changesets:
492 # Changesets:
492 # r52 -> Link to revision 52
493 # r52 -> Link to revision 52
493 # commit:a85130f -> Link to scmid starting with a85130f
494 # commit:a85130f -> Link to scmid starting with a85130f
494 # Documents:
495 # Documents:
495 # document#17 -> Link to document with id 17
496 # document#17 -> Link to document with id 17
496 # document:Greetings -> Link to the document with title "Greetings"
497 # document:Greetings -> Link to the document with title "Greetings"
497 # document:"Some document" -> Link to the document with title "Some document"
498 # document:"Some document" -> Link to the document with title "Some document"
498 # Versions:
499 # Versions:
499 # version#3 -> Link to version with id 3
500 # version#3 -> Link to version with id 3
500 # version:1.0.0 -> Link to version named "1.0.0"
501 # version:1.0.0 -> Link to version named "1.0.0"
501 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
502 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
502 # Attachments:
503 # Attachments:
503 # attachment:file.zip -> Link to the attachment of the current object named file.zip
504 # attachment:file.zip -> Link to the attachment of the current object named file.zip
504 # Source files:
505 # Source files:
505 # source:some/file -> Link to the file located at /some/file in the project's repository
506 # source:some/file -> Link to the file located at /some/file in the project's repository
506 # source:some/file@52 -> Link to the file's revision 52
507 # source:some/file@52 -> Link to the file's revision 52
507 # source:some/file#L120 -> Link to line 120 of the file
508 # source:some/file#L120 -> Link to line 120 of the file
508 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
509 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
509 # export:some/file -> Force the download of the file
510 # export:some/file -> Force the download of the file
510 # Forum messages:
511 # Forum messages:
511 # message#1218 -> Link to message with id 1218
512 # message#1218 -> Link to message with id 1218
512 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
513 text = text.gsub(%r{([\s\(,\-\>]|^)(!)?(attachment|document|version|commit|source|export|message)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|<|$)}) do |m|
513 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
514 leading, esc, prefix, sep, oid = $1, $2, $3, $5 || $7, $6 || $8
514 link = nil
515 link = nil
515 if esc.nil?
516 if esc.nil?
516 if prefix.nil? && sep == 'r'
517 if prefix.nil? && sep == 'r'
517 if project && (changeset = project.changesets.find_by_revision(oid))
518 if project && (changeset = project.changesets.find_by_revision(oid))
518 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
519 link = link_to("r#{oid}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => oid},
519 :class => 'changeset',
520 :class => 'changeset',
520 :title => truncate_single_line(changeset.comments, :length => 100))
521 :title => truncate_single_line(changeset.comments, :length => 100))
521 end
522 end
522 elsif sep == '#'
523 elsif sep == '#'
523 oid = oid.to_i
524 oid = oid.to_i
524 case prefix
525 case prefix
525 when nil
526 when nil
526 if issue = Issue.visible.find_by_id(oid, :include => :status)
527 if issue = Issue.visible.find_by_id(oid, :include => :status)
527 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
528 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
528 :class => issue.css_classes,
529 :class => issue.css_classes,
529 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
530 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
530 end
531 end
531 when 'document'
532 when 'document'
532 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
533 if document = Document.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
533 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
534 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
534 :class => 'document'
535 :class => 'document'
535 end
536 end
536 when 'version'
537 when 'version'
537 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
538 if version = Version.find_by_id(oid, :include => [:project], :conditions => Project.visible_by(User.current))
538 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
539 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
539 :class => 'version'
540 :class => 'version'
540 end
541 end
541 when 'message'
542 when 'message'
542 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
543 if message = Message.find_by_id(oid, :include => [:parent, {:board => :project}], :conditions => Project.visible_by(User.current))
543 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
544 link = link_to h(truncate(message.subject, :length => 60)), {:only_path => only_path,
544 :controller => 'messages',
545 :controller => 'messages',
545 :action => 'show',
546 :action => 'show',
546 :board_id => message.board,
547 :board_id => message.board,
547 :id => message.root,
548 :id => message.root,
548 :anchor => (message.parent ? "message-#{message.id}" : nil)},
549 :anchor => (message.parent ? "message-#{message.id}" : nil)},
549 :class => 'message'
550 :class => 'message'
550 end
551 end
551 end
552 end
552 elsif sep == ':'
553 elsif sep == ':'
553 # removes the double quotes if any
554 # removes the double quotes if any
554 name = oid.gsub(%r{^"(.*)"$}, "\\1")
555 name = oid.gsub(%r{^"(.*)"$}, "\\1")
555 case prefix
556 case prefix
556 when 'document'
557 when 'document'
557 if project && document = project.documents.find_by_title(name)
558 if project && document = project.documents.find_by_title(name)
558 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
559 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
559 :class => 'document'
560 :class => 'document'
560 end
561 end
561 when 'version'
562 when 'version'
562 if project && version = project.versions.find_by_name(name)
563 if project && version = project.versions.find_by_name(name)
563 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
564 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
564 :class => 'version'
565 :class => 'version'
565 end
566 end
566 when 'commit'
567 when 'commit'
567 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
568 if project && (changeset = project.changesets.find(:first, :conditions => ["scmid LIKE ?", "#{name}%"]))
568 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
569 link = link_to h("#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
569 :class => 'changeset',
570 :class => 'changeset',
570 :title => truncate_single_line(changeset.comments, :length => 100)
571 :title => truncate_single_line(changeset.comments, :length => 100)
571 end
572 end
572 when 'source', 'export'
573 when 'source', 'export'
573 if project && project.repository
574 if project && project.repository
574 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
575 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
575 path, rev, anchor = $1, $3, $5
576 path, rev, anchor = $1, $3, $5
576 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
577 link = link_to h("#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
577 :path => to_path_param(path),
578 :path => to_path_param(path),
578 :rev => rev,
579 :rev => rev,
579 :anchor => anchor,
580 :anchor => anchor,
580 :format => (prefix == 'export' ? 'raw' : nil)},
581 :format => (prefix == 'export' ? 'raw' : nil)},
581 :class => (prefix == 'export' ? 'source download' : 'source')
582 :class => (prefix == 'export' ? 'source download' : 'source')
582 end
583 end
583 when 'attachment'
584 when 'attachment'
584 if attachments && attachment = attachments.detect {|a| a.filename == name }
585 if attachments && attachment = attachments.detect {|a| a.filename == name }
585 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
586 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
586 :class => 'attachment'
587 :class => 'attachment'
587 end
588 end
588 end
589 end
589 end
590 end
590 end
591 end
591 leading + (link || "#{prefix}#{sep}#{oid}")
592 leading + (link || "#{prefix}#{sep}#{oid}")
592 end
593 end
593
594
594 text
595 text
595 end
596 end
596
597
597 # Same as Rails' simple_format helper without using paragraphs
598 # Same as Rails' simple_format helper without using paragraphs
598 def simple_format_without_paragraph(text)
599 def simple_format_without_paragraph(text)
599 text.to_s.
600 text.to_s.
600 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
601 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
601 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
602 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
602 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
603 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
603 end
604 end
604
605
605 def lang_options_for_select(blank=true)
606 def lang_options_for_select(blank=true)
606 (blank ? [["(auto)", ""]] : []) +
607 (blank ? [["(auto)", ""]] : []) +
607 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
608 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
608 end
609 end
609
610
610 def label_tag_for(name, option_tags = nil, options = {})
611 def label_tag_for(name, option_tags = nil, options = {})
611 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
612 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
612 content_tag("label", label_text)
613 content_tag("label", label_text)
613 end
614 end
614
615
615 def labelled_tabular_form_for(name, object, options, &proc)
616 def labelled_tabular_form_for(name, object, options, &proc)
616 options[:html] ||= {}
617 options[:html] ||= {}
617 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
618 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
618 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
619 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
619 end
620 end
620
621
621 def back_url_hidden_field_tag
622 def back_url_hidden_field_tag
622 back_url = params[:back_url] || request.env['HTTP_REFERER']
623 back_url = params[:back_url] || request.env['HTTP_REFERER']
623 back_url = CGI.unescape(back_url.to_s)
624 back_url = CGI.unescape(back_url.to_s)
624 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
625 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
625 end
626 end
626
627
627 def check_all_links(form_name)
628 def check_all_links(form_name)
628 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
629 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
629 " | " +
630 " | " +
630 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
631 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
631 end
632 end
632
633
633 def progress_bar(pcts, options={})
634 def progress_bar(pcts, options={})
634 pcts = [pcts, pcts] unless pcts.is_a?(Array)
635 pcts = [pcts, pcts] unless pcts.is_a?(Array)
635 pcts = pcts.collect(&:round)
636 pcts = pcts.collect(&:round)
636 pcts[1] = pcts[1] - pcts[0]
637 pcts[1] = pcts[1] - pcts[0]
637 pcts << (100 - pcts[1] - pcts[0])
638 pcts << (100 - pcts[1] - pcts[0])
638 width = options[:width] || '100px;'
639 width = options[:width] || '100px;'
639 legend = options[:legend] || ''
640 legend = options[:legend] || ''
640 content_tag('table',
641 content_tag('table',
641 content_tag('tr',
642 content_tag('tr',
642 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
643 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
643 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
644 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
644 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
645 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
645 ), :class => 'progress', :style => "width: #{width};") +
646 ), :class => 'progress', :style => "width: #{width};") +
646 content_tag('p', legend, :class => 'pourcent')
647 content_tag('p', legend, :class => 'pourcent')
647 end
648 end
648
649
649 def context_menu_link(name, url, options={})
650 def context_menu_link(name, url, options={})
650 options[:class] ||= ''
651 options[:class] ||= ''
651 if options.delete(:selected)
652 if options.delete(:selected)
652 options[:class] << ' icon-checked disabled'
653 options[:class] << ' icon-checked disabled'
653 options[:disabled] = true
654 options[:disabled] = true
654 end
655 end
655 if options.delete(:disabled)
656 if options.delete(:disabled)
656 options.delete(:method)
657 options.delete(:method)
657 options.delete(:confirm)
658 options.delete(:confirm)
658 options.delete(:onclick)
659 options.delete(:onclick)
659 options[:class] << ' disabled'
660 options[:class] << ' disabled'
660 url = '#'
661 url = '#'
661 end
662 end
662 link_to name, url, options
663 link_to name, url, options
663 end
664 end
664
665
665 def calendar_for(field_id)
666 def calendar_for(field_id)
666 include_calendar_headers_tags
667 include_calendar_headers_tags
667 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
668 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
668 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
669 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
669 end
670 end
670
671
671 def include_calendar_headers_tags
672 def include_calendar_headers_tags
672 unless @calendar_headers_tags_included
673 unless @calendar_headers_tags_included
673 @calendar_headers_tags_included = true
674 @calendar_headers_tags_included = true
674 content_for :header_tags do
675 content_for :header_tags do
675 start_of_week = case Setting.start_of_week.to_i
676 start_of_week = case Setting.start_of_week.to_i
676 when 1
677 when 1
677 'Calendar._FD = 1;' # Monday
678 'Calendar._FD = 1;' # Monday
678 when 7
679 when 7
679 'Calendar._FD = 0;' # Sunday
680 'Calendar._FD = 0;' # Sunday
680 else
681 else
681 '' # use language
682 '' # use language
682 end
683 end
683
684
684 javascript_include_tag('calendar/calendar') +
685 javascript_include_tag('calendar/calendar') +
685 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
686 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
686 javascript_tag(start_of_week) +
687 javascript_tag(start_of_week) +
687 javascript_include_tag('calendar/calendar-setup') +
688 javascript_include_tag('calendar/calendar-setup') +
688 stylesheet_link_tag('calendar')
689 stylesheet_link_tag('calendar')
689 end
690 end
690 end
691 end
691 end
692 end
692
693
693 def content_for(name, content = nil, &block)
694 def content_for(name, content = nil, &block)
694 @has_content ||= {}
695 @has_content ||= {}
695 @has_content[name] = true
696 @has_content[name] = true
696 super(name, content, &block)
697 super(name, content, &block)
697 end
698 end
698
699
699 def has_content?(name)
700 def has_content?(name)
700 (@has_content && @has_content[name]) || false
701 (@has_content && @has_content[name]) || false
701 end
702 end
702
703
703 # Returns the avatar image tag for the given +user+ if avatars are enabled
704 # Returns the avatar image tag for the given +user+ if avatars are enabled
704 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
705 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
705 def avatar(user, options = { })
706 def avatar(user, options = { })
706 if Setting.gravatar_enabled?
707 if Setting.gravatar_enabled?
707 options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
708 options.merge!({:ssl => Setting.protocol == 'https', :default => Setting.gravatar_default})
708 email = nil
709 email = nil
709 if user.respond_to?(:mail)
710 if user.respond_to?(:mail)
710 email = user.mail
711 email = user.mail
711 elsif user.to_s =~ %r{<(.+?)>}
712 elsif user.to_s =~ %r{<(.+?)>}
712 email = $1
713 email = $1
713 end
714 end
714 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
715 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
715 end
716 end
716 end
717 end
717
718
718 private
719 private
719
720
720 def wiki_helper
721 def wiki_helper
721 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
722 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
722 extend helper
723 extend helper
723 return self
724 return self
724 end
725 end
725
726
726 def link_to_remote_content_update(text, url_params)
727 def link_to_remote_content_update(text, url_params)
727 link_to_remote(text,
728 link_to_remote(text,
728 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
729 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
729 {:href => url_for(:params => url_params)}
730 {:href => url_for(:params => url_params)}
730 )
731 )
731 end
732 end
732
733
733 end
734 end
@@ -1,28 +1,29
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 module MessagesHelper
18 module MessagesHelper
19
19
20 def link_to_message(message)
20 def link_to_message(message)
21 return '' unless message
21 return '' unless message
22 link_to h(truncate(message.subject, :length => 60)), :controller => 'messages',
22 link_to h(truncate(message.subject, :length => 60)), :controller => 'messages',
23 :action => 'show',
23 :action => 'show',
24 :board_id => message.board_id,
24 :board_id => message.board_id,
25 :id => message.root,
25 :id => message.root,
26 :r => (message.parent_id && message.id),
26 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
27 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
27 end
28 end
28 end
29 end
@@ -1,98 +1,98
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 class Message < ActiveRecord::Base
18 class Message < ActiveRecord::Base
19 belongs_to :board
19 belongs_to :board
20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
20 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
21 acts_as_tree :counter_cache => :replies_count, :order => "#{Message.table_name}.created_on ASC"
22 acts_as_attachable
22 acts_as_attachable
23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
23 belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
24
24
25 acts_as_searchable :columns => ['subject', 'content'],
25 acts_as_searchable :columns => ['subject', 'content'],
26 :include => {:board => :project},
26 :include => {:board => :project},
27 :project_key => 'project_id',
27 :project_key => 'project_id',
28 :date_column => "#{table_name}.created_on"
28 :date_column => "#{table_name}.created_on"
29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
29 acts_as_event :title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
30 :description => :content,
30 :description => :content,
31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
31 :type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
32 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
32 :url => Proc.new {|o| {:controller => 'messages', :action => 'show', :board_id => o.board_id}.merge(o.parent_id.nil? ? {:id => o.id} :
33 {:id => o.parent_id, :anchor => "message-#{o.id}"})}
33 {:id => o.parent_id, :r => o.id, :anchor => "message-#{o.id}"})}
34
34
35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
35 acts_as_activity_provider :find_options => {:include => [{:board => :project}, :author]},
36 :author_key => :author_id
36 :author_key => :author_id
37 acts_as_watchable
37 acts_as_watchable
38
38
39 attr_protected :locked, :sticky
39 attr_protected :locked, :sticky
40 validates_presence_of :board, :subject, :content
40 validates_presence_of :board, :subject, :content
41 validates_length_of :subject, :maximum => 255
41 validates_length_of :subject, :maximum => 255
42
42
43 after_create :add_author_as_watcher
43 after_create :add_author_as_watcher
44
44
45 def visible?(user=User.current)
45 def visible?(user=User.current)
46 !user.nil? && user.allowed_to?(:view_messages, project)
46 !user.nil? && user.allowed_to?(:view_messages, project)
47 end
47 end
48
48
49 def validate_on_create
49 def validate_on_create
50 # Can not reply to a locked topic
50 # Can not reply to a locked topic
51 errors.add_to_base 'Topic is locked' if root.locked? && self != root
51 errors.add_to_base 'Topic is locked' if root.locked? && self != root
52 end
52 end
53
53
54 def after_create
54 def after_create
55 if parent
55 if parent
56 parent.reload.update_attribute(:last_reply_id, self.id)
56 parent.reload.update_attribute(:last_reply_id, self.id)
57 end
57 end
58 board.reset_counters!
58 board.reset_counters!
59 end
59 end
60
60
61 def after_update
61 def after_update
62 if board_id_changed?
62 if board_id_changed?
63 Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
63 Message.update_all("board_id = #{board_id}", ["id = ? OR parent_id = ?", root.id, root.id])
64 Board.reset_counters!(board_id_was)
64 Board.reset_counters!(board_id_was)
65 Board.reset_counters!(board_id)
65 Board.reset_counters!(board_id)
66 end
66 end
67 end
67 end
68
68
69 def after_destroy
69 def after_destroy
70 board.reset_counters!
70 board.reset_counters!
71 end
71 end
72
72
73 def sticky=(arg)
73 def sticky=(arg)
74 write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
74 write_attribute :sticky, (arg == true || arg.to_s == '1' ? 1 : 0)
75 end
75 end
76
76
77 def sticky?
77 def sticky?
78 sticky == 1
78 sticky == 1
79 end
79 end
80
80
81 def project
81 def project
82 board.project
82 board.project
83 end
83 end
84
84
85 def editable_by?(usr)
85 def editable_by?(usr)
86 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
86 usr && usr.logged? && (usr.allowed_to?(:edit_messages, project) || (self.author == usr && usr.allowed_to?(:edit_own_messages, project)))
87 end
87 end
88
88
89 def destroyable_by?(usr)
89 def destroyable_by?(usr)
90 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
90 usr && usr.logged? && (usr.allowed_to?(:delete_messages, project) || (self.author == usr && usr.allowed_to?(:delete_own_messages, project)))
91 end
91 end
92
92
93 private
93 private
94
94
95 def add_author_as_watcher
95 def add_author_as_watcher
96 Watcher.create(:watchable => self.root, :user => author)
96 Watcher.create(:watchable => self.root, :user => author)
97 end
97 end
98 end
98 end
@@ -1,65 +1,66
1 <%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}),
1 <%= breadcrumb link_to(l(:label_board_plural), {:controller => 'boards', :action => 'index', :project_id => @project}),
2 link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %>
2 link_to(h(@board.name), {:controller => 'boards', :action => 'show', :project_id => @project, :id => @board}) %>
3
3
4 <div class="contextual">
4 <div class="contextual">
5 <%= watcher_tag(@topic, User.current) %>
5 <%= watcher_tag(@topic, User.current) %>
6 <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %>
6 <%= link_to_remote_if_authorized l(:button_quote), { :url => {:action => 'quote', :id => @topic} }, :class => 'icon icon-comment' %>
7 <%= link_to(l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit') if @message.editable_by?(User.current) %>
7 <%= link_to(l(:button_edit), {:action => 'edit', :id => @topic}, :class => 'icon icon-edit') if @message.editable_by?(User.current) %>
8 <%= link_to(l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') if @message.destroyable_by?(User.current) %>
8 <%= link_to(l(:button_delete), {:action => 'destroy', :id => @topic}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') if @message.destroyable_by?(User.current) %>
9 </div>
9 </div>
10
10
11 <h2><%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %></h2>
11 <h2><%= avatar(@topic.author, :size => "24") %><%=h @topic.subject %></h2>
12
12
13 <div class="message">
13 <div class="message">
14 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
14 <p><span class="author"><%= authoring @topic.created_on, @topic.author %></span></p>
15 <div class="wiki">
15 <div class="wiki">
16 <%= textilizable(@topic.content, :attachments => @topic.attachments) %>
16 <%= textilizable(@topic.content, :attachments => @topic.attachments) %>
17 </div>
17 </div>
18 <%= link_to_attachments @topic, :author => false %>
18 <%= link_to_attachments @topic, :author => false %>
19 </div>
19 </div>
20 <br />
20 <br />
21
21
22 <% unless @replies.empty? %>
22 <% unless @replies.empty? %>
23 <h3 class="comments"><%= l(:label_reply_plural) %></h3>
23 <h3 class="comments"><%= l(:label_reply_plural) %> (<%= @reply_count %>)</h3>
24 <% @replies.each do |message| %>
24 <% @replies.each do |message| %>
25 <div class="message reply" id="<%= "message-#{message.id}" %>">
25 <div class="message reply" id="<%= "message-#{message.id}" %>">
26 <div class="contextual">
26 <div class="contextual">
27 <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %>
27 <%= link_to_remote_if_authorized image_tag('comment.png'), { :url => {:action => 'quote', :id => message} }, :title => l(:button_quote) %>
28 <%= link_to(image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit)) if message.editable_by?(User.current) %>
28 <%= link_to(image_tag('edit.png'), {:action => 'edit', :id => message}, :title => l(:button_edit)) if message.editable_by?(User.current) %>
29 <%= link_to(image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete)) if message.destroyable_by?(User.current) %>
29 <%= link_to(image_tag('delete.png'), {:action => 'destroy', :id => message}, :method => :post, :confirm => l(:text_are_you_sure), :title => l(:button_delete)) if message.destroyable_by?(User.current) %>
30 </div>
30 </div>
31 <h4>
31 <h4>
32 <%= avatar(message.author, :size => "24") %>
32 <%= avatar(message.author, :size => "24") %>
33 <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :anchor => "message-#{message.id}" } %>
33 <%= link_to h(message.subject), { :controller => 'messages', :action => 'show', :board_id => @board, :id => @topic, :anchor => "message-#{message.id}" } %>
34 -
34 -
35 <%= authoring message.created_on, message.author %>
35 <%= authoring message.created_on, message.author %>
36 </h4>
36 </h4>
37 <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
37 <div class="wiki"><%= textilizable message, :content, :attachments => message.attachments %></div>
38 <%= link_to_attachments message, :author => false %>
38 <%= link_to_attachments message, :author => false %>
39 </div>
39 </div>
40 <% end %>
40 <% end %>
41 <p class="pagination"><%= pagination_links_full @reply_pages, @reply_count, :per_page_links => false %></p>
41 <% end %>
42 <% end %>
42
43
43 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
44 <% if !@topic.locked? && authorize_for('messages', 'reply') %>
44 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
45 <p><%= toggle_link l(:button_reply), "reply", :focus => 'message_content' %></p>
45 <div id="reply" style="display:none;">
46 <div id="reply" style="display:none;">
46 <% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
47 <% form_for :reply, @reply, :url => {:action => 'reply', :id => @topic}, :html => {:multipart => true, :id => 'message-form'} do |f| %>
47 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
48 <%= render :partial => 'form', :locals => {:f => f, :replying => true} %>
48 <%= submit_tag l(:button_submit) %>
49 <%= submit_tag l(:button_submit) %>
49 <%= link_to_remote l(:label_preview),
50 <%= link_to_remote l(:label_preview),
50 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
51 { :url => { :controller => 'messages', :action => 'preview', :board_id => @board },
51 :method => 'post',
52 :method => 'post',
52 :update => 'preview',
53 :update => 'preview',
53 :with => "Form.serialize('message-form')",
54 :with => "Form.serialize('message-form')",
54 :complete => "Element.scrollTo('preview')"
55 :complete => "Element.scrollTo('preview')"
55 }, :accesskey => accesskey(:preview) %>
56 }, :accesskey => accesskey(:preview) %>
56 <% end %>
57 <% end %>
57 <div id="preview" class="wiki"></div>
58 <div id="preview" class="wiki"></div>
58 </div>
59 </div>
59 <% end %>
60 <% end %>
60
61
61 <% content_for :header_tags do %>
62 <% content_for :header_tags do %>
62 <%= stylesheet_link_tag 'scm' %>
63 <%= stylesheet_link_tag 'scm' %>
63 <% end %>
64 <% end %>
64
65
65 <% html_title h(@topic.subject) %>
66 <% html_title h(@topic.subject) %>
@@ -1,170 +1,187
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 File.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19 require 'messages_controller'
19 require 'messages_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class MessagesController; def rescue_action(e) raise e end; end
22 class MessagesController; def rescue_action(e) raise e end; end
23
23
24 class MessagesControllerTest < ActionController::TestCase
24 class MessagesControllerTest < ActionController::TestCase
25 fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
25 fixtures :projects, :users, :members, :member_roles, :roles, :boards, :messages, :enabled_modules
26
26
27 def setup
27 def setup
28 @controller = MessagesController.new
28 @controller = MessagesController.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 end
32 end
33
33
34 def test_show_routing
34 def test_show_routing
35 assert_routing(
35 assert_routing(
36 {:method => :get, :path => '/boards/22/topics/2'},
36 {:method => :get, :path => '/boards/22/topics/2'},
37 :controller => 'messages', :action => 'show', :id => '2', :board_id => '22'
37 :controller => 'messages', :action => 'show', :id => '2', :board_id => '22'
38 )
38 )
39 end
39 end
40
40
41 def test_show
41 def test_show
42 get :show, :board_id => 1, :id => 1
42 get :show, :board_id => 1, :id => 1
43 assert_response :success
43 assert_response :success
44 assert_template 'show'
44 assert_template 'show'
45 assert_not_nil assigns(:board)
45 assert_not_nil assigns(:board)
46 assert_not_nil assigns(:project)
46 assert_not_nil assigns(:project)
47 assert_not_nil assigns(:topic)
47 assert_not_nil assigns(:topic)
48 end
48 end
49
49
50 def test_show_with_pagination
51 message = Message.find(1)
52 assert_difference 'Message.count', 30 do
53 30.times do
54 message.children << Message.new(:subject => 'Reply', :content => 'Reply body', :author_id => 2, :board_id => 1)
55 end
56 end
57 get :show, :board_id => 1, :id => 1, :r => message.children.last(:order => 'id').id
58 assert_response :success
59 assert_template 'show'
60 replies = assigns(:replies)
61 assert_not_nil replies
62 assert !replies.include?(message.children.first(:order => 'id'))
63 assert replies.include?(message.children.last(:order => 'id'))
64 end
65
50 def test_show_with_reply_permission
66 def test_show_with_reply_permission
51 @request.session[:user_id] = 2
67 @request.session[:user_id] = 2
52 get :show, :board_id => 1, :id => 1
68 get :show, :board_id => 1, :id => 1
53 assert_response :success
69 assert_response :success
54 assert_template 'show'
70 assert_template 'show'
55 assert_tag :div, :attributes => { :id => 'reply' },
71 assert_tag :div, :attributes => { :id => 'reply' },
56 :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } }
72 :descendant => { :tag => 'textarea', :attributes => { :id => 'message_content' } }
57 end
73 end
58
74
59 def test_show_message_not_found
75 def test_show_message_not_found
60 get :show, :board_id => 1, :id => 99999
76 get :show, :board_id => 1, :id => 99999
61 assert_response 404
77 assert_response 404
62 end
78 end
63
79
64 def test_new_routing
80 def test_new_routing
65 assert_routing(
81 assert_routing(
66 {:method => :get, :path => '/boards/lala/topics/new'},
82 {:method => :get, :path => '/boards/lala/topics/new'},
67 :controller => 'messages', :action => 'new', :board_id => 'lala'
83 :controller => 'messages', :action => 'new', :board_id => 'lala'
68 )
84 )
69 assert_recognizes(#TODO: POST to collection, need to adjust form accordingly
85 assert_recognizes(#TODO: POST to collection, need to adjust form accordingly
70 {:controller => 'messages', :action => 'new', :board_id => 'lala'},
86 {:controller => 'messages', :action => 'new', :board_id => 'lala'},
71 {:method => :post, :path => '/boards/lala/topics/new'}
87 {:method => :post, :path => '/boards/lala/topics/new'}
72 )
88 )
73 end
89 end
74
90
75 def test_get_new
91 def test_get_new
76 @request.session[:user_id] = 2
92 @request.session[:user_id] = 2
77 get :new, :board_id => 1
93 get :new, :board_id => 1
78 assert_response :success
94 assert_response :success
79 assert_template 'new'
95 assert_template 'new'
80 end
96 end
81
97
82 def test_post_new
98 def test_post_new
83 @request.session[:user_id] = 2
99 @request.session[:user_id] = 2
84 ActionMailer::Base.deliveries.clear
100 ActionMailer::Base.deliveries.clear
85 Setting.notified_events = ['message_posted']
101 Setting.notified_events = ['message_posted']
86
102
87 post :new, :board_id => 1,
103 post :new, :board_id => 1,
88 :message => { :subject => 'Test created message',
104 :message => { :subject => 'Test created message',
89 :content => 'Message body'}
105 :content => 'Message body'}
90 message = Message.find_by_subject('Test created message')
106 message = Message.find_by_subject('Test created message')
91 assert_not_nil message
107 assert_not_nil message
92 assert_redirected_to "boards/1/topics/#{message.to_param}"
108 assert_redirected_to "boards/1/topics/#{message.to_param}"
93 assert_equal 'Message body', message.content
109 assert_equal 'Message body', message.content
94 assert_equal 2, message.author_id
110 assert_equal 2, message.author_id
95 assert_equal 1, message.board_id
111 assert_equal 1, message.board_id
96
112
97 mail = ActionMailer::Base.deliveries.last
113 mail = ActionMailer::Base.deliveries.last
98 assert_kind_of TMail::Mail, mail
114 assert_kind_of TMail::Mail, mail
99 assert_equal "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] Test created message", mail.subject
115 assert_equal "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] Test created message", mail.subject
100 assert mail.body.include?('Message body')
116 assert mail.body.include?('Message body')
101 # author
117 # author
102 assert mail.bcc.include?('jsmith@somenet.foo')
118 assert mail.bcc.include?('jsmith@somenet.foo')
103 # project member
119 # project member
104 assert mail.bcc.include?('dlopper@somenet.foo')
120 assert mail.bcc.include?('dlopper@somenet.foo')
105 end
121 end
106
122
107 def test_edit_routing
123 def test_edit_routing
108 assert_routing(
124 assert_routing(
109 {:method => :get, :path => '/boards/lala/topics/22/edit'},
125 {:method => :get, :path => '/boards/lala/topics/22/edit'},
110 :controller => 'messages', :action => 'edit', :board_id => 'lala', :id => '22'
126 :controller => 'messages', :action => 'edit', :board_id => 'lala', :id => '22'
111 )
127 )
112 assert_recognizes( #TODO: use PUT to topic_path, modify form accordingly
128 assert_recognizes( #TODO: use PUT to topic_path, modify form accordingly
113 {:controller => 'messages', :action => 'edit', :board_id => 'lala', :id => '22'},
129 {:controller => 'messages', :action => 'edit', :board_id => 'lala', :id => '22'},
114 {:method => :post, :path => '/boards/lala/topics/22/edit'}
130 {:method => :post, :path => '/boards/lala/topics/22/edit'}
115 )
131 )
116 end
132 end
117
133
118 def test_get_edit
134 def test_get_edit
119 @request.session[:user_id] = 2
135 @request.session[:user_id] = 2
120 get :edit, :board_id => 1, :id => 1
136 get :edit, :board_id => 1, :id => 1
121 assert_response :success
137 assert_response :success
122 assert_template 'edit'
138 assert_template 'edit'
123 end
139 end
124
140
125 def test_post_edit
141 def test_post_edit
126 @request.session[:user_id] = 2
142 @request.session[:user_id] = 2
127 post :edit, :board_id => 1, :id => 1,
143 post :edit, :board_id => 1, :id => 1,
128 :message => { :subject => 'New subject',
144 :message => { :subject => 'New subject',
129 :content => 'New body'}
145 :content => 'New body'}
130 assert_redirected_to 'boards/1/topics/1'
146 assert_redirected_to 'boards/1/topics/1'
131 message = Message.find(1)
147 message = Message.find(1)
132 assert_equal 'New subject', message.subject
148 assert_equal 'New subject', message.subject
133 assert_equal 'New body', message.content
149 assert_equal 'New body', message.content
134 end
150 end
135
151
136 def test_reply_routing
152 def test_reply_routing
137 assert_recognizes(
153 assert_recognizes(
138 {:controller => 'messages', :action => 'reply', :board_id => '22', :id => '555'},
154 {:controller => 'messages', :action => 'reply', :board_id => '22', :id => '555'},
139 {:method => :post, :path => '/boards/22/topics/555/replies'}
155 {:method => :post, :path => '/boards/22/topics/555/replies'}
140 )
156 )
141 end
157 end
142
158
143 def test_reply
159 def test_reply
144 @request.session[:user_id] = 2
160 @request.session[:user_id] = 2
145 post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' }
161 post :reply, :board_id => 1, :id => 1, :reply => { :content => 'This is a test reply', :subject => 'Test reply' }
146 assert_redirected_to 'boards/1/topics/1'
162 reply = Message.find(:first, :order => 'id DESC')
163 assert_redirected_to "boards/1/topics/1?r=#{reply.id}"
147 assert Message.find_by_subject('Test reply')
164 assert Message.find_by_subject('Test reply')
148 end
165 end
149
166
150 def test_destroy_routing
167 def test_destroy_routing
151 assert_recognizes(#TODO: use DELETE to topic_path, adjust form accordingly
168 assert_recognizes(#TODO: use DELETE to topic_path, adjust form accordingly
152 {:controller => 'messages', :action => 'destroy', :board_id => '22', :id => '555'},
169 {:controller => 'messages', :action => 'destroy', :board_id => '22', :id => '555'},
153 {:method => :post, :path => '/boards/22/topics/555/destroy'}
170 {:method => :post, :path => '/boards/22/topics/555/destroy'}
154 )
171 )
155 end
172 end
156
173
157 def test_destroy_topic
174 def test_destroy_topic
158 @request.session[:user_id] = 2
175 @request.session[:user_id] = 2
159 post :destroy, :board_id => 1, :id => 1
176 post :destroy, :board_id => 1, :id => 1
160 assert_redirected_to 'projects/ecookbook/boards/1'
177 assert_redirected_to 'projects/ecookbook/boards/1'
161 assert_nil Message.find_by_id(1)
178 assert_nil Message.find_by_id(1)
162 end
179 end
163
180
164 def test_quote
181 def test_quote
165 @request.session[:user_id] = 2
182 @request.session[:user_id] = 2
166 xhr :get, :quote, :board_id => 1, :id => 3
183 xhr :get, :quote, :board_id => 1, :id => 3
167 assert_response :success
184 assert_response :success
168 assert_select_rjs :show, 'reply'
185 assert_select_rjs :show, 'reply'
169 end
186 end
170 end
187 end
General Comments 0
You need to be logged in to leave comments. Login now