##// END OF EJS Templates
Moved wiki page updated_on eager load to a scope and fixed timestamp titles on wiki page index (#7818)....
Jean-Philippe Lang -
r4978:b8b35ab05f70
parent child
Show More
@@ -1,279 +1,276
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 'diff'
18 require 'diff'
19
19
20 # The WikiController follows the Rails REST controller pattern but with
20 # The WikiController follows the Rails REST controller pattern but with
21 # a few differences
21 # a few differences
22 #
22 #
23 # * index - shows a list of WikiPages grouped by page or date
23 # * index - shows a list of WikiPages grouped by page or date
24 # * new - not used
24 # * new - not used
25 # * create - not used
25 # * create - not used
26 # * show - will also show the form for creating a new wiki page
26 # * show - will also show the form for creating a new wiki page
27 # * edit - used to edit an existing or new page
27 # * edit - used to edit an existing or new page
28 # * update - used to save a wiki page update to the database, including new pages
28 # * update - used to save a wiki page update to the database, including new pages
29 # * destroy - normal
29 # * destroy - normal
30 #
30 #
31 # Other member and collection methods are also used
31 # Other member and collection methods are also used
32 #
32 #
33 # TODO: still being worked on
33 # TODO: still being worked on
34 class WikiController < ApplicationController
34 class WikiController < ApplicationController
35 default_search_scope :wiki_pages
35 default_search_scope :wiki_pages
36 before_filter :find_wiki, :authorize
36 before_filter :find_wiki, :authorize
37 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
37 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
38
38
39 verify :method => :post, :only => [:protect], :redirect_to => { :action => :show }
39 verify :method => :post, :only => [:protect], :redirect_to => { :action => :show }
40
40
41 helper :attachments
41 helper :attachments
42 include AttachmentsHelper
42 include AttachmentsHelper
43 helper :watchers
43 helper :watchers
44
44
45 # List of pages, sorted alphabetically and by parent (hierarchy)
45 # List of pages, sorted alphabetically and by parent (hierarchy)
46 def index
46 def index
47 load_pages_grouped_by_date_without_content
47 load_pages_for_index
48 @pages_by_parent_id = @pages.group_by(&:parent_id)
49 end
50
51 # List of page, by last update
52 def date_index
53 load_pages_for_index
54 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
48 end
55 end
49
56
50 # display a page (in editing mode if it doesn't exist)
57 # display a page (in editing mode if it doesn't exist)
51 def show
58 def show
52 page_title = params[:id]
59 page_title = params[:id]
53 @page = @wiki.find_or_new_page(page_title)
60 @page = @wiki.find_or_new_page(page_title)
54 if @page.new_record?
61 if @page.new_record?
55 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
62 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
56 edit
63 edit
57 render :action => 'edit'
64 render :action => 'edit'
58 else
65 else
59 render_404
66 render_404
60 end
67 end
61 return
68 return
62 end
69 end
63 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
70 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
64 # Redirects user to the current version if he's not allowed to view previous versions
71 # Redirects user to the current version if he's not allowed to view previous versions
65 redirect_to :version => nil
72 redirect_to :version => nil
66 return
73 return
67 end
74 end
68 @content = @page.content_for_version(params[:version])
75 @content = @page.content_for_version(params[:version])
69 if User.current.allowed_to?(:export_wiki_pages, @project)
76 if User.current.allowed_to?(:export_wiki_pages, @project)
70 if params[:format] == 'html'
77 if params[:format] == 'html'
71 export = render_to_string :action => 'export', :layout => false
78 export = render_to_string :action => 'export', :layout => false
72 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
79 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
73 return
80 return
74 elsif params[:format] == 'txt'
81 elsif params[:format] == 'txt'
75 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
82 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
76 return
83 return
77 end
84 end
78 end
85 end
79 @editable = editable?
86 @editable = editable?
80 render :action => 'show'
87 render :action => 'show'
81 end
88 end
82
89
83 # edit an existing page or a new one
90 # edit an existing page or a new one
84 def edit
91 def edit
85 @page = @wiki.find_or_new_page(params[:id])
92 @page = @wiki.find_or_new_page(params[:id])
86 return render_403 unless editable?
93 return render_403 unless editable?
87 @page.content = WikiContent.new(:page => @page) if @page.new_record?
94 @page.content = WikiContent.new(:page => @page) if @page.new_record?
88
95
89 @content = @page.content_for_version(params[:version])
96 @content = @page.content_for_version(params[:version])
90 @content.text = initial_page_content(@page) if @content.text.blank?
97 @content.text = initial_page_content(@page) if @content.text.blank?
91 # don't keep previous comment
98 # don't keep previous comment
92 @content.comments = nil
99 @content.comments = nil
93
100
94 # To prevent StaleObjectError exception when reverting to a previous version
101 # To prevent StaleObjectError exception when reverting to a previous version
95 @content.version = @page.content.version
102 @content.version = @page.content.version
96 rescue ActiveRecord::StaleObjectError
103 rescue ActiveRecord::StaleObjectError
97 # Optimistic locking exception
104 # Optimistic locking exception
98 flash[:error] = l(:notice_locking_conflict)
105 flash[:error] = l(:notice_locking_conflict)
99 end
106 end
100
107
101 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
108 verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
102 # Creates a new page or updates an existing one
109 # Creates a new page or updates an existing one
103 def update
110 def update
104 @page = @wiki.find_or_new_page(params[:id])
111 @page = @wiki.find_or_new_page(params[:id])
105 return render_403 unless editable?
112 return render_403 unless editable?
106 @page.content = WikiContent.new(:page => @page) if @page.new_record?
113 @page.content = WikiContent.new(:page => @page) if @page.new_record?
107
114
108 @content = @page.content_for_version(params[:version])
115 @content = @page.content_for_version(params[:version])
109 @content.text = initial_page_content(@page) if @content.text.blank?
116 @content.text = initial_page_content(@page) if @content.text.blank?
110 # don't keep previous comment
117 # don't keep previous comment
111 @content.comments = nil
118 @content.comments = nil
112
119
113 if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
120 if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
114 attachments = Attachment.attach_files(@page, params[:attachments])
121 attachments = Attachment.attach_files(@page, params[:attachments])
115 render_attachment_warning_if_needed(@page)
122 render_attachment_warning_if_needed(@page)
116 # don't save if text wasn't changed
123 # don't save if text wasn't changed
117 redirect_to :action => 'show', :project_id => @project, :id => @page.title
124 redirect_to :action => 'show', :project_id => @project, :id => @page.title
118 return
125 return
119 end
126 end
120 @content.attributes = params[:content]
127 @content.attributes = params[:content]
121 @content.author = User.current
128 @content.author = User.current
122 # if page is new @page.save will also save content, but not if page isn't a new record
129 # if page is new @page.save will also save content, but not if page isn't a new record
123 if (@page.new_record? ? @page.save : @content.save)
130 if (@page.new_record? ? @page.save : @content.save)
124 attachments = Attachment.attach_files(@page, params[:attachments])
131 attachments = Attachment.attach_files(@page, params[:attachments])
125 render_attachment_warning_if_needed(@page)
132 render_attachment_warning_if_needed(@page)
126 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
133 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
127 redirect_to :action => 'show', :project_id => @project, :id => @page.title
134 redirect_to :action => 'show', :project_id => @project, :id => @page.title
128 else
135 else
129 render :action => 'edit'
136 render :action => 'edit'
130 end
137 end
131
138
132 rescue ActiveRecord::StaleObjectError
139 rescue ActiveRecord::StaleObjectError
133 # Optimistic locking exception
140 # Optimistic locking exception
134 flash[:error] = l(:notice_locking_conflict)
141 flash[:error] = l(:notice_locking_conflict)
135 end
142 end
136
143
137 # rename a page
144 # rename a page
138 def rename
145 def rename
139 return render_403 unless editable?
146 return render_403 unless editable?
140 @page.redirect_existing_links = true
147 @page.redirect_existing_links = true
141 # used to display the *original* title if some AR validation errors occur
148 # used to display the *original* title if some AR validation errors occur
142 @original_title = @page.pretty_title
149 @original_title = @page.pretty_title
143 if request.post? && @page.update_attributes(params[:wiki_page])
150 if request.post? && @page.update_attributes(params[:wiki_page])
144 flash[:notice] = l(:notice_successful_update)
151 flash[:notice] = l(:notice_successful_update)
145 redirect_to :action => 'show', :project_id => @project, :id => @page.title
152 redirect_to :action => 'show', :project_id => @project, :id => @page.title
146 end
153 end
147 end
154 end
148
155
149 def protect
156 def protect
150 @page.update_attribute :protected, params[:protected]
157 @page.update_attribute :protected, params[:protected]
151 redirect_to :action => 'show', :project_id => @project, :id => @page.title
158 redirect_to :action => 'show', :project_id => @project, :id => @page.title
152 end
159 end
153
160
154 # show page history
161 # show page history
155 def history
162 def history
156 @version_count = @page.content.versions.count
163 @version_count = @page.content.versions.count
157 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
164 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
158 # don't load text
165 # don't load text
159 @versions = @page.content.versions.find :all,
166 @versions = @page.content.versions.find :all,
160 :select => "id, author_id, comments, updated_on, version",
167 :select => "id, author_id, comments, updated_on, version",
161 :order => 'version DESC',
168 :order => 'version DESC',
162 :limit => @version_pages.items_per_page + 1,
169 :limit => @version_pages.items_per_page + 1,
163 :offset => @version_pages.current.offset
170 :offset => @version_pages.current.offset
164
171
165 render :layout => false if request.xhr?
172 render :layout => false if request.xhr?
166 end
173 end
167
174
168 def diff
175 def diff
169 @diff = @page.diff(params[:version], params[:version_from])
176 @diff = @page.diff(params[:version], params[:version_from])
170 render_404 unless @diff
177 render_404 unless @diff
171 end
178 end
172
179
173 def annotate
180 def annotate
174 @annotate = @page.annotate(params[:version])
181 @annotate = @page.annotate(params[:version])
175 render_404 unless @annotate
182 render_404 unless @annotate
176 end
183 end
177
184
178 verify :method => :delete, :only => [:destroy], :redirect_to => { :action => :show }
185 verify :method => :delete, :only => [:destroy], :redirect_to => { :action => :show }
179 # Removes a wiki page and its history
186 # Removes a wiki page and its history
180 # Children can be either set as root pages, removed or reassigned to another parent page
187 # Children can be either set as root pages, removed or reassigned to another parent page
181 def destroy
188 def destroy
182 return render_403 unless editable?
189 return render_403 unless editable?
183
190
184 @descendants_count = @page.descendants.size
191 @descendants_count = @page.descendants.size
185 if @descendants_count > 0
192 if @descendants_count > 0
186 case params[:todo]
193 case params[:todo]
187 when 'nullify'
194 when 'nullify'
188 # Nothing to do
195 # Nothing to do
189 when 'destroy'
196 when 'destroy'
190 # Removes all its descendants
197 # Removes all its descendants
191 @page.descendants.each(&:destroy)
198 @page.descendants.each(&:destroy)
192 when 'reassign'
199 when 'reassign'
193 # Reassign children to another parent page
200 # Reassign children to another parent page
194 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
201 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
195 return unless reassign_to
202 return unless reassign_to
196 @page.children.each do |child|
203 @page.children.each do |child|
197 child.update_attribute(:parent, reassign_to)
204 child.update_attribute(:parent, reassign_to)
198 end
205 end
199 else
206 else
200 @reassignable_to = @wiki.pages - @page.self_and_descendants
207 @reassignable_to = @wiki.pages - @page.self_and_descendants
201 return
208 return
202 end
209 end
203 end
210 end
204 @page.destroy
211 @page.destroy
205 redirect_to :action => 'index', :project_id => @project
212 redirect_to :action => 'index', :project_id => @project
206 end
213 end
207
214
208 # Export wiki to a single html file
215 # Export wiki to a single html file
209 def export
216 def export
210 if User.current.allowed_to?(:export_wiki_pages, @project)
217 if User.current.allowed_to?(:export_wiki_pages, @project)
211 @pages = @wiki.pages.find :all, :order => 'title'
218 @pages = @wiki.pages.find :all, :order => 'title'
212 export = render_to_string :action => 'export_multiple', :layout => false
219 export = render_to_string :action => 'export_multiple', :layout => false
213 send_data(export, :type => 'text/html', :filename => "wiki.html")
220 send_data(export, :type => 'text/html', :filename => "wiki.html")
214 else
221 else
215 redirect_to :action => 'show', :project_id => @project, :id => nil
222 redirect_to :action => 'show', :project_id => @project, :id => nil
216 end
223 end
217 end
224 end
218
219 def date_index
220 load_pages_grouped_by_date_without_content
221 end
222
225
223 def preview
226 def preview
224 page = @wiki.find_page(params[:id])
227 page = @wiki.find_page(params[:id])
225 # page is nil when previewing a new page
228 # page is nil when previewing a new page
226 return render_403 unless page.nil? || editable?(page)
229 return render_403 unless page.nil? || editable?(page)
227 if page
230 if page
228 @attachements = page.attachments
231 @attachements = page.attachments
229 @previewed = page.content
232 @previewed = page.content
230 end
233 end
231 @text = params[:content][:text]
234 @text = params[:content][:text]
232 render :partial => 'common/preview'
235 render :partial => 'common/preview'
233 end
236 end
234
237
235 def add_attachment
238 def add_attachment
236 return render_403 unless editable?
239 return render_403 unless editable?
237 attachments = Attachment.attach_files(@page, params[:attachments])
240 attachments = Attachment.attach_files(@page, params[:attachments])
238 render_attachment_warning_if_needed(@page)
241 render_attachment_warning_if_needed(@page)
239 redirect_to :action => 'show', :id => @page.title, :project_id => @project
242 redirect_to :action => 'show', :id => @page.title, :project_id => @project
240 end
243 end
241
244
242 private
245 private
243
246
244 def find_wiki
247 def find_wiki
245 @project = Project.find(params[:project_id])
248 @project = Project.find(params[:project_id])
246 @wiki = @project.wiki
249 @wiki = @project.wiki
247 render_404 unless @wiki
250 render_404 unless @wiki
248 rescue ActiveRecord::RecordNotFound
251 rescue ActiveRecord::RecordNotFound
249 render_404
252 render_404
250 end
253 end
251
254
252 # Finds the requested page and returns a 404 error if it doesn't exist
255 # Finds the requested page and returns a 404 error if it doesn't exist
253 def find_existing_page
256 def find_existing_page
254 @page = @wiki.find_page(params[:id])
257 @page = @wiki.find_page(params[:id])
255 render_404 if @page.nil?
258 render_404 if @page.nil?
256 end
259 end
257
260
258 # Returns true if the current user is allowed to edit the page, otherwise false
261 # Returns true if the current user is allowed to edit the page, otherwise false
259 def editable?(page = @page)
262 def editable?(page = @page)
260 page.editable_by?(User.current)
263 page.editable_by?(User.current)
261 end
264 end
262
265
263 # Returns the default content of a new wiki page
266 # Returns the default content of a new wiki page
264 def initial_page_content(page)
267 def initial_page_content(page)
265 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
268 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
266 extend helper unless self.instance_of?(helper)
269 extend helper unless self.instance_of?(helper)
267 helper.instance_method(:initial_page_content).bind(self).call(page)
270 helper.instance_method(:initial_page_content).bind(self).call(page)
268 end
271 end
269
270 # eager load information about last updates, without loading text
271 def load_pages_grouped_by_date_without_content
272 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
273 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
274 :order => 'title'
275 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
276 @pages_by_parent_id = @pages.group_by(&:parent_id)
277 end
278
272
273 def load_pages_for_index
274 @pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
275 end
279 end
276 end
@@ -1,948 +1,948
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 'forwardable'
18 require 'forwardable'
19 require 'cgi'
19 require 'cgi'
20
20
21 module ApplicationHelper
21 module ApplicationHelper
22 include Redmine::WikiFormatting::Macros::Definitions
22 include Redmine::WikiFormatting::Macros::Definitions
23 include Redmine::I18n
23 include Redmine::I18n
24 include GravatarHelper::PublicMethods
24 include GravatarHelper::PublicMethods
25
25
26 extend Forwardable
26 extend Forwardable
27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
27 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
28
28
29 # Return true if user is authorized for controller/action, otherwise false
29 # Return true if user is authorized for controller/action, otherwise false
30 def authorize_for(controller, action)
30 def authorize_for(controller, action)
31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
31 User.current.allowed_to?({:controller => controller, :action => action}, @project)
32 end
32 end
33
33
34 # Display a link if user is authorized
34 # Display a link if user is authorized
35 #
35 #
36 # @param [String] name Anchor text (passed to link_to)
36 # @param [String] name Anchor text (passed to link_to)
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
37 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
38 # @param [optional, Hash] html_options Options passed to link_to
38 # @param [optional, Hash] html_options Options passed to link_to
39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
39 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
40 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
41 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
42 end
42 end
43
43
44 # Display a link to remote if user is authorized
44 # Display a link to remote if user is authorized
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
45 def link_to_remote_if_authorized(name, options = {}, html_options = nil)
46 url = options[:url] || {}
46 url = options[:url] || {}
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
47 link_to_remote(name, options, html_options) if authorize_for(url[:controller] || params[:controller], url[:action])
48 end
48 end
49
49
50 # Displays a link to user's account page if active
50 # Displays a link to user's account page if active
51 def link_to_user(user, options={})
51 def link_to_user(user, options={})
52 if user.is_a?(User)
52 if user.is_a?(User)
53 name = h(user.name(options[:format]))
53 name = h(user.name(options[:format]))
54 if user.active?
54 if user.active?
55 link_to name, :controller => 'users', :action => 'show', :id => user
55 link_to name, :controller => 'users', :action => 'show', :id => user
56 else
56 else
57 name
57 name
58 end
58 end
59 else
59 else
60 h(user.to_s)
60 h(user.to_s)
61 end
61 end
62 end
62 end
63
63
64 # Displays a link to +issue+ with its subject.
64 # Displays a link to +issue+ with its subject.
65 # Examples:
65 # Examples:
66 #
66 #
67 # link_to_issue(issue) # => Defect #6: This is the subject
67 # link_to_issue(issue) # => Defect #6: This is the subject
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
68 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
69 # link_to_issue(issue, :subject => false) # => Defect #6
69 # link_to_issue(issue, :subject => false) # => Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
70 # link_to_issue(issue, :project => true) # => Foo - Defect #6
71 #
71 #
72 def link_to_issue(issue, options={})
72 def link_to_issue(issue, options={})
73 title = nil
73 title = nil
74 subject = nil
74 subject = nil
75 if options[:subject] == false
75 if options[:subject] == false
76 title = truncate(issue.subject, :length => 60)
76 title = truncate(issue.subject, :length => 60)
77 else
77 else
78 subject = issue.subject
78 subject = issue.subject
79 if options[:truncate]
79 if options[:truncate]
80 subject = truncate(subject, :length => options[:truncate])
80 subject = truncate(subject, :length => options[:truncate])
81 end
81 end
82 end
82 end
83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
83 s = link_to "#{issue.tracker} ##{issue.id}", {:controller => "issues", :action => "show", :id => issue},
84 :class => issue.css_classes,
84 :class => issue.css_classes,
85 :title => title
85 :title => title
86 s << ": #{h subject}" if subject
86 s << ": #{h subject}" if subject
87 s = "#{h issue.project} - " + s if options[:project]
87 s = "#{h issue.project} - " + s if options[:project]
88 s
88 s
89 end
89 end
90
90
91 # Generates a link to an attachment.
91 # Generates a link to an attachment.
92 # Options:
92 # Options:
93 # * :text - Link text (default to attachment filename)
93 # * :text - Link text (default to attachment filename)
94 # * :download - Force download (default: false)
94 # * :download - Force download (default: false)
95 def link_to_attachment(attachment, options={})
95 def link_to_attachment(attachment, options={})
96 text = options.delete(:text) || attachment.filename
96 text = options.delete(:text) || attachment.filename
97 action = options.delete(:download) ? 'download' : 'show'
97 action = options.delete(:download) ? 'download' : 'show'
98
98
99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
99 link_to(h(text), {:controller => 'attachments', :action => action, :id => attachment, :filename => attachment.filename }, options)
100 end
100 end
101
101
102 # Generates a link to a SCM revision
102 # Generates a link to a SCM revision
103 # Options:
103 # Options:
104 # * :text - Link text (default to the formatted revision)
104 # * :text - Link text (default to the formatted revision)
105 def link_to_revision(revision, project, options={})
105 def link_to_revision(revision, project, options={})
106 text = options.delete(:text) || format_revision(revision)
106 text = options.delete(:text) || format_revision(revision)
107 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
107 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
108
108
109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
109 link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => rev},
110 :title => l(:label_revision_id, format_revision(revision)))
110 :title => l(:label_revision_id, format_revision(revision)))
111 end
111 end
112
112
113 # Generates a link to a message
113 # Generates a link to a message
114 def link_to_message(message, options={}, html_options = nil)
114 def link_to_message(message, options={}, html_options = nil)
115 link_to(
115 link_to(
116 h(truncate(message.subject, :length => 60)),
116 h(truncate(message.subject, :length => 60)),
117 { :controller => 'messages', :action => 'show',
117 { :controller => 'messages', :action => 'show',
118 :board_id => message.board_id,
118 :board_id => message.board_id,
119 :id => message.root,
119 :id => message.root,
120 :r => (message.parent_id && message.id),
120 :r => (message.parent_id && message.id),
121 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
121 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
122 }.merge(options),
122 }.merge(options),
123 html_options
123 html_options
124 )
124 )
125 end
125 end
126
126
127 # Generates a link to a project if active
127 # Generates a link to a project if active
128 # Examples:
128 # Examples:
129 #
129 #
130 # link_to_project(project) # => link to the specified project overview
130 # link_to_project(project) # => link to the specified project overview
131 # link_to_project(project, :action=>'settings') # => link to project settings
131 # link_to_project(project, :action=>'settings') # => link to project settings
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
132 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
133 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
134 #
134 #
135 def link_to_project(project, options={}, html_options = nil)
135 def link_to_project(project, options={}, html_options = nil)
136 if project.active?
136 if project.active?
137 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
137 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
138 link_to(h(project), url, html_options)
138 link_to(h(project), url, html_options)
139 else
139 else
140 h(project)
140 h(project)
141 end
141 end
142 end
142 end
143
143
144 def toggle_link(name, id, options={})
144 def toggle_link(name, id, options={})
145 onclick = "Element.toggle('#{id}'); "
145 onclick = "Element.toggle('#{id}'); "
146 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
146 onclick << (options[:focus] ? "Form.Element.focus('#{options[:focus]}'); " : "this.blur(); ")
147 onclick << "return false;"
147 onclick << "return false;"
148 link_to(name, "#", :onclick => onclick)
148 link_to(name, "#", :onclick => onclick)
149 end
149 end
150
150
151 def image_to_function(name, function, html_options = {})
151 def image_to_function(name, function, html_options = {})
152 html_options.symbolize_keys!
152 html_options.symbolize_keys!
153 tag(:input, html_options.merge({
153 tag(:input, html_options.merge({
154 :type => "image", :src => image_path(name),
154 :type => "image", :src => image_path(name),
155 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
155 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
156 }))
156 }))
157 end
157 end
158
158
159 def prompt_to_remote(name, text, param, url, html_options = {})
159 def prompt_to_remote(name, text, param, url, html_options = {})
160 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
160 html_options[:onclick] = "promptToRemote('#{text}', '#{param}', '#{url_for(url)}'); return false;"
161 link_to name, {}, html_options
161 link_to name, {}, html_options
162 end
162 end
163
163
164 def format_activity_title(text)
164 def format_activity_title(text)
165 h(truncate_single_line(text, :length => 100))
165 h(truncate_single_line(text, :length => 100))
166 end
166 end
167
167
168 def format_activity_day(date)
168 def format_activity_day(date)
169 date == Date.today ? l(:label_today).titleize : format_date(date)
169 date == Date.today ? l(:label_today).titleize : format_date(date)
170 end
170 end
171
171
172 def format_activity_description(text)
172 def format_activity_description(text)
173 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
173 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')).gsub(/[\r\n]+/, "<br />")
174 end
174 end
175
175
176 def format_version_name(version)
176 def format_version_name(version)
177 if version.project == @project
177 if version.project == @project
178 h(version)
178 h(version)
179 else
179 else
180 h("#{version.project} - #{version}")
180 h("#{version.project} - #{version}")
181 end
181 end
182 end
182 end
183
183
184 def due_date_distance_in_words(date)
184 def due_date_distance_in_words(date)
185 if date
185 if date
186 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
186 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
187 end
187 end
188 end
188 end
189
189
190 def render_page_hierarchy(pages, node=nil)
190 def render_page_hierarchy(pages, node=nil)
191 content = ''
191 content = ''
192 if pages[node]
192 if pages[node]
193 content << "<ul class=\"pages-hierarchy\">\n"
193 content << "<ul class=\"pages-hierarchy\">\n"
194 pages[node].each do |page|
194 pages[node].each do |page|
195 content << "<li>"
195 content << "<li>"
196 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
196 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title},
197 :title => (page.respond_to?(:updated_on) ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
197 :title => (page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
198 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
198 content << "\n" + render_page_hierarchy(pages, page.id) if pages[page.id]
199 content << "</li>\n"
199 content << "</li>\n"
200 end
200 end
201 content << "</ul>\n"
201 content << "</ul>\n"
202 end
202 end
203 content
203 content
204 end
204 end
205
205
206 # Renders flash messages
206 # Renders flash messages
207 def render_flash_messages
207 def render_flash_messages
208 s = ''
208 s = ''
209 flash.each do |k,v|
209 flash.each do |k,v|
210 s << content_tag('div', v, :class => "flash #{k}")
210 s << content_tag('div', v, :class => "flash #{k}")
211 end
211 end
212 s
212 s
213 end
213 end
214
214
215 # Renders tabs and their content
215 # Renders tabs and their content
216 def render_tabs(tabs)
216 def render_tabs(tabs)
217 if tabs.any?
217 if tabs.any?
218 render :partial => 'common/tabs', :locals => {:tabs => tabs}
218 render :partial => 'common/tabs', :locals => {:tabs => tabs}
219 else
219 else
220 content_tag 'p', l(:label_no_data), :class => "nodata"
220 content_tag 'p', l(:label_no_data), :class => "nodata"
221 end
221 end
222 end
222 end
223
223
224 # Renders the project quick-jump box
224 # Renders the project quick-jump box
225 def render_project_jump_box
225 def render_project_jump_box
226 # Retrieve them now to avoid a COUNT query
226 # Retrieve them now to avoid a COUNT query
227 projects = User.current.projects.all
227 projects = User.current.projects.all
228 if projects.any?
228 if projects.any?
229 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
229 s = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
230 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
230 "<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
231 '<option value="" disabled="disabled">---</option>'
231 '<option value="" disabled="disabled">---</option>'
232 s << project_tree_options_for_select(projects, :selected => @project) do |p|
232 s << project_tree_options_for_select(projects, :selected => @project) do |p|
233 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
233 { :value => url_for(:controller => 'projects', :action => 'show', :id => p, :jump => current_menu_item) }
234 end
234 end
235 s << '</select>'
235 s << '</select>'
236 s
236 s
237 end
237 end
238 end
238 end
239
239
240 def project_tree_options_for_select(projects, options = {})
240 def project_tree_options_for_select(projects, options = {})
241 s = ''
241 s = ''
242 project_tree(projects) do |project, level|
242 project_tree(projects) do |project, level|
243 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
243 name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
244 tag_options = {:value => project.id}
244 tag_options = {:value => project.id}
245 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
245 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
246 tag_options[:selected] = 'selected'
246 tag_options[:selected] = 'selected'
247 else
247 else
248 tag_options[:selected] = nil
248 tag_options[:selected] = nil
249 end
249 end
250 tag_options.merge!(yield(project)) if block_given?
250 tag_options.merge!(yield(project)) if block_given?
251 s << content_tag('option', name_prefix + h(project), tag_options)
251 s << content_tag('option', name_prefix + h(project), tag_options)
252 end
252 end
253 s
253 s
254 end
254 end
255
255
256 # Yields the given block for each project with its level in the tree
256 # Yields the given block for each project with its level in the tree
257 #
257 #
258 # Wrapper for Project#project_tree
258 # Wrapper for Project#project_tree
259 def project_tree(projects, &block)
259 def project_tree(projects, &block)
260 Project.project_tree(projects, &block)
260 Project.project_tree(projects, &block)
261 end
261 end
262
262
263 def project_nested_ul(projects, &block)
263 def project_nested_ul(projects, &block)
264 s = ''
264 s = ''
265 if projects.any?
265 if projects.any?
266 ancestors = []
266 ancestors = []
267 projects.sort_by(&:lft).each do |project|
267 projects.sort_by(&:lft).each do |project|
268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
268 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
269 s << "<ul>\n"
269 s << "<ul>\n"
270 else
270 else
271 ancestors.pop
271 ancestors.pop
272 s << "</li>"
272 s << "</li>"
273 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
273 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
274 ancestors.pop
274 ancestors.pop
275 s << "</ul></li>\n"
275 s << "</ul></li>\n"
276 end
276 end
277 end
277 end
278 s << "<li>"
278 s << "<li>"
279 s << yield(project).to_s
279 s << yield(project).to_s
280 ancestors << project
280 ancestors << project
281 end
281 end
282 s << ("</li></ul>\n" * ancestors.size)
282 s << ("</li></ul>\n" * ancestors.size)
283 end
283 end
284 s
284 s
285 end
285 end
286
286
287 def principals_check_box_tags(name, principals)
287 def principals_check_box_tags(name, principals)
288 s = ''
288 s = ''
289 principals.sort.each do |principal|
289 principals.sort.each do |principal|
290 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
290 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
291 end
291 end
292 s
292 s
293 end
293 end
294
294
295 # Truncates and returns the string as a single line
295 # Truncates and returns the string as a single line
296 def truncate_single_line(string, *args)
296 def truncate_single_line(string, *args)
297 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
297 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
298 end
298 end
299
299
300 # Truncates at line break after 250 characters or options[:length]
300 # Truncates at line break after 250 characters or options[:length]
301 def truncate_lines(string, options={})
301 def truncate_lines(string, options={})
302 length = options[:length] || 250
302 length = options[:length] || 250
303 if string.to_s =~ /\A(.{#{length}}.*?)$/m
303 if string.to_s =~ /\A(.{#{length}}.*?)$/m
304 "#{$1}..."
304 "#{$1}..."
305 else
305 else
306 string
306 string
307 end
307 end
308 end
308 end
309
309
310 def html_hours(text)
310 def html_hours(text)
311 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
311 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>')
312 end
312 end
313
313
314 def authoring(created, author, options={})
314 def authoring(created, author, options={})
315 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
315 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created))
316 end
316 end
317
317
318 def time_tag(time)
318 def time_tag(time)
319 text = distance_of_time_in_words(Time.now, time)
319 text = distance_of_time_in_words(Time.now, time)
320 if @project
320 if @project
321 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
321 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => time.to_date}, :title => format_time(time))
322 else
322 else
323 content_tag('acronym', text, :title => format_time(time))
323 content_tag('acronym', text, :title => format_time(time))
324 end
324 end
325 end
325 end
326
326
327 def syntax_highlight(name, content)
327 def syntax_highlight(name, content)
328 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
328 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
329 end
329 end
330
330
331 def to_path_param(path)
331 def to_path_param(path)
332 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
332 path.to_s.split(%r{[/\\]}).select {|p| !p.blank?}
333 end
333 end
334
334
335 def pagination_links_full(paginator, count=nil, options={})
335 def pagination_links_full(paginator, count=nil, options={})
336 page_param = options.delete(:page_param) || :page
336 page_param = options.delete(:page_param) || :page
337 per_page_links = options.delete(:per_page_links)
337 per_page_links = options.delete(:per_page_links)
338 url_param = params.dup
338 url_param = params.dup
339 # don't reuse query params if filters are present
339 # don't reuse query params if filters are present
340 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
340 url_param.merge!(:fields => nil, :values => nil, :operators => nil) if url_param.delete(:set_filter)
341
341
342 html = ''
342 html = ''
343 if paginator.current.previous
343 if paginator.current.previous
344 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
344 html << link_to_remote_content_update('&#171; ' + l(:label_previous), url_param.merge(page_param => paginator.current.previous)) + ' '
345 end
345 end
346
346
347 html << (pagination_links_each(paginator, options) do |n|
347 html << (pagination_links_each(paginator, options) do |n|
348 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
348 link_to_remote_content_update(n.to_s, url_param.merge(page_param => n))
349 end || '')
349 end || '')
350
350
351 if paginator.current.next
351 if paginator.current.next
352 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
352 html << ' ' + link_to_remote_content_update((l(:label_next) + ' &#187;'), url_param.merge(page_param => paginator.current.next))
353 end
353 end
354
354
355 unless count.nil?
355 unless count.nil?
356 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
356 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
357 if per_page_links != false && links = per_page_links(paginator.items_per_page)
357 if per_page_links != false && links = per_page_links(paginator.items_per_page)
358 html << " | #{links}"
358 html << " | #{links}"
359 end
359 end
360 end
360 end
361
361
362 html
362 html
363 end
363 end
364
364
365 def per_page_links(selected=nil)
365 def per_page_links(selected=nil)
366 url_param = params.dup
366 url_param = params.dup
367 url_param.clear if url_param.has_key?(:set_filter)
367 url_param.clear if url_param.has_key?(:set_filter)
368
368
369 links = Setting.per_page_options_array.collect do |n|
369 links = Setting.per_page_options_array.collect do |n|
370 n == selected ? n : link_to_remote(n, {:update => "content",
370 n == selected ? n : link_to_remote(n, {:update => "content",
371 :url => params.dup.merge(:per_page => n),
371 :url => params.dup.merge(:per_page => n),
372 :method => :get},
372 :method => :get},
373 {:href => url_for(url_param.merge(:per_page => n))})
373 {:href => url_for(url_param.merge(:per_page => n))})
374 end
374 end
375 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
375 links.size > 1 ? l(:label_display_per_page, links.join(', ')) : nil
376 end
376 end
377
377
378 def reorder_links(name, url)
378 def reorder_links(name, url)
379 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
379 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)), url.merge({"#{name}[move_to]" => 'highest'}), :method => :post, :title => l(:label_sort_highest)) +
380 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
380 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)), url.merge({"#{name}[move_to]" => 'higher'}), :method => :post, :title => l(:label_sort_higher)) +
381 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
381 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)), url.merge({"#{name}[move_to]" => 'lower'}), :method => :post, :title => l(:label_sort_lower)) +
382 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
382 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)), url.merge({"#{name}[move_to]" => 'lowest'}), :method => :post, :title => l(:label_sort_lowest))
383 end
383 end
384
384
385 def breadcrumb(*args)
385 def breadcrumb(*args)
386 elements = args.flatten
386 elements = args.flatten
387 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
387 elements.any? ? content_tag('p', args.join(' &#187; ') + ' &#187; ', :class => 'breadcrumb') : nil
388 end
388 end
389
389
390 def other_formats_links(&block)
390 def other_formats_links(&block)
391 concat('<p class="other-formats">' + l(:label_export_to))
391 concat('<p class="other-formats">' + l(:label_export_to))
392 yield Redmine::Views::OtherFormatsBuilder.new(self)
392 yield Redmine::Views::OtherFormatsBuilder.new(self)
393 concat('</p>')
393 concat('</p>')
394 end
394 end
395
395
396 def page_header_title
396 def page_header_title
397 if @project.nil? || @project.new_record?
397 if @project.nil? || @project.new_record?
398 h(Setting.app_title)
398 h(Setting.app_title)
399 else
399 else
400 b = []
400 b = []
401 ancestors = (@project.root? ? [] : @project.ancestors.visible)
401 ancestors = (@project.root? ? [] : @project.ancestors.visible)
402 if ancestors.any?
402 if ancestors.any?
403 root = ancestors.shift
403 root = ancestors.shift
404 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
404 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
405 if ancestors.size > 2
405 if ancestors.size > 2
406 b << '&#8230;'
406 b << '&#8230;'
407 ancestors = ancestors[-2, 2]
407 ancestors = ancestors[-2, 2]
408 end
408 end
409 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
409 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
410 end
410 end
411 b << h(@project)
411 b << h(@project)
412 b.join(' &#187; ')
412 b.join(' &#187; ')
413 end
413 end
414 end
414 end
415
415
416 def html_title(*args)
416 def html_title(*args)
417 if args.empty?
417 if args.empty?
418 title = []
418 title = []
419 title << @project.name if @project
419 title << @project.name if @project
420 title += @html_title if @html_title
420 title += @html_title if @html_title
421 title << Setting.app_title
421 title << Setting.app_title
422 title.select {|t| !t.blank? }.join(' - ')
422 title.select {|t| !t.blank? }.join(' - ')
423 else
423 else
424 @html_title ||= []
424 @html_title ||= []
425 @html_title += args
425 @html_title += args
426 end
426 end
427 end
427 end
428
428
429 # Returns the theme, controller name, and action as css classes for the
429 # Returns the theme, controller name, and action as css classes for the
430 # HTML body.
430 # HTML body.
431 def body_css_classes
431 def body_css_classes
432 css = []
432 css = []
433 if theme = Redmine::Themes.theme(Setting.ui_theme)
433 if theme = Redmine::Themes.theme(Setting.ui_theme)
434 css << 'theme-' + theme.name
434 css << 'theme-' + theme.name
435 end
435 end
436
436
437 css << 'controller-' + params[:controller]
437 css << 'controller-' + params[:controller]
438 css << 'action-' + params[:action]
438 css << 'action-' + params[:action]
439 css.join(' ')
439 css.join(' ')
440 end
440 end
441
441
442 def accesskey(s)
442 def accesskey(s)
443 Redmine::AccessKeys.key_for s
443 Redmine::AccessKeys.key_for s
444 end
444 end
445
445
446 # Formats text according to system settings.
446 # Formats text according to system settings.
447 # 2 ways to call this method:
447 # 2 ways to call this method:
448 # * with a String: textilizable(text, options)
448 # * with a String: textilizable(text, options)
449 # * with an object and one of its attribute: textilizable(issue, :description, options)
449 # * with an object and one of its attribute: textilizable(issue, :description, options)
450 def textilizable(*args)
450 def textilizable(*args)
451 options = args.last.is_a?(Hash) ? args.pop : {}
451 options = args.last.is_a?(Hash) ? args.pop : {}
452 case args.size
452 case args.size
453 when 1
453 when 1
454 obj = options[:object]
454 obj = options[:object]
455 text = args.shift
455 text = args.shift
456 when 2
456 when 2
457 obj = args.shift
457 obj = args.shift
458 attr = args.shift
458 attr = args.shift
459 text = obj.send(attr).to_s
459 text = obj.send(attr).to_s
460 else
460 else
461 raise ArgumentError, 'invalid arguments to textilizable'
461 raise ArgumentError, 'invalid arguments to textilizable'
462 end
462 end
463 return '' if text.blank?
463 return '' if text.blank?
464 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
464 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
465 only_path = options.delete(:only_path) == false ? false : true
465 only_path = options.delete(:only_path) == false ? false : true
466
466
467 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
467 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr) { |macro, args| exec_macro(macro, obj, args) }
468
468
469 @parsed_headings = []
469 @parsed_headings = []
470 text = parse_non_pre_blocks(text) do |text|
470 text = parse_non_pre_blocks(text) do |text|
471 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
471 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links, :parse_headings].each do |method_name|
472 send method_name, text, project, obj, attr, only_path, options
472 send method_name, text, project, obj, attr, only_path, options
473 end
473 end
474 end
474 end
475
475
476 if @parsed_headings.any?
476 if @parsed_headings.any?
477 replace_toc(text, @parsed_headings)
477 replace_toc(text, @parsed_headings)
478 end
478 end
479
479
480 text
480 text
481 end
481 end
482
482
483 def parse_non_pre_blocks(text)
483 def parse_non_pre_blocks(text)
484 s = StringScanner.new(text)
484 s = StringScanner.new(text)
485 tags = []
485 tags = []
486 parsed = ''
486 parsed = ''
487 while !s.eos?
487 while !s.eos?
488 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
488 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
489 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
489 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
490 if tags.empty?
490 if tags.empty?
491 yield text
491 yield text
492 end
492 end
493 parsed << text
493 parsed << text
494 if tag
494 if tag
495 if closing
495 if closing
496 if tags.last == tag.downcase
496 if tags.last == tag.downcase
497 tags.pop
497 tags.pop
498 end
498 end
499 else
499 else
500 tags << tag.downcase
500 tags << tag.downcase
501 end
501 end
502 parsed << full_tag
502 parsed << full_tag
503 end
503 end
504 end
504 end
505 # Close any non closing tags
505 # Close any non closing tags
506 while tag = tags.pop
506 while tag = tags.pop
507 parsed << "</#{tag}>"
507 parsed << "</#{tag}>"
508 end
508 end
509 parsed
509 parsed
510 end
510 end
511
511
512 def parse_inline_attachments(text, project, obj, attr, only_path, options)
512 def parse_inline_attachments(text, project, obj, attr, only_path, options)
513 # when using an image link, try to use an attachment, if possible
513 # when using an image link, try to use an attachment, if possible
514 if options[:attachments] || (obj && obj.respond_to?(:attachments))
514 if options[:attachments] || (obj && obj.respond_to?(:attachments))
515 attachments = nil
515 attachments = nil
516 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
516 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
517 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
517 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
518 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
518 attachments ||= (options[:attachments] || obj.attachments).sort_by(&:created_on).reverse
519 # search for the picture in attachments
519 # search for the picture in attachments
520 if found = attachments.detect { |att| att.filename.downcase == filename }
520 if found = attachments.detect { |att| att.filename.downcase == filename }
521 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
521 image_url = url_for :only_path => only_path, :controller => 'attachments', :action => 'download', :id => found
522 desc = found.description.to_s.gsub('"', '')
522 desc = found.description.to_s.gsub('"', '')
523 if !desc.blank? && alttext.blank?
523 if !desc.blank? && alttext.blank?
524 alt = " title=\"#{desc}\" alt=\"#{desc}\""
524 alt = " title=\"#{desc}\" alt=\"#{desc}\""
525 end
525 end
526 "src=\"#{image_url}\"#{alt}"
526 "src=\"#{image_url}\"#{alt}"
527 else
527 else
528 m
528 m
529 end
529 end
530 end
530 end
531 end
531 end
532 end
532 end
533
533
534 # Wiki links
534 # Wiki links
535 #
535 #
536 # Examples:
536 # Examples:
537 # [[mypage]]
537 # [[mypage]]
538 # [[mypage|mytext]]
538 # [[mypage|mytext]]
539 # wiki links can refer other project wikis, using project name or identifier:
539 # wiki links can refer other project wikis, using project name or identifier:
540 # [[project:]] -> wiki starting page
540 # [[project:]] -> wiki starting page
541 # [[project:|mytext]]
541 # [[project:|mytext]]
542 # [[project:mypage]]
542 # [[project:mypage]]
543 # [[project:mypage|mytext]]
543 # [[project:mypage|mytext]]
544 def parse_wiki_links(text, project, obj, attr, only_path, options)
544 def parse_wiki_links(text, project, obj, attr, only_path, options)
545 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
545 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
546 link_project = project
546 link_project = project
547 esc, all, page, title = $1, $2, $3, $5
547 esc, all, page, title = $1, $2, $3, $5
548 if esc.nil?
548 if esc.nil?
549 if page =~ /^([^\:]+)\:(.*)$/
549 if page =~ /^([^\:]+)\:(.*)$/
550 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
550 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
551 page = $2
551 page = $2
552 title ||= $1 if page.blank?
552 title ||= $1 if page.blank?
553 end
553 end
554
554
555 if link_project && link_project.wiki
555 if link_project && link_project.wiki
556 # extract anchor
556 # extract anchor
557 anchor = nil
557 anchor = nil
558 if page =~ /^(.+?)\#(.+)$/
558 if page =~ /^(.+?)\#(.+)$/
559 page, anchor = $1, $2
559 page, anchor = $1, $2
560 end
560 end
561 # check if page exists
561 # check if page exists
562 wiki_page = link_project.wiki.find_page(page)
562 wiki_page = link_project.wiki.find_page(page)
563 url = case options[:wiki_links]
563 url = case options[:wiki_links]
564 when :local; "#{title}.html"
564 when :local; "#{title}.html"
565 when :anchor; "##{title}" # used for single-file wiki export
565 when :anchor; "##{title}" # used for single-file wiki export
566 else
566 else
567 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
567 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
568 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
568 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project, :id => wiki_page_id, :anchor => anchor)
569 end
569 end
570 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
570 link_to((title || page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
571 else
571 else
572 # project or wiki doesn't exist
572 # project or wiki doesn't exist
573 all
573 all
574 end
574 end
575 else
575 else
576 all
576 all
577 end
577 end
578 end
578 end
579 end
579 end
580
580
581 # Redmine links
581 # Redmine links
582 #
582 #
583 # Examples:
583 # Examples:
584 # Issues:
584 # Issues:
585 # #52 -> Link to issue #52
585 # #52 -> Link to issue #52
586 # Changesets:
586 # Changesets:
587 # r52 -> Link to revision 52
587 # r52 -> Link to revision 52
588 # commit:a85130f -> Link to scmid starting with a85130f
588 # commit:a85130f -> Link to scmid starting with a85130f
589 # Documents:
589 # Documents:
590 # document#17 -> Link to document with id 17
590 # document#17 -> Link to document with id 17
591 # document:Greetings -> Link to the document with title "Greetings"
591 # document:Greetings -> Link to the document with title "Greetings"
592 # document:"Some document" -> Link to the document with title "Some document"
592 # document:"Some document" -> Link to the document with title "Some document"
593 # Versions:
593 # Versions:
594 # version#3 -> Link to version with id 3
594 # version#3 -> Link to version with id 3
595 # version:1.0.0 -> Link to version named "1.0.0"
595 # version:1.0.0 -> Link to version named "1.0.0"
596 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
596 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
597 # Attachments:
597 # Attachments:
598 # attachment:file.zip -> Link to the attachment of the current object named file.zip
598 # attachment:file.zip -> Link to the attachment of the current object named file.zip
599 # Source files:
599 # Source files:
600 # source:some/file -> Link to the file located at /some/file in the project's repository
600 # source:some/file -> Link to the file located at /some/file in the project's repository
601 # source:some/file@52 -> Link to the file's revision 52
601 # source:some/file@52 -> Link to the file's revision 52
602 # source:some/file#L120 -> Link to line 120 of the file
602 # source:some/file#L120 -> Link to line 120 of the file
603 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
603 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
604 # export:some/file -> Force the download of the file
604 # export:some/file -> Force the download of the file
605 # Forum messages:
605 # Forum messages:
606 # message#1218 -> Link to message with id 1218
606 # message#1218 -> Link to message with id 1218
607 #
607 #
608 # Links can refer other objects from other projects, using project identifier:
608 # Links can refer other objects from other projects, using project identifier:
609 # identifier:r52
609 # identifier:r52
610 # identifier:document:"Some document"
610 # identifier:document:"Some document"
611 # identifier:version:1.0.0
611 # identifier:version:1.0.0
612 # identifier:source:some/file
612 # identifier:source:some/file
613 def parse_redmine_links(text, project, obj, attr, only_path, options)
613 def parse_redmine_links(text, project, obj, attr, only_path, options)
614 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
614 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-]+):)?(attachment|document|version|commit|source|export|message|project)?((#|r)(\d+)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]]\W)|,|\s|\]|<|$)}) do |m|
615 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
615 leading, esc, project_prefix, project_identifier, prefix, sep, identifier = $1, $2, $3, $4, $5, $7 || $9, $8 || $10
616 link = nil
616 link = nil
617 if project_identifier
617 if project_identifier
618 project = Project.visible.find_by_identifier(project_identifier)
618 project = Project.visible.find_by_identifier(project_identifier)
619 end
619 end
620 if esc.nil?
620 if esc.nil?
621 if prefix.nil? && sep == 'r'
621 if prefix.nil? && sep == 'r'
622 # project.changesets.visible raises an SQL error because of a double join on repositories
622 # project.changesets.visible raises an SQL error because of a double join on repositories
623 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
623 if project && project.repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(project.repository.id, identifier))
624 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
624 link = link_to("#{project_prefix}r#{identifier}", {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.revision},
625 :class => 'changeset',
625 :class => 'changeset',
626 :title => truncate_single_line(changeset.comments, :length => 100))
626 :title => truncate_single_line(changeset.comments, :length => 100))
627 end
627 end
628 elsif sep == '#'
628 elsif sep == '#'
629 oid = identifier.to_i
629 oid = identifier.to_i
630 case prefix
630 case prefix
631 when nil
631 when nil
632 if issue = Issue.visible.find_by_id(oid, :include => :status)
632 if issue = Issue.visible.find_by_id(oid, :include => :status)
633 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
633 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid},
634 :class => issue.css_classes,
634 :class => issue.css_classes,
635 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
635 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
636 end
636 end
637 when 'document'
637 when 'document'
638 if document = Document.visible.find_by_id(oid)
638 if document = Document.visible.find_by_id(oid)
639 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
639 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
640 :class => 'document'
640 :class => 'document'
641 end
641 end
642 when 'version'
642 when 'version'
643 if version = Version.visible.find_by_id(oid)
643 if version = Version.visible.find_by_id(oid)
644 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
644 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
645 :class => 'version'
645 :class => 'version'
646 end
646 end
647 when 'message'
647 when 'message'
648 if message = Message.visible.find_by_id(oid, :include => :parent)
648 if message = Message.visible.find_by_id(oid, :include => :parent)
649 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
649 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
650 end
650 end
651 when 'project'
651 when 'project'
652 if p = Project.visible.find_by_id(oid)
652 if p = Project.visible.find_by_id(oid)
653 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
653 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
654 end
654 end
655 end
655 end
656 elsif sep == ':'
656 elsif sep == ':'
657 # removes the double quotes if any
657 # removes the double quotes if any
658 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
658 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
659 case prefix
659 case prefix
660 when 'document'
660 when 'document'
661 if project && document = project.documents.visible.find_by_title(name)
661 if project && document = project.documents.visible.find_by_title(name)
662 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
662 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
663 :class => 'document'
663 :class => 'document'
664 end
664 end
665 when 'version'
665 when 'version'
666 if project && version = project.versions.visible.find_by_name(name)
666 if project && version = project.versions.visible.find_by_name(name)
667 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
667 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
668 :class => 'version'
668 :class => 'version'
669 end
669 end
670 when 'commit'
670 when 'commit'
671 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
671 if project && project.repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", project.repository.id, "#{name}%"]))
672 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
672 link = link_to h("#{project_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :rev => changeset.identifier},
673 :class => 'changeset',
673 :class => 'changeset',
674 :title => truncate_single_line(changeset.comments, :length => 100)
674 :title => truncate_single_line(changeset.comments, :length => 100)
675 end
675 end
676 when 'source', 'export'
676 when 'source', 'export'
677 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
677 if project && project.repository && User.current.allowed_to?(:browse_repository, project)
678 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
678 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
679 path, rev, anchor = $1, $3, $5
679 path, rev, anchor = $1, $3, $5
680 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
680 link = link_to h("#{project_prefix}#{prefix}:#{name}"), {:controller => 'repositories', :action => 'entry', :id => project,
681 :path => to_path_param(path),
681 :path => to_path_param(path),
682 :rev => rev,
682 :rev => rev,
683 :anchor => anchor,
683 :anchor => anchor,
684 :format => (prefix == 'export' ? 'raw' : nil)},
684 :format => (prefix == 'export' ? 'raw' : nil)},
685 :class => (prefix == 'export' ? 'source download' : 'source')
685 :class => (prefix == 'export' ? 'source download' : 'source')
686 end
686 end
687 when 'attachment'
687 when 'attachment'
688 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
688 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
689 if attachments && attachment = attachments.detect {|a| a.filename == name }
689 if attachments && attachment = attachments.detect {|a| a.filename == name }
690 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
690 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
691 :class => 'attachment'
691 :class => 'attachment'
692 end
692 end
693 when 'project'
693 when 'project'
694 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
694 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
695 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
695 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
696 end
696 end
697 end
697 end
698 end
698 end
699 end
699 end
700 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
700 leading + (link || "#{project_prefix}#{prefix}#{sep}#{identifier}")
701 end
701 end
702 end
702 end
703
703
704 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
704 HEADING_RE = /<h(1|2|3|4)( [^>]+)?>(.+?)<\/h(1|2|3|4)>/i unless const_defined?(:HEADING_RE)
705
705
706 # Headings and TOC
706 # Headings and TOC
707 # Adds ids and links to headings unless options[:headings] is set to false
707 # Adds ids and links to headings unless options[:headings] is set to false
708 def parse_headings(text, project, obj, attr, only_path, options)
708 def parse_headings(text, project, obj, attr, only_path, options)
709 return if options[:headings] == false
709 return if options[:headings] == false
710
710
711 text.gsub!(HEADING_RE) do
711 text.gsub!(HEADING_RE) do
712 level, attrs, content = $1.to_i, $2, $3
712 level, attrs, content = $1.to_i, $2, $3
713 item = strip_tags(content).strip
713 item = strip_tags(content).strip
714 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
714 anchor = item.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
715 @parsed_headings << [level, anchor, item]
715 @parsed_headings << [level, anchor, item]
716 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
716 "<h#{level} #{attrs} id=\"#{anchor}\">#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
717 end
717 end
718 end
718 end
719
719
720 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
720 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
721
721
722 # Renders the TOC with given headings
722 # Renders the TOC with given headings
723 def replace_toc(text, headings)
723 def replace_toc(text, headings)
724 text.gsub!(TOC_RE) do
724 text.gsub!(TOC_RE) do
725 if headings.empty?
725 if headings.empty?
726 ''
726 ''
727 else
727 else
728 div_class = 'toc'
728 div_class = 'toc'
729 div_class << ' right' if $1 == '>'
729 div_class << ' right' if $1 == '>'
730 div_class << ' left' if $1 == '<'
730 div_class << ' left' if $1 == '<'
731 out = "<ul class=\"#{div_class}\"><li>"
731 out = "<ul class=\"#{div_class}\"><li>"
732 root = headings.map(&:first).min
732 root = headings.map(&:first).min
733 current = root
733 current = root
734 started = false
734 started = false
735 headings.each do |level, anchor, item|
735 headings.each do |level, anchor, item|
736 if level > current
736 if level > current
737 out << '<ul><li>' * (level - current)
737 out << '<ul><li>' * (level - current)
738 elsif level < current
738 elsif level < current
739 out << "</li></ul>\n" * (current - level) + "</li><li>"
739 out << "</li></ul>\n" * (current - level) + "</li><li>"
740 elsif started
740 elsif started
741 out << '</li><li>'
741 out << '</li><li>'
742 end
742 end
743 out << "<a href=\"##{anchor}\">#{item}</a>"
743 out << "<a href=\"##{anchor}\">#{item}</a>"
744 current = level
744 current = level
745 started = true
745 started = true
746 end
746 end
747 out << '</li></ul>' * (current - root)
747 out << '</li></ul>' * (current - root)
748 out << '</li></ul>'
748 out << '</li></ul>'
749 end
749 end
750 end
750 end
751 end
751 end
752
752
753 # Same as Rails' simple_format helper without using paragraphs
753 # Same as Rails' simple_format helper without using paragraphs
754 def simple_format_without_paragraph(text)
754 def simple_format_without_paragraph(text)
755 text.to_s.
755 text.to_s.
756 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
756 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
757 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
757 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
758 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
758 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />') # 1 newline -> br
759 end
759 end
760
760
761 def lang_options_for_select(blank=true)
761 def lang_options_for_select(blank=true)
762 (blank ? [["(auto)", ""]] : []) +
762 (blank ? [["(auto)", ""]] : []) +
763 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
763 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
764 end
764 end
765
765
766 def label_tag_for(name, option_tags = nil, options = {})
766 def label_tag_for(name, option_tags = nil, options = {})
767 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
767 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
768 content_tag("label", label_text)
768 content_tag("label", label_text)
769 end
769 end
770
770
771 def labelled_tabular_form_for(name, object, options, &proc)
771 def labelled_tabular_form_for(name, object, options, &proc)
772 options[:html] ||= {}
772 options[:html] ||= {}
773 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
773 options[:html][:class] = 'tabular' unless options[:html].has_key?(:class)
774 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
774 form_for(name, object, options.merge({ :builder => TabularFormBuilder, :lang => current_language}), &proc)
775 end
775 end
776
776
777 def back_url_hidden_field_tag
777 def back_url_hidden_field_tag
778 back_url = params[:back_url] || request.env['HTTP_REFERER']
778 back_url = params[:back_url] || request.env['HTTP_REFERER']
779 back_url = CGI.unescape(back_url.to_s)
779 back_url = CGI.unescape(back_url.to_s)
780 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
780 hidden_field_tag('back_url', CGI.escape(back_url)) unless back_url.blank?
781 end
781 end
782
782
783 def check_all_links(form_name)
783 def check_all_links(form_name)
784 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
784 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
785 " | " +
785 " | " +
786 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
786 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
787 end
787 end
788
788
789 def progress_bar(pcts, options={})
789 def progress_bar(pcts, options={})
790 pcts = [pcts, pcts] unless pcts.is_a?(Array)
790 pcts = [pcts, pcts] unless pcts.is_a?(Array)
791 pcts = pcts.collect(&:round)
791 pcts = pcts.collect(&:round)
792 pcts[1] = pcts[1] - pcts[0]
792 pcts[1] = pcts[1] - pcts[0]
793 pcts << (100 - pcts[1] - pcts[0])
793 pcts << (100 - pcts[1] - pcts[0])
794 width = options[:width] || '100px;'
794 width = options[:width] || '100px;'
795 legend = options[:legend] || ''
795 legend = options[:legend] || ''
796 content_tag('table',
796 content_tag('table',
797 content_tag('tr',
797 content_tag('tr',
798 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
798 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : '') +
799 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
799 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : '') +
800 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
800 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : '')
801 ), :class => 'progress', :style => "width: #{width};") +
801 ), :class => 'progress', :style => "width: #{width};") +
802 content_tag('p', legend, :class => 'pourcent')
802 content_tag('p', legend, :class => 'pourcent')
803 end
803 end
804
804
805 def checked_image(checked=true)
805 def checked_image(checked=true)
806 if checked
806 if checked
807 image_tag 'toggle_check.png'
807 image_tag 'toggle_check.png'
808 end
808 end
809 end
809 end
810
810
811 def context_menu(url)
811 def context_menu(url)
812 unless @context_menu_included
812 unless @context_menu_included
813 content_for :header_tags do
813 content_for :header_tags do
814 javascript_include_tag('context_menu') +
814 javascript_include_tag('context_menu') +
815 stylesheet_link_tag('context_menu')
815 stylesheet_link_tag('context_menu')
816 end
816 end
817 if l(:direction) == 'rtl'
817 if l(:direction) == 'rtl'
818 content_for :header_tags do
818 content_for :header_tags do
819 stylesheet_link_tag('context_menu_rtl')
819 stylesheet_link_tag('context_menu_rtl')
820 end
820 end
821 end
821 end
822 @context_menu_included = true
822 @context_menu_included = true
823 end
823 end
824 javascript_tag "new ContextMenu('#{ url_for(url) }')"
824 javascript_tag "new ContextMenu('#{ url_for(url) }')"
825 end
825 end
826
826
827 def context_menu_link(name, url, options={})
827 def context_menu_link(name, url, options={})
828 options[:class] ||= ''
828 options[:class] ||= ''
829 if options.delete(:selected)
829 if options.delete(:selected)
830 options[:class] << ' icon-checked disabled'
830 options[:class] << ' icon-checked disabled'
831 options[:disabled] = true
831 options[:disabled] = true
832 end
832 end
833 if options.delete(:disabled)
833 if options.delete(:disabled)
834 options.delete(:method)
834 options.delete(:method)
835 options.delete(:confirm)
835 options.delete(:confirm)
836 options.delete(:onclick)
836 options.delete(:onclick)
837 options[:class] << ' disabled'
837 options[:class] << ' disabled'
838 url = '#'
838 url = '#'
839 end
839 end
840 link_to name, url, options
840 link_to name, url, options
841 end
841 end
842
842
843 def calendar_for(field_id)
843 def calendar_for(field_id)
844 include_calendar_headers_tags
844 include_calendar_headers_tags
845 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
845 image_tag("calendar.png", {:id => "#{field_id}_trigger",:class => "calendar-trigger"}) +
846 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
846 javascript_tag("Calendar.setup({inputField : '#{field_id}', ifFormat : '%Y-%m-%d', button : '#{field_id}_trigger' });")
847 end
847 end
848
848
849 def include_calendar_headers_tags
849 def include_calendar_headers_tags
850 unless @calendar_headers_tags_included
850 unless @calendar_headers_tags_included
851 @calendar_headers_tags_included = true
851 @calendar_headers_tags_included = true
852 content_for :header_tags do
852 content_for :header_tags do
853 start_of_week = case Setting.start_of_week.to_i
853 start_of_week = case Setting.start_of_week.to_i
854 when 1
854 when 1
855 'Calendar._FD = 1;' # Monday
855 'Calendar._FD = 1;' # Monday
856 when 7
856 when 7
857 'Calendar._FD = 0;' # Sunday
857 'Calendar._FD = 0;' # Sunday
858 else
858 else
859 '' # use language
859 '' # use language
860 end
860 end
861
861
862 javascript_include_tag('calendar/calendar') +
862 javascript_include_tag('calendar/calendar') +
863 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
863 javascript_include_tag("calendar/lang/calendar-#{current_language.to_s.downcase}.js") +
864 javascript_tag(start_of_week) +
864 javascript_tag(start_of_week) +
865 javascript_include_tag('calendar/calendar-setup') +
865 javascript_include_tag('calendar/calendar-setup') +
866 stylesheet_link_tag('calendar')
866 stylesheet_link_tag('calendar')
867 end
867 end
868 end
868 end
869 end
869 end
870
870
871 def content_for(name, content = nil, &block)
871 def content_for(name, content = nil, &block)
872 @has_content ||= {}
872 @has_content ||= {}
873 @has_content[name] = true
873 @has_content[name] = true
874 super(name, content, &block)
874 super(name, content, &block)
875 end
875 end
876
876
877 def has_content?(name)
877 def has_content?(name)
878 (@has_content && @has_content[name]) || false
878 (@has_content && @has_content[name]) || false
879 end
879 end
880
880
881 # Returns the avatar image tag for the given +user+ if avatars are enabled
881 # Returns the avatar image tag for the given +user+ if avatars are enabled
882 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
882 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
883 def avatar(user, options = { })
883 def avatar(user, options = { })
884 if Setting.gravatar_enabled?
884 if Setting.gravatar_enabled?
885 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
885 options.merge!({:ssl => (defined?(request) && request.ssl?), :default => Setting.gravatar_default})
886 email = nil
886 email = nil
887 if user.respond_to?(:mail)
887 if user.respond_to?(:mail)
888 email = user.mail
888 email = user.mail
889 elsif user.to_s =~ %r{<(.+?)>}
889 elsif user.to_s =~ %r{<(.+?)>}
890 email = $1
890 email = $1
891 end
891 end
892 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
892 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
893 else
893 else
894 ''
894 ''
895 end
895 end
896 end
896 end
897
897
898 # Returns the javascript tags that are included in the html layout head
898 # Returns the javascript tags that are included in the html layout head
899 def javascript_heads
899 def javascript_heads
900 tags = javascript_include_tag(:defaults)
900 tags = javascript_include_tag(:defaults)
901 unless User.current.pref.warn_on_leaving_unsaved == '0'
901 unless User.current.pref.warn_on_leaving_unsaved == '0'
902 tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
902 tags << "\n" + javascript_tag("Event.observe(window, 'load', function(){ new WarnLeavingUnsaved('#{escape_javascript( l(:text_warn_on_leaving_unsaved) )}'); });")
903 end
903 end
904 tags
904 tags
905 end
905 end
906
906
907 def favicon
907 def favicon
908 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
908 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />"
909 end
909 end
910
910
911 # Returns true if arg is expected in the API response
911 # Returns true if arg is expected in the API response
912 def include_in_api_response?(arg)
912 def include_in_api_response?(arg)
913 unless @included_in_api_response
913 unless @included_in_api_response
914 param = params[:include]
914 param = params[:include]
915 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
915 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
916 @included_in_api_response.collect!(&:strip)
916 @included_in_api_response.collect!(&:strip)
917 end
917 end
918 @included_in_api_response.include?(arg.to_s)
918 @included_in_api_response.include?(arg.to_s)
919 end
919 end
920
920
921 # Returns options or nil if nometa param or X-Redmine-Nometa header
921 # Returns options or nil if nometa param or X-Redmine-Nometa header
922 # was set in the request
922 # was set in the request
923 def api_meta(options)
923 def api_meta(options)
924 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
924 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
925 # compatibility mode for activeresource clients that raise
925 # compatibility mode for activeresource clients that raise
926 # an error when unserializing an array with attributes
926 # an error when unserializing an array with attributes
927 nil
927 nil
928 else
928 else
929 options
929 options
930 end
930 end
931 end
931 end
932
932
933 private
933 private
934
934
935 def wiki_helper
935 def wiki_helper
936 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
936 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
937 extend helper
937 extend helper
938 return self
938 return self
939 end
939 end
940
940
941 def link_to_remote_content_update(text, url_params)
941 def link_to_remote_content_update(text, url_params)
942 link_to_remote(text,
942 link_to_remote(text,
943 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
943 {:url => url_params, :method => :get, :update => 'content', :complete => 'window.scrollTo(0,0)'},
944 {:href => url_for(:params => url_params)}
944 {:href => url_for(:params => url_params)}
945 )
945 )
946 end
946 end
947
947
948 end
948 end
@@ -1,198 +1,216
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 'diff'
18 require 'diff'
19 require 'enumerator'
19 require 'enumerator'
20
20
21 class WikiPage < ActiveRecord::Base
21 class WikiPage < ActiveRecord::Base
22 belongs_to :wiki
22 belongs_to :wiki
23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
23 has_one :content, :class_name => 'WikiContent', :foreign_key => 'page_id', :dependent => :destroy
24 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
24 acts_as_attachable :delete_permission => :delete_wiki_pages_attachments
25 acts_as_tree :dependent => :nullify, :order => 'title'
25 acts_as_tree :dependent => :nullify, :order => 'title'
26
26
27 acts_as_watchable
27 acts_as_watchable
28 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
28 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
29 :description => :text,
29 :description => :text,
30 :datetime => :created_on,
30 :datetime => :created_on,
31 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
31 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.wiki.project, :id => o.title}}
32
32
33 acts_as_searchable :columns => ['title', 'text'],
33 acts_as_searchable :columns => ['title', 'text'],
34 :include => [{:wiki => :project}, :content],
34 :include => [{:wiki => :project}, :content],
35 :project_key => "#{Wiki.table_name}.project_id"
35 :project_key => "#{Wiki.table_name}.project_id"
36
36
37 attr_accessor :redirect_existing_links
37 attr_accessor :redirect_existing_links
38
38
39 validates_presence_of :title
39 validates_presence_of :title
40 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
40 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
41 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
41 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
42 validates_associated :content
42 validates_associated :content
43
43
44 # eager load information about last updates, without loading text
45 named_scope :with_updated_on, {
46 :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
47 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id"
48 }
49
44 # Wiki pages that are protected by default
50 # Wiki pages that are protected by default
45 DEFAULT_PROTECTED_PAGES = %w(sidebar)
51 DEFAULT_PROTECTED_PAGES = %w(sidebar)
46
52
47 def after_initialize
53 def after_initialize
48 if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
54 if new_record? && DEFAULT_PROTECTED_PAGES.include?(title.to_s.downcase)
49 self.protected = true
55 self.protected = true
50 end
56 end
51 end
57 end
52
58
53 def visible?(user=User.current)
59 def visible?(user=User.current)
54 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
60 !user.nil? && user.allowed_to?(:view_wiki_pages, project)
55 end
61 end
56
62
57 def title=(value)
63 def title=(value)
58 value = Wiki.titleize(value)
64 value = Wiki.titleize(value)
59 @previous_title = read_attribute(:title) if @previous_title.blank?
65 @previous_title = read_attribute(:title) if @previous_title.blank?
60 write_attribute(:title, value)
66 write_attribute(:title, value)
61 end
67 end
62
68
63 def before_save
69 def before_save
64 self.title = Wiki.titleize(title)
70 self.title = Wiki.titleize(title)
65 # Manage redirects if the title has changed
71 # Manage redirects if the title has changed
66 if !@previous_title.blank? && (@previous_title != title) && !new_record?
72 if !@previous_title.blank? && (@previous_title != title) && !new_record?
67 # Update redirects that point to the old title
73 # Update redirects that point to the old title
68 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
74 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
69 r.redirects_to = title
75 r.redirects_to = title
70 r.title == r.redirects_to ? r.destroy : r.save
76 r.title == r.redirects_to ? r.destroy : r.save
71 end
77 end
72 # Remove redirects for the new title
78 # Remove redirects for the new title
73 wiki.redirects.find_all_by_title(title).each(&:destroy)
79 wiki.redirects.find_all_by_title(title).each(&:destroy)
74 # Create a redirect to the new title
80 # Create a redirect to the new title
75 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
81 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
76 @previous_title = nil
82 @previous_title = nil
77 end
83 end
78 end
84 end
79
85
80 def before_destroy
86 def before_destroy
81 # Remove redirects to this page
87 # Remove redirects to this page
82 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
88 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
83 end
89 end
84
90
85 def pretty_title
91 def pretty_title
86 WikiPage.pretty_title(title)
92 WikiPage.pretty_title(title)
87 end
93 end
88
94
89 def content_for_version(version=nil)
95 def content_for_version(version=nil)
90 result = content.versions.find_by_version(version.to_i) if version
96 result = content.versions.find_by_version(version.to_i) if version
91 result ||= content
97 result ||= content
92 result
98 result
93 end
99 end
94
100
95 def diff(version_to=nil, version_from=nil)
101 def diff(version_to=nil, version_from=nil)
96 version_to = version_to ? version_to.to_i : self.content.version
102 version_to = version_to ? version_to.to_i : self.content.version
97 version_from = version_from ? version_from.to_i : version_to - 1
103 version_from = version_from ? version_from.to_i : version_to - 1
98 version_to, version_from = version_from, version_to unless version_from < version_to
104 version_to, version_from = version_from, version_to unless version_from < version_to
99
105
100 content_to = content.versions.find_by_version(version_to)
106 content_to = content.versions.find_by_version(version_to)
101 content_from = content.versions.find_by_version(version_from)
107 content_from = content.versions.find_by_version(version_from)
102
108
103 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
109 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
104 end
110 end
105
111
106 def annotate(version=nil)
112 def annotate(version=nil)
107 version = version ? version.to_i : self.content.version
113 version = version ? version.to_i : self.content.version
108 c = content.versions.find_by_version(version)
114 c = content.versions.find_by_version(version)
109 c ? WikiAnnotate.new(c) : nil
115 c ? WikiAnnotate.new(c) : nil
110 end
116 end
111
117
112 def self.pretty_title(str)
118 def self.pretty_title(str)
113 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
119 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
114 end
120 end
115
121
116 def project
122 def project
117 wiki.project
123 wiki.project
118 end
124 end
119
125
120 def text
126 def text
121 content.text if content
127 content.text if content
122 end
128 end
123
129
130 def updated_on
131 unless @updated_on
132 if time = read_attribute(:updated_on)
133 # content updated_on was eager loaded with the page
134 @updated_on = Time.parse(time) rescue nil
135 else
136 @updated_on = content && content.updated_on
137 end
138 end
139 @updated_on
140 end
141
124 # Returns true if usr is allowed to edit the page, otherwise false
142 # Returns true if usr is allowed to edit the page, otherwise false
125 def editable_by?(usr)
143 def editable_by?(usr)
126 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
144 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
127 end
145 end
128
146
129 def attachments_deletable?(usr=User.current)
147 def attachments_deletable?(usr=User.current)
130 editable_by?(usr) && super(usr)
148 editable_by?(usr) && super(usr)
131 end
149 end
132
150
133 def parent_title
151 def parent_title
134 @parent_title || (self.parent && self.parent.pretty_title)
152 @parent_title || (self.parent && self.parent.pretty_title)
135 end
153 end
136
154
137 def parent_title=(t)
155 def parent_title=(t)
138 @parent_title = t
156 @parent_title = t
139 parent_page = t.blank? ? nil : self.wiki.find_page(t)
157 parent_page = t.blank? ? nil : self.wiki.find_page(t)
140 self.parent = parent_page
158 self.parent = parent_page
141 end
159 end
142
160
143 protected
161 protected
144
162
145 def validate
163 def validate
146 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
164 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
147 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
165 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
148 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
166 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
149 end
167 end
150 end
168 end
151
169
152 class WikiDiff < Redmine::Helpers::Diff
170 class WikiDiff < Redmine::Helpers::Diff
153 attr_reader :content_to, :content_from
171 attr_reader :content_to, :content_from
154
172
155 def initialize(content_to, content_from)
173 def initialize(content_to, content_from)
156 @content_to = content_to
174 @content_to = content_to
157 @content_from = content_from
175 @content_from = content_from
158 super(content_to.text, content_from.text)
176 super(content_to.text, content_from.text)
159 end
177 end
160 end
178 end
161
179
162 class WikiAnnotate
180 class WikiAnnotate
163 attr_reader :lines, :content
181 attr_reader :lines, :content
164
182
165 def initialize(content)
183 def initialize(content)
166 @content = content
184 @content = content
167 current = content
185 current = content
168 current_lines = current.text.split(/\r?\n/)
186 current_lines = current.text.split(/\r?\n/)
169 @lines = current_lines.collect {|t| [nil, nil, t]}
187 @lines = current_lines.collect {|t| [nil, nil, t]}
170 positions = []
188 positions = []
171 current_lines.size.times {|i| positions << i}
189 current_lines.size.times {|i| positions << i}
172 while (current.previous)
190 while (current.previous)
173 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
191 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
174 d.each_slice(3) do |s|
192 d.each_slice(3) do |s|
175 sign, line = s[0], s[1]
193 sign, line = s[0], s[1]
176 if sign == '+' && positions[line] && positions[line] != -1
194 if sign == '+' && positions[line] && positions[line] != -1
177 if @lines[positions[line]][0].nil?
195 if @lines[positions[line]][0].nil?
178 @lines[positions[line]][0] = current.version
196 @lines[positions[line]][0] = current.version
179 @lines[positions[line]][1] = current.author
197 @lines[positions[line]][1] = current.author
180 end
198 end
181 end
199 end
182 end
200 end
183 d.each_slice(3) do |s|
201 d.each_slice(3) do |s|
184 sign, line = s[0], s[1]
202 sign, line = s[0], s[1]
185 if sign == '-'
203 if sign == '-'
186 positions.insert(line, -1)
204 positions.insert(line, -1)
187 else
205 else
188 positions[line] = nil
206 positions[line] = nil
189 end
207 end
190 end
208 end
191 positions.compact!
209 positions.compact!
192 # Stop if every line is annotated
210 # Stop if every line is annotated
193 break unless @lines.detect { |line| line[0].nil? }
211 break unless @lines.detect { |line| line[0].nil? }
194 current = current.previous
212 current = current.previous
195 end
213 end
196 @lines.each { |line| line[0] ||= current.version }
214 @lines.each { |line| line[0] ||= current.version }
197 end
215 end
198 end
216 end
@@ -1,459 +1,460
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19 require 'wiki_controller'
19 require 'wiki_controller'
20
20
21 # Re-raise errors caught by the controller.
21 # Re-raise errors caught by the controller.
22 class WikiController; def rescue_action(e) raise e end; end
22 class WikiController; def rescue_action(e) raise e end; end
23
23
24 class WikiControllerTest < ActionController::TestCase
24 class WikiControllerTest < ActionController::TestCase
25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
25 fixtures :projects, :users, :roles, :members, :member_roles, :enabled_modules, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions, :attachments
26
26
27 def setup
27 def setup
28 @controller = WikiController.new
28 @controller = WikiController.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_start_page
34 def test_show_start_page
35 get :show, :project_id => 'ecookbook'
35 get :show, :project_id => 'ecookbook'
36 assert_response :success
36 assert_response :success
37 assert_template 'show'
37 assert_template 'show'
38 assert_tag :tag => 'h1', :content => /CookBook documentation/
38 assert_tag :tag => 'h1', :content => /CookBook documentation/
39
39
40 # child_pages macro
40 # child_pages macro
41 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
41 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
42 :child => { :tag => 'li',
42 :child => { :tag => 'li',
43 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
43 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
44 :content => 'Page with an inline image' } }
44 :content => 'Page with an inline image' } }
45 end
45 end
46
46
47 def test_show_page_with_name
47 def test_show_page_with_name
48 get :show, :project_id => 1, :id => 'Another_page'
48 get :show, :project_id => 1, :id => 'Another_page'
49 assert_response :success
49 assert_response :success
50 assert_template 'show'
50 assert_template 'show'
51 assert_tag :tag => 'h1', :content => /Another page/
51 assert_tag :tag => 'h1', :content => /Another page/
52 # Included page with an inline image
52 # Included page with an inline image
53 assert_tag :tag => 'p', :content => /This is an inline image/
53 assert_tag :tag => 'p', :content => /This is an inline image/
54 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
54 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
55 :alt => 'This is a logo' }
55 :alt => 'This is a logo' }
56 end
56 end
57
57
58 def test_show_with_sidebar
58 def test_show_with_sidebar
59 page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
59 page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
60 page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
60 page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
61 page.save!
61 page.save!
62
62
63 get :show, :project_id => 1, :id => 'Another_page'
63 get :show, :project_id => 1, :id => 'Another_page'
64 assert_response :success
64 assert_response :success
65 assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
65 assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
66 :content => /Side bar content for test_show_with_sidebar/
66 :content => /Side bar content for test_show_with_sidebar/
67 end
67 end
68
68
69 def test_show_unexistent_page_without_edit_right
69 def test_show_unexistent_page_without_edit_right
70 get :show, :project_id => 1, :id => 'Unexistent page'
70 get :show, :project_id => 1, :id => 'Unexistent page'
71 assert_response 404
71 assert_response 404
72 end
72 end
73
73
74 def test_show_unexistent_page_with_edit_right
74 def test_show_unexistent_page_with_edit_right
75 @request.session[:user_id] = 2
75 @request.session[:user_id] = 2
76 get :show, :project_id => 1, :id => 'Unexistent page'
76 get :show, :project_id => 1, :id => 'Unexistent page'
77 assert_response :success
77 assert_response :success
78 assert_template 'edit'
78 assert_template 'edit'
79 end
79 end
80
80
81 def test_create_page
81 def test_create_page
82 @request.session[:user_id] = 2
82 @request.session[:user_id] = 2
83 put :update, :project_id => 1,
83 put :update, :project_id => 1,
84 :id => 'New page',
84 :id => 'New page',
85 :content => {:comments => 'Created the page',
85 :content => {:comments => 'Created the page',
86 :text => "h1. New page\n\nThis is a new page",
86 :text => "h1. New page\n\nThis is a new page",
87 :version => 0}
87 :version => 0}
88 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
88 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
89 page = Project.find(1).wiki.find_page('New page')
89 page = Project.find(1).wiki.find_page('New page')
90 assert !page.new_record?
90 assert !page.new_record?
91 assert_not_nil page.content
91 assert_not_nil page.content
92 assert_equal 'Created the page', page.content.comments
92 assert_equal 'Created the page', page.content.comments
93 end
93 end
94
94
95 def test_create_page_with_attachments
95 def test_create_page_with_attachments
96 @request.session[:user_id] = 2
96 @request.session[:user_id] = 2
97 assert_difference 'WikiPage.count' do
97 assert_difference 'WikiPage.count' do
98 assert_difference 'Attachment.count' do
98 assert_difference 'Attachment.count' do
99 put :update, :project_id => 1,
99 put :update, :project_id => 1,
100 :id => 'New page',
100 :id => 'New page',
101 :content => {:comments => 'Created the page',
101 :content => {:comments => 'Created the page',
102 :text => "h1. New page\n\nThis is a new page",
102 :text => "h1. New page\n\nThis is a new page",
103 :version => 0},
103 :version => 0},
104 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
104 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
105 end
105 end
106 end
106 end
107 page = Project.find(1).wiki.find_page('New page')
107 page = Project.find(1).wiki.find_page('New page')
108 assert_equal 1, page.attachments.count
108 assert_equal 1, page.attachments.count
109 assert_equal 'testfile.txt', page.attachments.first.filename
109 assert_equal 'testfile.txt', page.attachments.first.filename
110 end
110 end
111
111
112 def test_update_page
112 def test_update_page
113 @request.session[:user_id] = 2
113 @request.session[:user_id] = 2
114 assert_no_difference 'WikiPage.count' do
114 assert_no_difference 'WikiPage.count' do
115 assert_no_difference 'WikiContent.count' do
115 assert_no_difference 'WikiContent.count' do
116 assert_difference 'WikiContent::Version.count' do
116 assert_difference 'WikiContent::Version.count' do
117 put :update, :project_id => 1,
117 put :update, :project_id => 1,
118 :id => 'Another_page',
118 :id => 'Another_page',
119 :content => {
119 :content => {
120 :comments => "my comments",
120 :comments => "my comments",
121 :text => "edited",
121 :text => "edited",
122 :version => 1
122 :version => 1
123 }
123 }
124 end
124 end
125 end
125 end
126 end
126 end
127 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
127 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
128
128
129 page = Wiki.find(1).pages.find_by_title('Another_page')
129 page = Wiki.find(1).pages.find_by_title('Another_page')
130 assert_equal "edited", page.content.text
130 assert_equal "edited", page.content.text
131 assert_equal 2, page.content.version
131 assert_equal 2, page.content.version
132 assert_equal "my comments", page.content.comments
132 assert_equal "my comments", page.content.comments
133 end
133 end
134
134
135 def test_update_page_with_failure
135 def test_update_page_with_failure
136 @request.session[:user_id] = 2
136 @request.session[:user_id] = 2
137 assert_no_difference 'WikiPage.count' do
137 assert_no_difference 'WikiPage.count' do
138 assert_no_difference 'WikiContent.count' do
138 assert_no_difference 'WikiContent.count' do
139 assert_no_difference 'WikiContent::Version.count' do
139 assert_no_difference 'WikiContent::Version.count' do
140 put :update, :project_id => 1,
140 put :update, :project_id => 1,
141 :id => 'Another_page',
141 :id => 'Another_page',
142 :content => {
142 :content => {
143 :comments => 'a' * 300, # failure here, comment is too long
143 :comments => 'a' * 300, # failure here, comment is too long
144 :text => 'edited',
144 :text => 'edited',
145 :version => 1
145 :version => 1
146 }
146 }
147 end
147 end
148 end
148 end
149 end
149 end
150 assert_response :success
150 assert_response :success
151 assert_template 'edit'
151 assert_template 'edit'
152
152
153 assert_error_tag :descendant => {:content => /Comment is too long/}
153 assert_error_tag :descendant => {:content => /Comment is too long/}
154 assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => 'edited'
154 assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => 'edited'
155 assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'}
155 assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'}
156 end
156 end
157
157
158 def test_preview
158 def test_preview
159 @request.session[:user_id] = 2
159 @request.session[:user_id] = 2
160 xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
160 xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
161 :content => { :comments => '',
161 :content => { :comments => '',
162 :text => 'this is a *previewed text*',
162 :text => 'this is a *previewed text*',
163 :version => 3 }
163 :version => 3 }
164 assert_response :success
164 assert_response :success
165 assert_template 'common/_preview'
165 assert_template 'common/_preview'
166 assert_tag :tag => 'strong', :content => /previewed text/
166 assert_tag :tag => 'strong', :content => /previewed text/
167 end
167 end
168
168
169 def test_preview_new_page
169 def test_preview_new_page
170 @request.session[:user_id] = 2
170 @request.session[:user_id] = 2
171 xhr :post, :preview, :project_id => 1, :id => 'New page',
171 xhr :post, :preview, :project_id => 1, :id => 'New page',
172 :content => { :text => 'h1. New page',
172 :content => { :text => 'h1. New page',
173 :comments => '',
173 :comments => '',
174 :version => 0 }
174 :version => 0 }
175 assert_response :success
175 assert_response :success
176 assert_template 'common/_preview'
176 assert_template 'common/_preview'
177 assert_tag :tag => 'h1', :content => /New page/
177 assert_tag :tag => 'h1', :content => /New page/
178 end
178 end
179
179
180 def test_history
180 def test_history
181 get :history, :project_id => 1, :id => 'CookBook_documentation'
181 get :history, :project_id => 1, :id => 'CookBook_documentation'
182 assert_response :success
182 assert_response :success
183 assert_template 'history'
183 assert_template 'history'
184 assert_not_nil assigns(:versions)
184 assert_not_nil assigns(:versions)
185 assert_equal 3, assigns(:versions).size
185 assert_equal 3, assigns(:versions).size
186 assert_select "input[type=submit][name=commit]"
186 assert_select "input[type=submit][name=commit]"
187 end
187 end
188
188
189 def test_history_with_one_version
189 def test_history_with_one_version
190 get :history, :project_id => 1, :id => 'Another_page'
190 get :history, :project_id => 1, :id => 'Another_page'
191 assert_response :success
191 assert_response :success
192 assert_template 'history'
192 assert_template 'history'
193 assert_not_nil assigns(:versions)
193 assert_not_nil assigns(:versions)
194 assert_equal 1, assigns(:versions).size
194 assert_equal 1, assigns(:versions).size
195 assert_select "input[type=submit][name=commit]", false
195 assert_select "input[type=submit][name=commit]", false
196 end
196 end
197
197
198 def test_diff
198 def test_diff
199 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => 2, :version_from => 1
199 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => 2, :version_from => 1
200 assert_response :success
200 assert_response :success
201 assert_template 'diff'
201 assert_template 'diff'
202 assert_tag :tag => 'span', :attributes => { :class => 'diff_in'},
202 assert_tag :tag => 'span', :attributes => { :class => 'diff_in'},
203 :content => /updated/
203 :content => /updated/
204 end
204 end
205
205
206 def test_annotate
206 def test_annotate
207 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2
207 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2
208 assert_response :success
208 assert_response :success
209 assert_template 'annotate'
209 assert_template 'annotate'
210 # Line 1
210 # Line 1
211 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1' },
211 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1' },
212 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/ },
212 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/ },
213 :child => { :tag => 'td', :content => /h1\. CookBook documentation/ }
213 :child => { :tag => 'td', :content => /h1\. CookBook documentation/ }
214 # Line 2
214 # Line 2
215 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '2' },
215 assert_tag :tag => 'tr', :child => { :tag => 'th', :attributes => {:class => 'line-num'}, :content => '2' },
216 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/ },
216 :child => { :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/ },
217 :child => { :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ }
217 :child => { :tag => 'td', :content => /Some updated \[\[documentation\]\] here/ }
218 end
218 end
219
219
220 def test_get_rename
220 def test_get_rename
221 @request.session[:user_id] = 2
221 @request.session[:user_id] = 2
222 get :rename, :project_id => 1, :id => 'Another_page'
222 get :rename, :project_id => 1, :id => 'Another_page'
223 assert_response :success
223 assert_response :success
224 assert_template 'rename'
224 assert_template 'rename'
225 assert_tag 'option',
225 assert_tag 'option',
226 :attributes => {:value => ''},
226 :attributes => {:value => ''},
227 :content => '',
227 :content => '',
228 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
228 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
229 assert_no_tag 'option',
229 assert_no_tag 'option',
230 :attributes => {:selected => 'selected'},
230 :attributes => {:selected => 'selected'},
231 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
231 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
232 end
232 end
233
233
234 def test_get_rename_child_page
234 def test_get_rename_child_page
235 @request.session[:user_id] = 2
235 @request.session[:user_id] = 2
236 get :rename, :project_id => 1, :id => 'Child_1'
236 get :rename, :project_id => 1, :id => 'Child_1'
237 assert_response :success
237 assert_response :success
238 assert_template 'rename'
238 assert_template 'rename'
239 assert_tag 'option',
239 assert_tag 'option',
240 :attributes => {:value => ''},
240 :attributes => {:value => ''},
241 :content => '',
241 :content => '',
242 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
242 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
243 assert_tag 'option',
243 assert_tag 'option',
244 :attributes => {:value => '2', :selected => 'selected'},
244 :attributes => {:value => '2', :selected => 'selected'},
245 :content => /Another page/,
245 :content => /Another page/,
246 :parent => {
246 :parent => {
247 :tag => 'select',
247 :tag => 'select',
248 :attributes => {:name => 'wiki_page[parent_id]'}
248 :attributes => {:name => 'wiki_page[parent_id]'}
249 }
249 }
250 end
250 end
251
251
252 def test_rename_with_redirect
252 def test_rename_with_redirect
253 @request.session[:user_id] = 2
253 @request.session[:user_id] = 2
254 post :rename, :project_id => 1, :id => 'Another_page',
254 post :rename, :project_id => 1, :id => 'Another_page',
255 :wiki_page => { :title => 'Another renamed page',
255 :wiki_page => { :title => 'Another renamed page',
256 :redirect_existing_links => 1 }
256 :redirect_existing_links => 1 }
257 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
257 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
258 wiki = Project.find(1).wiki
258 wiki = Project.find(1).wiki
259 # Check redirects
259 # Check redirects
260 assert_not_nil wiki.find_page('Another page')
260 assert_not_nil wiki.find_page('Another page')
261 assert_nil wiki.find_page('Another page', :with_redirect => false)
261 assert_nil wiki.find_page('Another page', :with_redirect => false)
262 end
262 end
263
263
264 def test_rename_without_redirect
264 def test_rename_without_redirect
265 @request.session[:user_id] = 2
265 @request.session[:user_id] = 2
266 post :rename, :project_id => 1, :id => 'Another_page',
266 post :rename, :project_id => 1, :id => 'Another_page',
267 :wiki_page => { :title => 'Another renamed page',
267 :wiki_page => { :title => 'Another renamed page',
268 :redirect_existing_links => "0" }
268 :redirect_existing_links => "0" }
269 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
269 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
270 wiki = Project.find(1).wiki
270 wiki = Project.find(1).wiki
271 # Check that there's no redirects
271 # Check that there's no redirects
272 assert_nil wiki.find_page('Another page')
272 assert_nil wiki.find_page('Another page')
273 end
273 end
274
274
275 def test_rename_with_parent_assignment
275 def test_rename_with_parent_assignment
276 @request.session[:user_id] = 2
276 @request.session[:user_id] = 2
277 post :rename, :project_id => 1, :id => 'Another_page',
277 post :rename, :project_id => 1, :id => 'Another_page',
278 :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' }
278 :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' }
279 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
279 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
280 assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent
280 assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent
281 end
281 end
282
282
283 def test_rename_with_parent_unassignment
283 def test_rename_with_parent_unassignment
284 @request.session[:user_id] = 2
284 @request.session[:user_id] = 2
285 post :rename, :project_id => 1, :id => 'Child_1',
285 post :rename, :project_id => 1, :id => 'Child_1',
286 :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' }
286 :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' }
287 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1'
287 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1'
288 assert_nil WikiPage.find_by_title('Child_1').parent
288 assert_nil WikiPage.find_by_title('Child_1').parent
289 end
289 end
290
290
291 def test_destroy_child
291 def test_destroy_child
292 @request.session[:user_id] = 2
292 @request.session[:user_id] = 2
293 delete :destroy, :project_id => 1, :id => 'Child_1'
293 delete :destroy, :project_id => 1, :id => 'Child_1'
294 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
294 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
295 end
295 end
296
296
297 def test_destroy_parent
297 def test_destroy_parent
298 @request.session[:user_id] = 2
298 @request.session[:user_id] = 2
299 assert_no_difference('WikiPage.count') do
299 assert_no_difference('WikiPage.count') do
300 delete :destroy, :project_id => 1, :id => 'Another_page'
300 delete :destroy, :project_id => 1, :id => 'Another_page'
301 end
301 end
302 assert_response :success
302 assert_response :success
303 assert_template 'destroy'
303 assert_template 'destroy'
304 end
304 end
305
305
306 def test_destroy_parent_with_nullify
306 def test_destroy_parent_with_nullify
307 @request.session[:user_id] = 2
307 @request.session[:user_id] = 2
308 assert_difference('WikiPage.count', -1) do
308 assert_difference('WikiPage.count', -1) do
309 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify'
309 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify'
310 end
310 end
311 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
311 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
312 assert_nil WikiPage.find_by_id(2)
312 assert_nil WikiPage.find_by_id(2)
313 end
313 end
314
314
315 def test_destroy_parent_with_cascade
315 def test_destroy_parent_with_cascade
316 @request.session[:user_id] = 2
316 @request.session[:user_id] = 2
317 assert_difference('WikiPage.count', -3) do
317 assert_difference('WikiPage.count', -3) do
318 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy'
318 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy'
319 end
319 end
320 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
320 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
321 assert_nil WikiPage.find_by_id(2)
321 assert_nil WikiPage.find_by_id(2)
322 assert_nil WikiPage.find_by_id(5)
322 assert_nil WikiPage.find_by_id(5)
323 end
323 end
324
324
325 def test_destroy_parent_with_reassign
325 def test_destroy_parent_with_reassign
326 @request.session[:user_id] = 2
326 @request.session[:user_id] = 2
327 assert_difference('WikiPage.count', -1) do
327 assert_difference('WikiPage.count', -1) do
328 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
328 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
329 end
329 end
330 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
330 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
331 assert_nil WikiPage.find_by_id(2)
331 assert_nil WikiPage.find_by_id(2)
332 assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
332 assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
333 end
333 end
334
334
335 def test_index
335 def test_index
336 get :index, :project_id => 'ecookbook'
336 get :index, :project_id => 'ecookbook'
337 assert_response :success
337 assert_response :success
338 assert_template 'index'
338 assert_template 'index'
339 pages = assigns(:pages)
339 pages = assigns(:pages)
340 assert_not_nil pages
340 assert_not_nil pages
341 assert_equal Project.find(1).wiki.pages.size, pages.size
341 assert_equal Project.find(1).wiki.pages.size, pages.size
342 assert_equal pages.first.content.updated_on, pages.first.updated_on
342
343
343 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
344 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
344 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
345 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
345 :content => 'CookBook documentation' },
346 :content => 'CookBook documentation' },
346 :child => { :tag => 'ul',
347 :child => { :tag => 'ul',
347 :child => { :tag => 'li',
348 :child => { :tag => 'li',
348 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
349 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
349 :content => 'Page with an inline image' } } } },
350 :content => 'Page with an inline image' } } } },
350 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
351 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
351 :content => 'Another page' } }
352 :content => 'Another page' } }
352 end
353 end
353
354
354 context "GET :export" do
355 context "GET :export" do
355 context "with an authorized user to export the wiki" do
356 context "with an authorized user to export the wiki" do
356 setup do
357 setup do
357 @request.session[:user_id] = 2
358 @request.session[:user_id] = 2
358 get :export, :project_id => 'ecookbook'
359 get :export, :project_id => 'ecookbook'
359 end
360 end
360
361
361 should_respond_with :success
362 should_respond_with :success
362 should_assign_to :pages
363 should_assign_to :pages
363 should_respond_with_content_type "text/html"
364 should_respond_with_content_type "text/html"
364 should "export all of the wiki pages to a single html file" do
365 should "export all of the wiki pages to a single html file" do
365 assert_select "a[name=?]", "CookBook_documentation"
366 assert_select "a[name=?]", "CookBook_documentation"
366 assert_select "a[name=?]", "Another_page"
367 assert_select "a[name=?]", "Another_page"
367 assert_select "a[name=?]", "Page_with_an_inline_image"
368 assert_select "a[name=?]", "Page_with_an_inline_image"
368 end
369 end
369
370
370 end
371 end
371
372
372 context "with an unauthorized user" do
373 context "with an unauthorized user" do
373 setup do
374 setup do
374 get :export, :project_id => 'ecookbook'
375 get :export, :project_id => 'ecookbook'
375
376
376 should_respond_with :redirect
377 should_respond_with :redirect
377 should_redirect_to('wiki index') { {:action => 'show', :project_id => @project, :id => nil} }
378 should_redirect_to('wiki index') { {:action => 'show', :project_id => @project, :id => nil} }
378 end
379 end
379 end
380 end
380 end
381 end
381
382
382 context "GET :date_index" do
383 context "GET :date_index" do
383 setup do
384 setup do
384 get :date_index, :project_id => 'ecookbook'
385 get :date_index, :project_id => 'ecookbook'
385 end
386 end
386
387
387 should_respond_with :success
388 should_respond_with :success
388 should_assign_to :pages
389 should_assign_to :pages
389 should_assign_to :pages_by_date
390 should_assign_to :pages_by_date
390 should_render_template 'wiki/date_index'
391 should_render_template 'wiki/date_index'
391
392
392 end
393 end
393
394
394 def test_not_found
395 def test_not_found
395 get :show, :project_id => 999
396 get :show, :project_id => 999
396 assert_response 404
397 assert_response 404
397 end
398 end
398
399
399 def test_protect_page
400 def test_protect_page
400 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
401 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
401 assert !page.protected?
402 assert !page.protected?
402 @request.session[:user_id] = 2
403 @request.session[:user_id] = 2
403 post :protect, :project_id => 1, :id => page.title, :protected => '1'
404 post :protect, :project_id => 1, :id => page.title, :protected => '1'
404 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
405 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
405 assert page.reload.protected?
406 assert page.reload.protected?
406 end
407 end
407
408
408 def test_unprotect_page
409 def test_unprotect_page
409 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
410 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
410 assert page.protected?
411 assert page.protected?
411 @request.session[:user_id] = 2
412 @request.session[:user_id] = 2
412 post :protect, :project_id => 1, :id => page.title, :protected => '0'
413 post :protect, :project_id => 1, :id => page.title, :protected => '0'
413 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation'
414 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation'
414 assert !page.reload.protected?
415 assert !page.reload.protected?
415 end
416 end
416
417
417 def test_show_page_with_edit_link
418 def test_show_page_with_edit_link
418 @request.session[:user_id] = 2
419 @request.session[:user_id] = 2
419 get :show, :project_id => 1
420 get :show, :project_id => 1
420 assert_response :success
421 assert_response :success
421 assert_template 'show'
422 assert_template 'show'
422 assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
423 assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
423 end
424 end
424
425
425 def test_show_page_without_edit_link
426 def test_show_page_without_edit_link
426 @request.session[:user_id] = 4
427 @request.session[:user_id] = 4
427 get :show, :project_id => 1
428 get :show, :project_id => 1
428 assert_response :success
429 assert_response :success
429 assert_template 'show'
430 assert_template 'show'
430 assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
431 assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
431 end
432 end
432
433
433 def test_edit_unprotected_page
434 def test_edit_unprotected_page
434 # Non members can edit unprotected wiki pages
435 # Non members can edit unprotected wiki pages
435 @request.session[:user_id] = 4
436 @request.session[:user_id] = 4
436 get :edit, :project_id => 1, :id => 'Another_page'
437 get :edit, :project_id => 1, :id => 'Another_page'
437 assert_response :success
438 assert_response :success
438 assert_template 'edit'
439 assert_template 'edit'
439 end
440 end
440
441
441 def test_edit_protected_page_by_nonmember
442 def test_edit_protected_page_by_nonmember
442 # Non members can't edit protected wiki pages
443 # Non members can't edit protected wiki pages
443 @request.session[:user_id] = 4
444 @request.session[:user_id] = 4
444 get :edit, :project_id => 1, :id => 'CookBook_documentation'
445 get :edit, :project_id => 1, :id => 'CookBook_documentation'
445 assert_response 403
446 assert_response 403
446 end
447 end
447
448
448 def test_edit_protected_page_by_member
449 def test_edit_protected_page_by_member
449 @request.session[:user_id] = 2
450 @request.session[:user_id] = 2
450 get :edit, :project_id => 1, :id => 'CookBook_documentation'
451 get :edit, :project_id => 1, :id => 'CookBook_documentation'
451 assert_response :success
452 assert_response :success
452 assert_template 'edit'
453 assert_template 'edit'
453 end
454 end
454
455
455 def test_history_of_non_existing_page_should_return_404
456 def test_history_of_non_existing_page_should_return_404
456 get :history, :project_id => 1, :id => 'Unknown_page'
457 get :history, :project_id => 1, :id => 'Unknown_page'
457 assert_response 404
458 assert_response 404
458 end
459 end
459 end
460 end
@@ -1,124 +1,131
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class WikiPageTest < ActiveSupport::TestCase
20 class WikiPageTest < ActiveSupport::TestCase
21 fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
21 fixtures :projects, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
22
22
23 def setup
23 def setup
24 @wiki = Wiki.find(1)
24 @wiki = Wiki.find(1)
25 @page = @wiki.pages.first
25 @page = @wiki.pages.first
26 end
26 end
27
27
28 def test_create
28 def test_create
29 page = WikiPage.new(:wiki => @wiki)
29 page = WikiPage.new(:wiki => @wiki)
30 assert !page.save
30 assert !page.save
31 assert_equal 1, page.errors.count
31 assert_equal 1, page.errors.count
32
32
33 page.title = "Page"
33 page.title = "Page"
34 assert page.save
34 assert page.save
35 page.reload
35 page.reload
36 assert !page.protected?
36 assert !page.protected?
37
37
38 @wiki.reload
38 @wiki.reload
39 assert @wiki.pages.include?(page)
39 assert @wiki.pages.include?(page)
40 end
40 end
41
41
42 def test_sidebar_should_be_protected_by_default
42 def test_sidebar_should_be_protected_by_default
43 page = @wiki.find_or_new_page('sidebar')
43 page = @wiki.find_or_new_page('sidebar')
44 assert page.new_record?
44 assert page.new_record?
45 assert page.protected?
45 assert page.protected?
46 end
46 end
47
47
48 def test_find_or_new_page
48 def test_find_or_new_page
49 page = @wiki.find_or_new_page("CookBook documentation")
49 page = @wiki.find_or_new_page("CookBook documentation")
50 assert_kind_of WikiPage, page
50 assert_kind_of WikiPage, page
51 assert !page.new_record?
51 assert !page.new_record?
52
52
53 page = @wiki.find_or_new_page("Non existing page")
53 page = @wiki.find_or_new_page("Non existing page")
54 assert_kind_of WikiPage, page
54 assert_kind_of WikiPage, page
55 assert page.new_record?
55 assert page.new_record?
56 end
56 end
57
57
58 def test_parent_title
58 def test_parent_title
59 page = WikiPage.find_by_title('Another_page')
59 page = WikiPage.find_by_title('Another_page')
60 assert_nil page.parent_title
60 assert_nil page.parent_title
61
61
62 page = WikiPage.find_by_title('Page_with_an_inline_image')
62 page = WikiPage.find_by_title('Page_with_an_inline_image')
63 assert_equal 'CookBook documentation', page.parent_title
63 assert_equal 'CookBook documentation', page.parent_title
64 end
64 end
65
65
66 def test_assign_parent
66 def test_assign_parent
67 page = WikiPage.find_by_title('Another_page')
67 page = WikiPage.find_by_title('Another_page')
68 page.parent_title = 'CookBook documentation'
68 page.parent_title = 'CookBook documentation'
69 assert page.save
69 assert page.save
70 page.reload
70 page.reload
71 assert_equal WikiPage.find_by_title('CookBook_documentation'), page.parent
71 assert_equal WikiPage.find_by_title('CookBook_documentation'), page.parent
72 end
72 end
73
73
74 def test_unassign_parent
74 def test_unassign_parent
75 page = WikiPage.find_by_title('Page_with_an_inline_image')
75 page = WikiPage.find_by_title('Page_with_an_inline_image')
76 page.parent_title = ''
76 page.parent_title = ''
77 assert page.save
77 assert page.save
78 page.reload
78 page.reload
79 assert_nil page.parent
79 assert_nil page.parent
80 end
80 end
81
81
82 def test_parent_validation
82 def test_parent_validation
83 page = WikiPage.find_by_title('CookBook_documentation')
83 page = WikiPage.find_by_title('CookBook_documentation')
84
84
85 # A page that doesn't exist
85 # A page that doesn't exist
86 page.parent_title = 'Unknown title'
86 page.parent_title = 'Unknown title'
87 assert !page.save
87 assert !page.save
88 assert_equal I18n.translate('activerecord.errors.messages.invalid'), page.errors.on(:parent_title)
88 assert_equal I18n.translate('activerecord.errors.messages.invalid'), page.errors.on(:parent_title)
89 # A child page
89 # A child page
90 page.parent_title = 'Page_with_an_inline_image'
90 page.parent_title = 'Page_with_an_inline_image'
91 assert !page.save
91 assert !page.save
92 assert_equal I18n.translate('activerecord.errors.messages.circular_dependency'), page.errors.on(:parent_title)
92 assert_equal I18n.translate('activerecord.errors.messages.circular_dependency'), page.errors.on(:parent_title)
93 # The page itself
93 # The page itself
94 page.parent_title = 'CookBook_documentation'
94 page.parent_title = 'CookBook_documentation'
95 assert !page.save
95 assert !page.save
96 assert_equal I18n.translate('activerecord.errors.messages.circular_dependency'), page.errors.on(:parent_title)
96 assert_equal I18n.translate('activerecord.errors.messages.circular_dependency'), page.errors.on(:parent_title)
97
97
98 page.parent_title = 'Another_page'
98 page.parent_title = 'Another_page'
99 assert page.save
99 assert page.save
100 end
100 end
101
101
102 def test_destroy
102 def test_destroy
103 page = WikiPage.find(1)
103 page = WikiPage.find(1)
104 page.destroy
104 page.destroy
105 assert_nil WikiPage.find_by_id(1)
105 assert_nil WikiPage.find_by_id(1)
106 # make sure that page content and its history are deleted
106 # make sure that page content and its history are deleted
107 assert WikiContent.find_all_by_page_id(1).empty?
107 assert WikiContent.find_all_by_page_id(1).empty?
108 assert WikiContent.versioned_class.find_all_by_page_id(1).empty?
108 assert WikiContent.versioned_class.find_all_by_page_id(1).empty?
109 end
109 end
110
110
111 def test_destroy_should_not_nullify_children
111 def test_destroy_should_not_nullify_children
112 page = WikiPage.find(2)
112 page = WikiPage.find(2)
113 child_ids = page.child_ids
113 child_ids = page.child_ids
114 assert child_ids.any?
114 assert child_ids.any?
115 page.destroy
115 page.destroy
116 assert_nil WikiPage.find_by_id(2)
116 assert_nil WikiPage.find_by_id(2)
117
117
118 children = WikiPage.find_all_by_id(child_ids)
118 children = WikiPage.find_all_by_id(child_ids)
119 assert_equal child_ids.size, children.size
119 assert_equal child_ids.size, children.size
120 children.each do |child|
120 children.each do |child|
121 assert_nil child.parent_id
121 assert_nil child.parent_id
122 end
122 end
123 end
123 end
124
125 def test_updated_on_eager_load
126 page = WikiPage.with_updated_on.first
127 assert page.is_a?(WikiPage)
128 assert_not_nil page.read_attribute(:updated_on)
129 assert_equal page.content.updated_on, page.updated_on
130 end
124 end
131 end
General Comments 0
You need to be logged in to leave comments. Login now