##// END OF EJS Templates
Ability to delete a version from a wiki page history (#10852)....
Jean-Philippe Lang -
r10493:6cccdce06eff
parent child
Show More
@@ -0,0 +1,68
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
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
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.expand_path('../../test_helper', __FILE__)
19
20 class WikiContentTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions
22
23 def setup
24 end
25
26 def test_destroy
27 v = WikiContent::Version.find(2)
28
29 assert_difference 'WikiContent::Version.count', -1 do
30 v.destroy
31 end
32 end
33
34 def test_destroy_last_version_should_revert_content
35 v = WikiContent::Version.find(3)
36
37 assert_no_difference 'WikiPage.count' do
38 assert_no_difference 'WikiContent.count' do
39 assert_difference 'WikiContent::Version.count', -1 do
40 assert v.destroy
41 end
42 end
43 end
44 c = WikiContent.find(1)
45 v = c.versions.last
46 assert_equal 2, c.version
47 assert_equal v.version, c.version
48 assert_equal v.comments, c.comments
49 assert_equal v.text, c.text
50 assert_equal v.author, c.author
51 assert_equal v.updated_on, c.updated_on
52 end
53
54 def test_destroy_all_versions_should_delete_page
55 WikiContent::Version.find(1).destroy
56 WikiContent::Version.find(2).destroy
57 v = WikiContent::Version.find(3)
58
59 assert_difference 'WikiPage.count', -1 do
60 assert_difference 'WikiContent.count', -1 do
61 assert_difference 'WikiContent::Version.count', -1 do
62 assert v.destroy
63 end
64 end
65 end
66 assert_nil WikiPage.find_by_id(1)
67 end
68 end
@@ -1,318 +1,326
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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_or_new_page, :only => [:show, :edit, :update]
37 before_filter :find_existing_or_new_page, :only => [:show, :edit, :update]
38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
38 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy, :destroy_version]
39
39
40 helper :attachments
40 helper :attachments
41 include AttachmentsHelper
41 include AttachmentsHelper
42 helper :watchers
42 helper :watchers
43 include Redmine::Export::PDF
43 include Redmine::Export::PDF
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_for_index
47 load_pages_for_index
48 @pages_by_parent_id = @pages.group_by(&:parent_id)
48 @pages_by_parent_id = @pages.group_by(&:parent_id)
49 end
49 end
50
50
51 # List of page, by last update
51 # List of page, by last update
52 def date_index
52 def date_index
53 load_pages_for_index
53 load_pages_for_index
54 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
54 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
55 end
55 end
56
56
57 # display a page (in editing mode if it doesn't exist)
57 # display a page (in editing mode if it doesn't exist)
58 def show
58 def show
59 if @page.new_record?
59 if @page.new_record?
60 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
60 if User.current.allowed_to?(:edit_wiki_pages, @project) && editable?
61 edit
61 edit
62 render :action => 'edit'
62 render :action => 'edit'
63 else
63 else
64 render_404
64 render_404
65 end
65 end
66 return
66 return
67 end
67 end
68 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
68 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
69 # Redirects user to the current version if he's not allowed to view previous versions
69 # Redirects user to the current version if he's not allowed to view previous versions
70 redirect_to :version => nil
70 redirect_to :version => nil
71 return
71 return
72 end
72 end
73 @content = @page.content_for_version(params[:version])
73 @content = @page.content_for_version(params[:version])
74 if User.current.allowed_to?(:export_wiki_pages, @project)
74 if User.current.allowed_to?(:export_wiki_pages, @project)
75 if params[:format] == 'pdf'
75 if params[:format] == 'pdf'
76 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
76 send_data(wiki_page_to_pdf(@page, @project), :type => 'application/pdf', :filename => "#{@page.title}.pdf")
77 return
77 return
78 elsif params[:format] == 'html'
78 elsif params[:format] == 'html'
79 export = render_to_string :action => 'export', :layout => false
79 export = render_to_string :action => 'export', :layout => false
80 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
80 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
81 return
81 return
82 elsif params[:format] == 'txt'
82 elsif params[:format] == 'txt'
83 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
83 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
84 return
84 return
85 end
85 end
86 end
86 end
87 @editable = editable?
87 @editable = editable?
88 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
88 @sections_editable = @editable && User.current.allowed_to?(:edit_wiki_pages, @page.project) &&
89 @content.current_version? &&
89 @content.current_version? &&
90 Redmine::WikiFormatting.supports_section_edit?
90 Redmine::WikiFormatting.supports_section_edit?
91
91
92 render :action => 'show'
92 render :action => 'show'
93 end
93 end
94
94
95 # edit an existing page or a new one
95 # edit an existing page or a new one
96 def edit
96 def edit
97 return render_403 unless editable?
97 return render_403 unless editable?
98 if @page.new_record?
98 if @page.new_record?
99 @page.content = WikiContent.new(:page => @page)
99 @page.content = WikiContent.new(:page => @page)
100 if params[:parent].present?
100 if params[:parent].present?
101 @page.parent = @page.wiki.find_page(params[:parent].to_s)
101 @page.parent = @page.wiki.find_page(params[:parent].to_s)
102 end
102 end
103 end
103 end
104
104
105 @content = @page.content_for_version(params[:version])
105 @content = @page.content_for_version(params[:version])
106 @content.text = initial_page_content(@page) if @content.text.blank?
106 @content.text = initial_page_content(@page) if @content.text.blank?
107 # don't keep previous comment
107 # don't keep previous comment
108 @content.comments = nil
108 @content.comments = nil
109
109
110 # To prevent StaleObjectError exception when reverting to a previous version
110 # To prevent StaleObjectError exception when reverting to a previous version
111 @content.version = @page.content.version
111 @content.version = @page.content.version
112
112
113 @text = @content.text
113 @text = @content.text
114 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
114 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
115 @section = params[:section].to_i
115 @section = params[:section].to_i
116 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
116 @text, @section_hash = Redmine::WikiFormatting.formatter.new(@text).get_section(@section)
117 render_404 if @text.blank?
117 render_404 if @text.blank?
118 end
118 end
119 end
119 end
120
120
121 # Creates a new page or updates an existing one
121 # Creates a new page or updates an existing one
122 def update
122 def update
123 return render_403 unless editable?
123 return render_403 unless editable?
124 @page.content = WikiContent.new(:page => @page) if @page.new_record?
124 @page.content = WikiContent.new(:page => @page) if @page.new_record?
125 @page.safe_attributes = params[:wiki_page]
125 @page.safe_attributes = params[:wiki_page]
126
126
127 @content = @page.content_for_version(params[:version])
127 @content = @page.content_for_version(params[:version])
128 @content.text = initial_page_content(@page) if @content.text.blank?
128 @content.text = initial_page_content(@page) if @content.text.blank?
129 # don't keep previous comment
129 # don't keep previous comment
130 @content.comments = nil
130 @content.comments = nil
131
131
132 if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
132 if !@page.new_record? && params[:content].present? && @content.text == params[:content][:text]
133 attachments = Attachment.attach_files(@page, params[:attachments])
133 attachments = Attachment.attach_files(@page, params[:attachments])
134 render_attachment_warning_if_needed(@page)
134 render_attachment_warning_if_needed(@page)
135 # don't save content if text wasn't changed
135 # don't save content if text wasn't changed
136 @page.save
136 @page.save
137 redirect_to :action => 'show', :project_id => @project, :id => @page.title
137 redirect_to :action => 'show', :project_id => @project, :id => @page.title
138 return
138 return
139 end
139 end
140
140
141 @content.comments = params[:content][:comments]
141 @content.comments = params[:content][:comments]
142 @text = params[:content][:text]
142 @text = params[:content][:text]
143 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
143 if params[:section].present? && Redmine::WikiFormatting.supports_section_edit?
144 @section = params[:section].to_i
144 @section = params[:section].to_i
145 @section_hash = params[:section_hash]
145 @section_hash = params[:section_hash]
146 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
146 @content.text = Redmine::WikiFormatting.formatter.new(@content.text).update_section(params[:section].to_i, @text, @section_hash)
147 else
147 else
148 @content.version = params[:content][:version]
148 @content.version = params[:content][:version]
149 @content.text = @text
149 @content.text = @text
150 end
150 end
151 @content.author = User.current
151 @content.author = User.current
152 @page.content = @content
152 @page.content = @content
153 if @page.save
153 if @page.save
154 attachments = Attachment.attach_files(@page, params[:attachments])
154 attachments = Attachment.attach_files(@page, params[:attachments])
155 render_attachment_warning_if_needed(@page)
155 render_attachment_warning_if_needed(@page)
156 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
156 call_hook(:controller_wiki_edit_after_save, { :params => params, :page => @page})
157 redirect_to :action => 'show', :project_id => @project, :id => @page.title
157 redirect_to :action => 'show', :project_id => @project, :id => @page.title
158 else
158 else
159 render :action => 'edit'
159 render :action => 'edit'
160 end
160 end
161
161
162 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
162 rescue ActiveRecord::StaleObjectError, Redmine::WikiFormatting::StaleSectionError
163 # Optimistic locking exception
163 # Optimistic locking exception
164 flash.now[:error] = l(:notice_locking_conflict)
164 flash.now[:error] = l(:notice_locking_conflict)
165 render :action => 'edit'
165 render :action => 'edit'
166 rescue ActiveRecord::RecordNotSaved
166 rescue ActiveRecord::RecordNotSaved
167 render :action => 'edit'
167 render :action => 'edit'
168 end
168 end
169
169
170 # rename a page
170 # rename a page
171 def rename
171 def rename
172 return render_403 unless editable?
172 return render_403 unless editable?
173 @page.redirect_existing_links = true
173 @page.redirect_existing_links = true
174 # used to display the *original* title if some AR validation errors occur
174 # used to display the *original* title if some AR validation errors occur
175 @original_title = @page.pretty_title
175 @original_title = @page.pretty_title
176 if request.post? && @page.update_attributes(params[:wiki_page])
176 if request.post? && @page.update_attributes(params[:wiki_page])
177 flash[:notice] = l(:notice_successful_update)
177 flash[:notice] = l(:notice_successful_update)
178 redirect_to :action => 'show', :project_id => @project, :id => @page.title
178 redirect_to :action => 'show', :project_id => @project, :id => @page.title
179 end
179 end
180 end
180 end
181
181
182 def protect
182 def protect
183 @page.update_attribute :protected, params[:protected]
183 @page.update_attribute :protected, params[:protected]
184 redirect_to :action => 'show', :project_id => @project, :id => @page.title
184 redirect_to :action => 'show', :project_id => @project, :id => @page.title
185 end
185 end
186
186
187 # show page history
187 # show page history
188 def history
188 def history
189 @version_count = @page.content.versions.count
189 @version_count = @page.content.versions.count
190 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
190 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
191 # don't load text
191 # don't load text
192 @versions = @page.content.versions.find :all,
192 @versions = @page.content.versions.find :all,
193 :select => "id, author_id, comments, updated_on, version",
193 :select => "id, author_id, comments, updated_on, version",
194 :order => 'version DESC',
194 :order => 'version DESC',
195 :limit => @version_pages.items_per_page + 1,
195 :limit => @version_pages.items_per_page + 1,
196 :offset => @version_pages.current.offset
196 :offset => @version_pages.current.offset
197
197
198 render :layout => false if request.xhr?
198 render :layout => false if request.xhr?
199 end
199 end
200
200
201 def diff
201 def diff
202 @diff = @page.diff(params[:version], params[:version_from])
202 @diff = @page.diff(params[:version], params[:version_from])
203 render_404 unless @diff
203 render_404 unless @diff
204 end
204 end
205
205
206 def annotate
206 def annotate
207 @annotate = @page.annotate(params[:version])
207 @annotate = @page.annotate(params[:version])
208 render_404 unless @annotate
208 render_404 unless @annotate
209 end
209 end
210
210
211 # Removes a wiki page and its history
211 # Removes a wiki page and its history
212 # Children can be either set as root pages, removed or reassigned to another parent page
212 # Children can be either set as root pages, removed or reassigned to another parent page
213 def destroy
213 def destroy
214 return render_403 unless editable?
214 return render_403 unless editable?
215
215
216 @descendants_count = @page.descendants.size
216 @descendants_count = @page.descendants.size
217 if @descendants_count > 0
217 if @descendants_count > 0
218 case params[:todo]
218 case params[:todo]
219 when 'nullify'
219 when 'nullify'
220 # Nothing to do
220 # Nothing to do
221 when 'destroy'
221 when 'destroy'
222 # Removes all its descendants
222 # Removes all its descendants
223 @page.descendants.each(&:destroy)
223 @page.descendants.each(&:destroy)
224 when 'reassign'
224 when 'reassign'
225 # Reassign children to another parent page
225 # Reassign children to another parent page
226 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
226 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
227 return unless reassign_to
227 return unless reassign_to
228 @page.children.each do |child|
228 @page.children.each do |child|
229 child.update_attribute(:parent, reassign_to)
229 child.update_attribute(:parent, reassign_to)
230 end
230 end
231 else
231 else
232 @reassignable_to = @wiki.pages - @page.self_and_descendants
232 @reassignable_to = @wiki.pages - @page.self_and_descendants
233 return
233 return
234 end
234 end
235 end
235 end
236 @page.destroy
236 @page.destroy
237 redirect_to :action => 'index', :project_id => @project
237 redirect_to :action => 'index', :project_id => @project
238 end
238 end
239
239
240 def destroy_version
241 return render_403 unless editable?
242
243 @content = @page.content_for_version(params[:version])
244 @content.destroy
245 redirect_to_referer_or :action => 'history', :id => @page.title, :project_id => @project
246 end
247
240 # Export wiki to a single pdf or html file
248 # Export wiki to a single pdf or html file
241 def export
249 def export
242 @pages = @wiki.pages.all(:order => 'title', :include => [:content, :attachments], :limit => 75)
250 @pages = @wiki.pages.all(:order => 'title', :include => [:content, :attachments], :limit => 75)
243 respond_to do |format|
251 respond_to do |format|
244 format.html {
252 format.html {
245 export = render_to_string :action => 'export_multiple', :layout => false
253 export = render_to_string :action => 'export_multiple', :layout => false
246 send_data(export, :type => 'text/html', :filename => "wiki.html")
254 send_data(export, :type => 'text/html', :filename => "wiki.html")
247 }
255 }
248 format.pdf {
256 format.pdf {
249 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
257 send_data(wiki_pages_to_pdf(@pages, @project), :type => 'application/pdf', :filename => "#{@project.identifier}.pdf")
250 }
258 }
251 end
259 end
252 end
260 end
253
261
254 def preview
262 def preview
255 page = @wiki.find_page(params[:id])
263 page = @wiki.find_page(params[:id])
256 # page is nil when previewing a new page
264 # page is nil when previewing a new page
257 return render_403 unless page.nil? || editable?(page)
265 return render_403 unless page.nil? || editable?(page)
258 if page
266 if page
259 @attachements = page.attachments
267 @attachements = page.attachments
260 @previewed = page.content
268 @previewed = page.content
261 end
269 end
262 @text = params[:content][:text]
270 @text = params[:content][:text]
263 render :partial => 'common/preview'
271 render :partial => 'common/preview'
264 end
272 end
265
273
266 def add_attachment
274 def add_attachment
267 return render_403 unless editable?
275 return render_403 unless editable?
268 attachments = Attachment.attach_files(@page, params[:attachments])
276 attachments = Attachment.attach_files(@page, params[:attachments])
269 render_attachment_warning_if_needed(@page)
277 render_attachment_warning_if_needed(@page)
270 redirect_to :action => 'show', :id => @page.title, :project_id => @project
278 redirect_to :action => 'show', :id => @page.title, :project_id => @project
271 end
279 end
272
280
273 private
281 private
274
282
275 def find_wiki
283 def find_wiki
276 @project = Project.find(params[:project_id])
284 @project = Project.find(params[:project_id])
277 @wiki = @project.wiki
285 @wiki = @project.wiki
278 render_404 unless @wiki
286 render_404 unless @wiki
279 rescue ActiveRecord::RecordNotFound
287 rescue ActiveRecord::RecordNotFound
280 render_404
288 render_404
281 end
289 end
282
290
283 # Finds the requested page or a new page if it doesn't exist
291 # Finds the requested page or a new page if it doesn't exist
284 def find_existing_or_new_page
292 def find_existing_or_new_page
285 @page = @wiki.find_or_new_page(params[:id])
293 @page = @wiki.find_or_new_page(params[:id])
286 if @wiki.page_found_with_redirect?
294 if @wiki.page_found_with_redirect?
287 redirect_to params.update(:id => @page.title)
295 redirect_to params.update(:id => @page.title)
288 end
296 end
289 end
297 end
290
298
291 # Finds the requested page and returns a 404 error if it doesn't exist
299 # Finds the requested page and returns a 404 error if it doesn't exist
292 def find_existing_page
300 def find_existing_page
293 @page = @wiki.find_page(params[:id])
301 @page = @wiki.find_page(params[:id])
294 if @page.nil?
302 if @page.nil?
295 render_404
303 render_404
296 return
304 return
297 end
305 end
298 if @wiki.page_found_with_redirect?
306 if @wiki.page_found_with_redirect?
299 redirect_to params.update(:id => @page.title)
307 redirect_to params.update(:id => @page.title)
300 end
308 end
301 end
309 end
302
310
303 # Returns true if the current user is allowed to edit the page, otherwise false
311 # Returns true if the current user is allowed to edit the page, otherwise false
304 def editable?(page = @page)
312 def editable?(page = @page)
305 page.editable_by?(User.current)
313 page.editable_by?(User.current)
306 end
314 end
307
315
308 # Returns the default content of a new wiki page
316 # Returns the default content of a new wiki page
309 def initial_page_content(page)
317 def initial_page_content(page)
310 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
318 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
311 extend helper unless self.instance_of?(helper)
319 extend helper unless self.instance_of?(helper)
312 helper.instance_method(:initial_page_content).bind(self).call(page)
320 helper.instance_method(:initial_page_content).bind(self).call(page)
313 end
321 end
314
322
315 def load_pages_for_index
323 def load_pages_for_index
316 @pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
324 @pages = @wiki.pages.with_updated_on.all(:order => 'title', :include => {:wiki => :project})
317 end
325 end
318 end
326 end
@@ -1,1273 +1,1277
1 # encoding: utf-8
1 # encoding: utf-8
2 #
2 #
3 # Redmine - project management software
3 # Redmine - project management software
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
4 # Copyright (C) 2006-2012 Jean-Philippe Lang
5 #
5 #
6 # This program is free software; you can redistribute it and/or
6 # This program is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU General Public License
7 # modify it under the terms of the GNU General Public License
8 # as published by the Free Software Foundation; either version 2
8 # as published by the Free Software Foundation; either version 2
9 # of the License, or (at your option) any later version.
9 # of the License, or (at your option) any later version.
10 #
10 #
11 # This program is distributed in the hope that it will be useful,
11 # This program is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
14 # GNU General Public License for more details.
15 #
15 #
16 # You should have received a copy of the GNU General Public License
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19
19
20 require 'forwardable'
20 require 'forwardable'
21 require 'cgi'
21 require 'cgi'
22
22
23 module ApplicationHelper
23 module ApplicationHelper
24 include Redmine::WikiFormatting::Macros::Definitions
24 include Redmine::WikiFormatting::Macros::Definitions
25 include Redmine::I18n
25 include Redmine::I18n
26 include GravatarHelper::PublicMethods
26 include GravatarHelper::PublicMethods
27
27
28 extend Forwardable
28 extend Forwardable
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
29 def_delegators :wiki_helper, :wikitoolbar_for, :heads_for_wiki_formatter
30
30
31 # Return true if user is authorized for controller/action, otherwise false
31 # Return true if user is authorized for controller/action, otherwise false
32 def authorize_for(controller, action)
32 def authorize_for(controller, action)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
33 User.current.allowed_to?({:controller => controller, :action => action}, @project)
34 end
34 end
35
35
36 # Display a link if user is authorized
36 # Display a link if user is authorized
37 #
37 #
38 # @param [String] name Anchor text (passed to link_to)
38 # @param [String] name Anchor text (passed to link_to)
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
39 # @param [Hash] options Hash params. This will checked by authorize_for to see if the user is authorized
40 # @param [optional, Hash] html_options Options passed to link_to
40 # @param [optional, Hash] html_options Options passed to link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
41 # @param [optional, Hash] parameters_for_method_reference Extra parameters for link_to
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
42 def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
43 link_to(name, options, html_options, *parameters_for_method_reference) if authorize_for(options[:controller] || params[:controller], options[:action])
44 end
44 end
45
45
46 # Displays a link to user's account page if active
46 # Displays a link to user's account page if active
47 def link_to_user(user, options={})
47 def link_to_user(user, options={})
48 if user.is_a?(User)
48 if user.is_a?(User)
49 name = h(user.name(options[:format]))
49 name = h(user.name(options[:format]))
50 if user.active? || (User.current.admin? && user.logged?)
50 if user.active? || (User.current.admin? && user.logged?)
51 link_to name, user_path(user), :class => user.css_classes
51 link_to name, user_path(user), :class => user.css_classes
52 else
52 else
53 name
53 name
54 end
54 end
55 else
55 else
56 h(user.to_s)
56 h(user.to_s)
57 end
57 end
58 end
58 end
59
59
60 # Displays a link to +issue+ with its subject.
60 # Displays a link to +issue+ with its subject.
61 # Examples:
61 # Examples:
62 #
62 #
63 # link_to_issue(issue) # => Defect #6: This is the subject
63 # link_to_issue(issue) # => Defect #6: This is the subject
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
64 # link_to_issue(issue, :truncate => 6) # => Defect #6: This i...
65 # link_to_issue(issue, :subject => false) # => Defect #6
65 # link_to_issue(issue, :subject => false) # => Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
66 # link_to_issue(issue, :project => true) # => Foo - Defect #6
67 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
67 # link_to_issue(issue, :subject => false, :tracker => false) # => #6
68 #
68 #
69 def link_to_issue(issue, options={})
69 def link_to_issue(issue, options={})
70 title = nil
70 title = nil
71 subject = nil
71 subject = nil
72 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
72 text = options[:tracker] == false ? "##{issue.id}" : "#{issue.tracker} ##{issue.id}"
73 if options[:subject] == false
73 if options[:subject] == false
74 title = truncate(issue.subject, :length => 60)
74 title = truncate(issue.subject, :length => 60)
75 else
75 else
76 subject = issue.subject
76 subject = issue.subject
77 if options[:truncate]
77 if options[:truncate]
78 subject = truncate(subject, :length => options[:truncate])
78 subject = truncate(subject, :length => options[:truncate])
79 end
79 end
80 end
80 end
81 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
81 s = link_to text, issue_path(issue), :class => issue.css_classes, :title => title
82 s << h(": #{subject}") if subject
82 s << h(": #{subject}") if subject
83 s = h("#{issue.project} - ") + s if options[:project]
83 s = h("#{issue.project} - ") + s if options[:project]
84 s
84 s
85 end
85 end
86
86
87 # Generates a link to an attachment.
87 # Generates a link to an attachment.
88 # Options:
88 # Options:
89 # * :text - Link text (default to attachment filename)
89 # * :text - Link text (default to attachment filename)
90 # * :download - Force download (default: false)
90 # * :download - Force download (default: false)
91 def link_to_attachment(attachment, options={})
91 def link_to_attachment(attachment, options={})
92 text = options.delete(:text) || attachment.filename
92 text = options.delete(:text) || attachment.filename
93 action = options.delete(:download) ? 'download' : 'show'
93 action = options.delete(:download) ? 'download' : 'show'
94 opt_only_path = {}
94 opt_only_path = {}
95 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
95 opt_only_path[:only_path] = (options[:only_path] == false ? false : true)
96 options.delete(:only_path)
96 options.delete(:only_path)
97 link_to(h(text),
97 link_to(h(text),
98 {:controller => 'attachments', :action => action,
98 {:controller => 'attachments', :action => action,
99 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
99 :id => attachment, :filename => attachment.filename}.merge(opt_only_path),
100 options)
100 options)
101 end
101 end
102
102
103 # Generates a link to a SCM revision
103 # Generates a link to a SCM revision
104 # Options:
104 # Options:
105 # * :text - Link text (default to the formatted revision)
105 # * :text - Link text (default to the formatted revision)
106 def link_to_revision(revision, repository, options={})
106 def link_to_revision(revision, repository, options={})
107 if repository.is_a?(Project)
107 if repository.is_a?(Project)
108 repository = repository.repository
108 repository = repository.repository
109 end
109 end
110 text = options.delete(:text) || format_revision(revision)
110 text = options.delete(:text) || format_revision(revision)
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
111 rev = revision.respond_to?(:identifier) ? revision.identifier : revision
112 link_to(
112 link_to(
113 h(text),
113 h(text),
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
114 {:controller => 'repositories', :action => 'revision', :id => repository.project, :repository_id => repository.identifier_param, :rev => rev},
115 :title => l(:label_revision_id, format_revision(revision))
115 :title => l(:label_revision_id, format_revision(revision))
116 )
116 )
117 end
117 end
118
118
119 # Generates a link to a message
119 # Generates a link to a message
120 def link_to_message(message, options={}, html_options = nil)
120 def link_to_message(message, options={}, html_options = nil)
121 link_to(
121 link_to(
122 h(truncate(message.subject, :length => 60)),
122 h(truncate(message.subject, :length => 60)),
123 { :controller => 'messages', :action => 'show',
123 { :controller => 'messages', :action => 'show',
124 :board_id => message.board_id,
124 :board_id => message.board_id,
125 :id => (message.parent_id || message.id),
125 :id => (message.parent_id || message.id),
126 :r => (message.parent_id && message.id),
126 :r => (message.parent_id && message.id),
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
127 :anchor => (message.parent_id ? "message-#{message.id}" : nil)
128 }.merge(options),
128 }.merge(options),
129 html_options
129 html_options
130 )
130 )
131 end
131 end
132
132
133 # Generates a link to a project if active
133 # Generates a link to a project if active
134 # Examples:
134 # Examples:
135 #
135 #
136 # link_to_project(project) # => link to the specified project overview
136 # link_to_project(project) # => link to the specified project overview
137 # link_to_project(project, :action=>'settings') # => link to project settings
137 # link_to_project(project, :action=>'settings') # => link to project settings
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
138 # link_to_project(project, {:only_path => false}, :class => "project") # => 3rd arg adds html options
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
139 # link_to_project(project, {}, :class => "project") # => html options with default url (project overview)
140 #
140 #
141 def link_to_project(project, options={}, html_options = nil)
141 def link_to_project(project, options={}, html_options = nil)
142 if project.archived?
142 if project.archived?
143 h(project)
143 h(project)
144 else
144 else
145 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
145 url = {:controller => 'projects', :action => 'show', :id => project}.merge(options)
146 link_to(h(project), url, html_options)
146 link_to(h(project), url, html_options)
147 end
147 end
148 end
148 end
149
149
150 def wiki_page_path(page, options={})
151 url_for({:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title}.merge(options))
152 end
153
150 def thumbnail_tag(attachment)
154 def thumbnail_tag(attachment)
151 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
155 link_to image_tag(url_for(:controller => 'attachments', :action => 'thumbnail', :id => attachment)),
152 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
156 {:controller => 'attachments', :action => 'show', :id => attachment, :filename => attachment.filename},
153 :title => attachment.filename
157 :title => attachment.filename
154 end
158 end
155
159
156 def toggle_link(name, id, options={})
160 def toggle_link(name, id, options={})
157 onclick = "$('##{id}').toggle(); "
161 onclick = "$('##{id}').toggle(); "
158 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
162 onclick << (options[:focus] ? "$('##{options[:focus]}').focus(); " : "this.blur(); ")
159 onclick << "return false;"
163 onclick << "return false;"
160 link_to(name, "#", :onclick => onclick)
164 link_to(name, "#", :onclick => onclick)
161 end
165 end
162
166
163 def image_to_function(name, function, html_options = {})
167 def image_to_function(name, function, html_options = {})
164 html_options.symbolize_keys!
168 html_options.symbolize_keys!
165 tag(:input, html_options.merge({
169 tag(:input, html_options.merge({
166 :type => "image", :src => image_path(name),
170 :type => "image", :src => image_path(name),
167 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
171 :onclick => (html_options[:onclick] ? "#{html_options[:onclick]}; " : "") + "#{function};"
168 }))
172 }))
169 end
173 end
170
174
171 def format_activity_title(text)
175 def format_activity_title(text)
172 h(truncate_single_line(text, :length => 100))
176 h(truncate_single_line(text, :length => 100))
173 end
177 end
174
178
175 def format_activity_day(date)
179 def format_activity_day(date)
176 date == User.current.today ? l(:label_today).titleize : format_date(date)
180 date == User.current.today ? l(:label_today).titleize : format_date(date)
177 end
181 end
178
182
179 def format_activity_description(text)
183 def format_activity_description(text)
180 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
184 h(truncate(text.to_s, :length => 120).gsub(%r{[\r\n]*<(pre|code)>.*$}m, '...')
181 ).gsub(/[\r\n]+/, "<br />").html_safe
185 ).gsub(/[\r\n]+/, "<br />").html_safe
182 end
186 end
183
187
184 def format_version_name(version)
188 def format_version_name(version)
185 if version.project == @project
189 if version.project == @project
186 h(version)
190 h(version)
187 else
191 else
188 h("#{version.project} - #{version}")
192 h("#{version.project} - #{version}")
189 end
193 end
190 end
194 end
191
195
192 def due_date_distance_in_words(date)
196 def due_date_distance_in_words(date)
193 if date
197 if date
194 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
198 l((date < Date.today ? :label_roadmap_overdue : :label_roadmap_due_in), distance_of_date_in_words(Date.today, date))
195 end
199 end
196 end
200 end
197
201
198 # Renders a tree of projects as a nested set of unordered lists
202 # Renders a tree of projects as a nested set of unordered lists
199 # The given collection may be a subset of the whole project tree
203 # The given collection may be a subset of the whole project tree
200 # (eg. some intermediate nodes are private and can not be seen)
204 # (eg. some intermediate nodes are private and can not be seen)
201 def render_project_nested_lists(projects)
205 def render_project_nested_lists(projects)
202 s = ''
206 s = ''
203 if projects.any?
207 if projects.any?
204 ancestors = []
208 ancestors = []
205 original_project = @project
209 original_project = @project
206 projects.sort_by(&:lft).each do |project|
210 projects.sort_by(&:lft).each do |project|
207 # set the project environment to please macros.
211 # set the project environment to please macros.
208 @project = project
212 @project = project
209 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
213 if (ancestors.empty? || project.is_descendant_of?(ancestors.last))
210 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
214 s << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
211 else
215 else
212 ancestors.pop
216 ancestors.pop
213 s << "</li>"
217 s << "</li>"
214 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
218 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
215 ancestors.pop
219 ancestors.pop
216 s << "</ul></li>\n"
220 s << "</ul></li>\n"
217 end
221 end
218 end
222 end
219 classes = (ancestors.empty? ? 'root' : 'child')
223 classes = (ancestors.empty? ? 'root' : 'child')
220 s << "<li class='#{classes}'><div class='#{classes}'>"
224 s << "<li class='#{classes}'><div class='#{classes}'>"
221 s << h(block_given? ? yield(project) : project.name)
225 s << h(block_given? ? yield(project) : project.name)
222 s << "</div>\n"
226 s << "</div>\n"
223 ancestors << project
227 ancestors << project
224 end
228 end
225 s << ("</li></ul>\n" * ancestors.size)
229 s << ("</li></ul>\n" * ancestors.size)
226 @project = original_project
230 @project = original_project
227 end
231 end
228 s.html_safe
232 s.html_safe
229 end
233 end
230
234
231 def render_page_hierarchy(pages, node=nil, options={})
235 def render_page_hierarchy(pages, node=nil, options={})
232 content = ''
236 content = ''
233 if pages[node]
237 if pages[node]
234 content << "<ul class=\"pages-hierarchy\">\n"
238 content << "<ul class=\"pages-hierarchy\">\n"
235 pages[node].each do |page|
239 pages[node].each do |page|
236 content << "<li>"
240 content << "<li>"
237 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
241 content << link_to(h(page.pretty_title), {:controller => 'wiki', :action => 'show', :project_id => page.project, :id => page.title, :version => nil},
238 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
242 :title => (options[:timestamp] && page.updated_on ? l(:label_updated_time, distance_of_time_in_words(Time.now, page.updated_on)) : nil))
239 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
243 content << "\n" + render_page_hierarchy(pages, page.id, options) if pages[page.id]
240 content << "</li>\n"
244 content << "</li>\n"
241 end
245 end
242 content << "</ul>\n"
246 content << "</ul>\n"
243 end
247 end
244 content.html_safe
248 content.html_safe
245 end
249 end
246
250
247 # Renders flash messages
251 # Renders flash messages
248 def render_flash_messages
252 def render_flash_messages
249 s = ''
253 s = ''
250 flash.each do |k,v|
254 flash.each do |k,v|
251 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
255 s << content_tag('div', v.html_safe, :class => "flash #{k}", :id => "flash_#{k}")
252 end
256 end
253 s.html_safe
257 s.html_safe
254 end
258 end
255
259
256 # Renders tabs and their content
260 # Renders tabs and their content
257 def render_tabs(tabs)
261 def render_tabs(tabs)
258 if tabs.any?
262 if tabs.any?
259 render :partial => 'common/tabs', :locals => {:tabs => tabs}
263 render :partial => 'common/tabs', :locals => {:tabs => tabs}
260 else
264 else
261 content_tag 'p', l(:label_no_data), :class => "nodata"
265 content_tag 'p', l(:label_no_data), :class => "nodata"
262 end
266 end
263 end
267 end
264
268
265 # Renders the project quick-jump box
269 # Renders the project quick-jump box
266 def render_project_jump_box
270 def render_project_jump_box
267 return unless User.current.logged?
271 return unless User.current.logged?
268 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
272 projects = User.current.memberships.collect(&:project).compact.select(&:active?).uniq
269 if projects.any?
273 if projects.any?
270 options =
274 options =
271 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
275 ("<option value=''>#{ l(:label_jump_to_a_project) }</option>" +
272 '<option value="" disabled="disabled">---</option>').html_safe
276 '<option value="" disabled="disabled">---</option>').html_safe
273
277
274 options << project_tree_options_for_select(projects, :selected => @project) do |p|
278 options << project_tree_options_for_select(projects, :selected => @project) do |p|
275 { :value => project_path(:id => p, :jump => current_menu_item) }
279 { :value => project_path(:id => p, :jump => current_menu_item) }
276 end
280 end
277
281
278 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
282 select_tag('project_quick_jump_box', options, :onchange => 'if (this.value != \'\') { window.location = this.value; }')
279 end
283 end
280 end
284 end
281
285
282 def project_tree_options_for_select(projects, options = {})
286 def project_tree_options_for_select(projects, options = {})
283 s = ''
287 s = ''
284 project_tree(projects) do |project, level|
288 project_tree(projects) do |project, level|
285 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
289 name_prefix = (level > 0 ? '&nbsp;' * 2 * level + '&#187; ' : '').html_safe
286 tag_options = {:value => project.id}
290 tag_options = {:value => project.id}
287 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
291 if project == options[:selected] || (options[:selected].respond_to?(:include?) && options[:selected].include?(project))
288 tag_options[:selected] = 'selected'
292 tag_options[:selected] = 'selected'
289 else
293 else
290 tag_options[:selected] = nil
294 tag_options[:selected] = nil
291 end
295 end
292 tag_options.merge!(yield(project)) if block_given?
296 tag_options.merge!(yield(project)) if block_given?
293 s << content_tag('option', name_prefix + h(project), tag_options)
297 s << content_tag('option', name_prefix + h(project), tag_options)
294 end
298 end
295 s.html_safe
299 s.html_safe
296 end
300 end
297
301
298 # Yields the given block for each project with its level in the tree
302 # Yields the given block for each project with its level in the tree
299 #
303 #
300 # Wrapper for Project#project_tree
304 # Wrapper for Project#project_tree
301 def project_tree(projects, &block)
305 def project_tree(projects, &block)
302 Project.project_tree(projects, &block)
306 Project.project_tree(projects, &block)
303 end
307 end
304
308
305 def principals_check_box_tags(name, principals)
309 def principals_check_box_tags(name, principals)
306 s = ''
310 s = ''
307 principals.sort.each do |principal|
311 principals.sort.each do |principal|
308 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
312 s << "<label>#{ check_box_tag name, principal.id, false } #{h principal}</label>\n"
309 end
313 end
310 s.html_safe
314 s.html_safe
311 end
315 end
312
316
313 # Returns a string for users/groups option tags
317 # Returns a string for users/groups option tags
314 def principals_options_for_select(collection, selected=nil)
318 def principals_options_for_select(collection, selected=nil)
315 s = ''
319 s = ''
316 if collection.include?(User.current)
320 if collection.include?(User.current)
317 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
321 s << content_tag('option', "<< #{l(:label_me)} >>", :value => User.current.id)
318 end
322 end
319 groups = ''
323 groups = ''
320 collection.sort.each do |element|
324 collection.sort.each do |element|
321 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
325 selected_attribute = ' selected="selected"' if option_value_selected?(element, selected)
322 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
326 (element.is_a?(Group) ? groups : s) << %(<option value="#{element.id}"#{selected_attribute}>#{h element.name}</option>)
323 end
327 end
324 unless groups.empty?
328 unless groups.empty?
325 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
329 s << %(<optgroup label="#{h(l(:label_group_plural))}">#{groups}</optgroup>)
326 end
330 end
327 s.html_safe
331 s.html_safe
328 end
332 end
329
333
330 # Truncates and returns the string as a single line
334 # Truncates and returns the string as a single line
331 def truncate_single_line(string, *args)
335 def truncate_single_line(string, *args)
332 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
336 truncate(string.to_s, *args).gsub(%r{[\r\n]+}m, ' ')
333 end
337 end
334
338
335 # Truncates at line break after 250 characters or options[:length]
339 # Truncates at line break after 250 characters or options[:length]
336 def truncate_lines(string, options={})
340 def truncate_lines(string, options={})
337 length = options[:length] || 250
341 length = options[:length] || 250
338 if string.to_s =~ /\A(.{#{length}}.*?)$/m
342 if string.to_s =~ /\A(.{#{length}}.*?)$/m
339 "#{$1}..."
343 "#{$1}..."
340 else
344 else
341 string
345 string
342 end
346 end
343 end
347 end
344
348
345 def anchor(text)
349 def anchor(text)
346 text.to_s.gsub(' ', '_')
350 text.to_s.gsub(' ', '_')
347 end
351 end
348
352
349 def html_hours(text)
353 def html_hours(text)
350 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
354 text.gsub(%r{(\d+)\.(\d+)}, '<span class="hours hours-int">\1</span><span class="hours hours-dec">.\2</span>').html_safe
351 end
355 end
352
356
353 def authoring(created, author, options={})
357 def authoring(created, author, options={})
354 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
358 l(options[:label] || :label_added_time_by, :author => link_to_user(author), :age => time_tag(created)).html_safe
355 end
359 end
356
360
357 def time_tag(time)
361 def time_tag(time)
358 text = distance_of_time_in_words(Time.now, time)
362 text = distance_of_time_in_words(Time.now, time)
359 if @project
363 if @project
360 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
364 link_to(text, {:controller => 'activities', :action => 'index', :id => @project, :from => User.current.time_to_date(time)}, :title => format_time(time))
361 else
365 else
362 content_tag('acronym', text, :title => format_time(time))
366 content_tag('acronym', text, :title => format_time(time))
363 end
367 end
364 end
368 end
365
369
366 def syntax_highlight_lines(name, content)
370 def syntax_highlight_lines(name, content)
367 lines = []
371 lines = []
368 syntax_highlight(name, content).each_line { |line| lines << line }
372 syntax_highlight(name, content).each_line { |line| lines << line }
369 lines
373 lines
370 end
374 end
371
375
372 def syntax_highlight(name, content)
376 def syntax_highlight(name, content)
373 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
377 Redmine::SyntaxHighlighting.highlight_by_filename(content, name)
374 end
378 end
375
379
376 def to_path_param(path)
380 def to_path_param(path)
377 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
381 str = path.to_s.split(%r{[/\\]}).select{|p| !p.blank?}.join("/")
378 str.blank? ? nil : str
382 str.blank? ? nil : str
379 end
383 end
380
384
381 def pagination_links_full(paginator, count=nil, options={})
385 def pagination_links_full(paginator, count=nil, options={})
382 page_param = options.delete(:page_param) || :page
386 page_param = options.delete(:page_param) || :page
383 per_page_links = options.delete(:per_page_links)
387 per_page_links = options.delete(:per_page_links)
384 url_param = params.dup
388 url_param = params.dup
385
389
386 html = ''
390 html = ''
387 if paginator.current.previous
391 if paginator.current.previous
388 # \xc2\xab(utf-8) = &#171;
392 # \xc2\xab(utf-8) = &#171;
389 html << link_to_content_update(
393 html << link_to_content_update(
390 "\xc2\xab " + l(:label_previous),
394 "\xc2\xab " + l(:label_previous),
391 url_param.merge(page_param => paginator.current.previous)) + ' '
395 url_param.merge(page_param => paginator.current.previous)) + ' '
392 end
396 end
393
397
394 html << (pagination_links_each(paginator, options) do |n|
398 html << (pagination_links_each(paginator, options) do |n|
395 link_to_content_update(n.to_s, url_param.merge(page_param => n))
399 link_to_content_update(n.to_s, url_param.merge(page_param => n))
396 end || '')
400 end || '')
397
401
398 if paginator.current.next
402 if paginator.current.next
399 # \xc2\xbb(utf-8) = &#187;
403 # \xc2\xbb(utf-8) = &#187;
400 html << ' ' + link_to_content_update(
404 html << ' ' + link_to_content_update(
401 (l(:label_next) + " \xc2\xbb"),
405 (l(:label_next) + " \xc2\xbb"),
402 url_param.merge(page_param => paginator.current.next))
406 url_param.merge(page_param => paginator.current.next))
403 end
407 end
404
408
405 unless count.nil?
409 unless count.nil?
406 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
410 html << " (#{paginator.current.first_item}-#{paginator.current.last_item}/#{count})"
407 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
411 if per_page_links != false && links = per_page_links(paginator.items_per_page, count)
408 html << " | #{links}"
412 html << " | #{links}"
409 end
413 end
410 end
414 end
411
415
412 html.html_safe
416 html.html_safe
413 end
417 end
414
418
415 def per_page_links(selected=nil, item_count=nil)
419 def per_page_links(selected=nil, item_count=nil)
416 values = Setting.per_page_options_array
420 values = Setting.per_page_options_array
417 if item_count && values.any?
421 if item_count && values.any?
418 if item_count > values.first
422 if item_count > values.first
419 max = values.detect {|value| value >= item_count} || item_count
423 max = values.detect {|value| value >= item_count} || item_count
420 else
424 else
421 max = item_count
425 max = item_count
422 end
426 end
423 values = values.select {|value| value <= max || value == selected}
427 values = values.select {|value| value <= max || value == selected}
424 end
428 end
425 if values.empty? || (values.size == 1 && values.first == selected)
429 if values.empty? || (values.size == 1 && values.first == selected)
426 return nil
430 return nil
427 end
431 end
428 links = values.collect do |n|
432 links = values.collect do |n|
429 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
433 n == selected ? n : link_to_content_update(n, params.merge(:per_page => n))
430 end
434 end
431 l(:label_display_per_page, links.join(', '))
435 l(:label_display_per_page, links.join(', '))
432 end
436 end
433
437
434 def reorder_links(name, url, method = :post)
438 def reorder_links(name, url, method = :post)
435 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
439 link_to(image_tag('2uparrow.png', :alt => l(:label_sort_highest)),
436 url.merge({"#{name}[move_to]" => 'highest'}),
440 url.merge({"#{name}[move_to]" => 'highest'}),
437 :method => method, :title => l(:label_sort_highest)) +
441 :method => method, :title => l(:label_sort_highest)) +
438 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
442 link_to(image_tag('1uparrow.png', :alt => l(:label_sort_higher)),
439 url.merge({"#{name}[move_to]" => 'higher'}),
443 url.merge({"#{name}[move_to]" => 'higher'}),
440 :method => method, :title => l(:label_sort_higher)) +
444 :method => method, :title => l(:label_sort_higher)) +
441 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
445 link_to(image_tag('1downarrow.png', :alt => l(:label_sort_lower)),
442 url.merge({"#{name}[move_to]" => 'lower'}),
446 url.merge({"#{name}[move_to]" => 'lower'}),
443 :method => method, :title => l(:label_sort_lower)) +
447 :method => method, :title => l(:label_sort_lower)) +
444 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
448 link_to(image_tag('2downarrow.png', :alt => l(:label_sort_lowest)),
445 url.merge({"#{name}[move_to]" => 'lowest'}),
449 url.merge({"#{name}[move_to]" => 'lowest'}),
446 :method => method, :title => l(:label_sort_lowest))
450 :method => method, :title => l(:label_sort_lowest))
447 end
451 end
448
452
449 def breadcrumb(*args)
453 def breadcrumb(*args)
450 elements = args.flatten
454 elements = args.flatten
451 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
455 elements.any? ? content_tag('p', (args.join(" \xc2\xbb ") + " \xc2\xbb ").html_safe, :class => 'breadcrumb') : nil
452 end
456 end
453
457
454 def other_formats_links(&block)
458 def other_formats_links(&block)
455 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
459 concat('<p class="other-formats">'.html_safe + l(:label_export_to))
456 yield Redmine::Views::OtherFormatsBuilder.new(self)
460 yield Redmine::Views::OtherFormatsBuilder.new(self)
457 concat('</p>'.html_safe)
461 concat('</p>'.html_safe)
458 end
462 end
459
463
460 def page_header_title
464 def page_header_title
461 if @project.nil? || @project.new_record?
465 if @project.nil? || @project.new_record?
462 h(Setting.app_title)
466 h(Setting.app_title)
463 else
467 else
464 b = []
468 b = []
465 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
469 ancestors = (@project.root? ? [] : @project.ancestors.visible.all)
466 if ancestors.any?
470 if ancestors.any?
467 root = ancestors.shift
471 root = ancestors.shift
468 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
472 b << link_to_project(root, {:jump => current_menu_item}, :class => 'root')
469 if ancestors.size > 2
473 if ancestors.size > 2
470 b << "\xe2\x80\xa6"
474 b << "\xe2\x80\xa6"
471 ancestors = ancestors[-2, 2]
475 ancestors = ancestors[-2, 2]
472 end
476 end
473 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
477 b += ancestors.collect {|p| link_to_project(p, {:jump => current_menu_item}, :class => 'ancestor') }
474 end
478 end
475 b << h(@project)
479 b << h(@project)
476 b.join(" \xc2\xbb ").html_safe
480 b.join(" \xc2\xbb ").html_safe
477 end
481 end
478 end
482 end
479
483
480 def html_title(*args)
484 def html_title(*args)
481 if args.empty?
485 if args.empty?
482 title = @html_title || []
486 title = @html_title || []
483 title << @project.name if @project
487 title << @project.name if @project
484 title << Setting.app_title unless Setting.app_title == title.last
488 title << Setting.app_title unless Setting.app_title == title.last
485 title.select {|t| !t.blank? }.join(' - ')
489 title.select {|t| !t.blank? }.join(' - ')
486 else
490 else
487 @html_title ||= []
491 @html_title ||= []
488 @html_title += args
492 @html_title += args
489 end
493 end
490 end
494 end
491
495
492 # Returns the theme, controller name, and action as css classes for the
496 # Returns the theme, controller name, and action as css classes for the
493 # HTML body.
497 # HTML body.
494 def body_css_classes
498 def body_css_classes
495 css = []
499 css = []
496 if theme = Redmine::Themes.theme(Setting.ui_theme)
500 if theme = Redmine::Themes.theme(Setting.ui_theme)
497 css << 'theme-' + theme.name
501 css << 'theme-' + theme.name
498 end
502 end
499
503
500 css << 'controller-' + controller_name
504 css << 'controller-' + controller_name
501 css << 'action-' + action_name
505 css << 'action-' + action_name
502 css.join(' ')
506 css.join(' ')
503 end
507 end
504
508
505 def accesskey(s)
509 def accesskey(s)
506 Redmine::AccessKeys.key_for s
510 Redmine::AccessKeys.key_for s
507 end
511 end
508
512
509 # Formats text according to system settings.
513 # Formats text according to system settings.
510 # 2 ways to call this method:
514 # 2 ways to call this method:
511 # * with a String: textilizable(text, options)
515 # * with a String: textilizable(text, options)
512 # * with an object and one of its attribute: textilizable(issue, :description, options)
516 # * with an object and one of its attribute: textilizable(issue, :description, options)
513 def textilizable(*args)
517 def textilizable(*args)
514 options = args.last.is_a?(Hash) ? args.pop : {}
518 options = args.last.is_a?(Hash) ? args.pop : {}
515 case args.size
519 case args.size
516 when 1
520 when 1
517 obj = options[:object]
521 obj = options[:object]
518 text = args.shift
522 text = args.shift
519 when 2
523 when 2
520 obj = args.shift
524 obj = args.shift
521 attr = args.shift
525 attr = args.shift
522 text = obj.send(attr).to_s
526 text = obj.send(attr).to_s
523 else
527 else
524 raise ArgumentError, 'invalid arguments to textilizable'
528 raise ArgumentError, 'invalid arguments to textilizable'
525 end
529 end
526 return '' if text.blank?
530 return '' if text.blank?
527 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
531 project = options[:project] || @project || (obj && obj.respond_to?(:project) ? obj.project : nil)
528 only_path = options.delete(:only_path) == false ? false : true
532 only_path = options.delete(:only_path) == false ? false : true
529
533
530 text = text.dup
534 text = text.dup
531 macros = catch_macros(text)
535 macros = catch_macros(text)
532 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
536 text = Redmine::WikiFormatting.to_html(Setting.text_formatting, text, :object => obj, :attribute => attr)
533
537
534 @parsed_headings = []
538 @parsed_headings = []
535 @heading_anchors = {}
539 @heading_anchors = {}
536 @current_section = 0 if options[:edit_section_links]
540 @current_section = 0 if options[:edit_section_links]
537
541
538 parse_sections(text, project, obj, attr, only_path, options)
542 parse_sections(text, project, obj, attr, only_path, options)
539 text = parse_non_pre_blocks(text, obj, macros) do |text|
543 text = parse_non_pre_blocks(text, obj, macros) do |text|
540 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
544 [:parse_inline_attachments, :parse_wiki_links, :parse_redmine_links].each do |method_name|
541 send method_name, text, project, obj, attr, only_path, options
545 send method_name, text, project, obj, attr, only_path, options
542 end
546 end
543 end
547 end
544 parse_headings(text, project, obj, attr, only_path, options)
548 parse_headings(text, project, obj, attr, only_path, options)
545
549
546 if @parsed_headings.any?
550 if @parsed_headings.any?
547 replace_toc(text, @parsed_headings)
551 replace_toc(text, @parsed_headings)
548 end
552 end
549
553
550 text.html_safe
554 text.html_safe
551 end
555 end
552
556
553 def parse_non_pre_blocks(text, obj, macros)
557 def parse_non_pre_blocks(text, obj, macros)
554 s = StringScanner.new(text)
558 s = StringScanner.new(text)
555 tags = []
559 tags = []
556 parsed = ''
560 parsed = ''
557 while !s.eos?
561 while !s.eos?
558 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
562 s.scan(/(.*?)(<(\/)?(pre|code)(.*?)>|\z)/im)
559 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
563 text, full_tag, closing, tag = s[1], s[2], s[3], s[4]
560 if tags.empty?
564 if tags.empty?
561 yield text
565 yield text
562 inject_macros(text, obj, macros) if macros.any?
566 inject_macros(text, obj, macros) if macros.any?
563 else
567 else
564 inject_macros(text, obj, macros, false) if macros.any?
568 inject_macros(text, obj, macros, false) if macros.any?
565 end
569 end
566 parsed << text
570 parsed << text
567 if tag
571 if tag
568 if closing
572 if closing
569 if tags.last == tag.downcase
573 if tags.last == tag.downcase
570 tags.pop
574 tags.pop
571 end
575 end
572 else
576 else
573 tags << tag.downcase
577 tags << tag.downcase
574 end
578 end
575 parsed << full_tag
579 parsed << full_tag
576 end
580 end
577 end
581 end
578 # Close any non closing tags
582 # Close any non closing tags
579 while tag = tags.pop
583 while tag = tags.pop
580 parsed << "</#{tag}>"
584 parsed << "</#{tag}>"
581 end
585 end
582 parsed
586 parsed
583 end
587 end
584
588
585 def parse_inline_attachments(text, project, obj, attr, only_path, options)
589 def parse_inline_attachments(text, project, obj, attr, only_path, options)
586 # when using an image link, try to use an attachment, if possible
590 # when using an image link, try to use an attachment, if possible
587 if options[:attachments] || (obj && obj.respond_to?(:attachments))
591 if options[:attachments] || (obj && obj.respond_to?(:attachments))
588 attachments = options[:attachments] || obj.attachments
592 attachments = options[:attachments] || obj.attachments
589 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
593 text.gsub!(/src="([^\/"]+\.(bmp|gif|jpg|jpe|jpeg|png))"(\s+alt="([^"]*)")?/i) do |m|
590 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
594 filename, ext, alt, alttext = $1.downcase, $2, $3, $4
591 # search for the picture in attachments
595 # search for the picture in attachments
592 if found = Attachment.latest_attach(attachments, filename)
596 if found = Attachment.latest_attach(attachments, filename)
593 image_url = url_for :only_path => only_path, :controller => 'attachments',
597 image_url = url_for :only_path => only_path, :controller => 'attachments',
594 :action => 'download', :id => found
598 :action => 'download', :id => found
595 desc = found.description.to_s.gsub('"', '')
599 desc = found.description.to_s.gsub('"', '')
596 if !desc.blank? && alttext.blank?
600 if !desc.blank? && alttext.blank?
597 alt = " title=\"#{desc}\" alt=\"#{desc}\""
601 alt = " title=\"#{desc}\" alt=\"#{desc}\""
598 end
602 end
599 "src=\"#{image_url}\"#{alt}"
603 "src=\"#{image_url}\"#{alt}"
600 else
604 else
601 m
605 m
602 end
606 end
603 end
607 end
604 end
608 end
605 end
609 end
606
610
607 # Wiki links
611 # Wiki links
608 #
612 #
609 # Examples:
613 # Examples:
610 # [[mypage]]
614 # [[mypage]]
611 # [[mypage|mytext]]
615 # [[mypage|mytext]]
612 # wiki links can refer other project wikis, using project name or identifier:
616 # wiki links can refer other project wikis, using project name or identifier:
613 # [[project:]] -> wiki starting page
617 # [[project:]] -> wiki starting page
614 # [[project:|mytext]]
618 # [[project:|mytext]]
615 # [[project:mypage]]
619 # [[project:mypage]]
616 # [[project:mypage|mytext]]
620 # [[project:mypage|mytext]]
617 def parse_wiki_links(text, project, obj, attr, only_path, options)
621 def parse_wiki_links(text, project, obj, attr, only_path, options)
618 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
622 text.gsub!(/(!)?(\[\[([^\]\n\|]+)(\|([^\]\n\|]+))?\]\])/) do |m|
619 link_project = project
623 link_project = project
620 esc, all, page, title = $1, $2, $3, $5
624 esc, all, page, title = $1, $2, $3, $5
621 if esc.nil?
625 if esc.nil?
622 if page =~ /^([^\:]+)\:(.*)$/
626 if page =~ /^([^\:]+)\:(.*)$/
623 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
627 link_project = Project.find_by_identifier($1) || Project.find_by_name($1)
624 page = $2
628 page = $2
625 title ||= $1 if page.blank?
629 title ||= $1 if page.blank?
626 end
630 end
627
631
628 if link_project && link_project.wiki
632 if link_project && link_project.wiki
629 # extract anchor
633 # extract anchor
630 anchor = nil
634 anchor = nil
631 if page =~ /^(.+?)\#(.+)$/
635 if page =~ /^(.+?)\#(.+)$/
632 page, anchor = $1, $2
636 page, anchor = $1, $2
633 end
637 end
634 anchor = sanitize_anchor_name(anchor) if anchor.present?
638 anchor = sanitize_anchor_name(anchor) if anchor.present?
635 # check if page exists
639 # check if page exists
636 wiki_page = link_project.wiki.find_page(page)
640 wiki_page = link_project.wiki.find_page(page)
637 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
641 url = if anchor.present? && wiki_page.present? && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version)) && obj.page == wiki_page
638 "##{anchor}"
642 "##{anchor}"
639 else
643 else
640 case options[:wiki_links]
644 case options[:wiki_links]
641 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
645 when :local; "#{page.present? ? Wiki.titleize(page) : ''}.html" + (anchor.present? ? "##{anchor}" : '')
642 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
646 when :anchor; "##{page.present? ? Wiki.titleize(page) : title}" + (anchor.present? ? "_#{anchor}" : '') # used for single-file wiki export
643 else
647 else
644 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
648 wiki_page_id = page.present? ? Wiki.titleize(page) : nil
645 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
649 parent = wiki_page.nil? && obj.is_a?(WikiContent) && obj.page && project == link_project ? obj.page.title : nil
646 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
650 url_for(:only_path => only_path, :controller => 'wiki', :action => 'show', :project_id => link_project,
647 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
651 :id => wiki_page_id, :version => nil, :anchor => anchor, :parent => parent)
648 end
652 end
649 end
653 end
650 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
654 link_to(title.present? ? title.html_safe : h(page), url, :class => ('wiki-page' + (wiki_page ? '' : ' new')))
651 else
655 else
652 # project or wiki doesn't exist
656 # project or wiki doesn't exist
653 all
657 all
654 end
658 end
655 else
659 else
656 all
660 all
657 end
661 end
658 end
662 end
659 end
663 end
660
664
661 # Redmine links
665 # Redmine links
662 #
666 #
663 # Examples:
667 # Examples:
664 # Issues:
668 # Issues:
665 # #52 -> Link to issue #52
669 # #52 -> Link to issue #52
666 # Changesets:
670 # Changesets:
667 # r52 -> Link to revision 52
671 # r52 -> Link to revision 52
668 # commit:a85130f -> Link to scmid starting with a85130f
672 # commit:a85130f -> Link to scmid starting with a85130f
669 # Documents:
673 # Documents:
670 # document#17 -> Link to document with id 17
674 # document#17 -> Link to document with id 17
671 # document:Greetings -> Link to the document with title "Greetings"
675 # document:Greetings -> Link to the document with title "Greetings"
672 # document:"Some document" -> Link to the document with title "Some document"
676 # document:"Some document" -> Link to the document with title "Some document"
673 # Versions:
677 # Versions:
674 # version#3 -> Link to version with id 3
678 # version#3 -> Link to version with id 3
675 # version:1.0.0 -> Link to version named "1.0.0"
679 # version:1.0.0 -> Link to version named "1.0.0"
676 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
680 # version:"1.0 beta 2" -> Link to version named "1.0 beta 2"
677 # Attachments:
681 # Attachments:
678 # attachment:file.zip -> Link to the attachment of the current object named file.zip
682 # attachment:file.zip -> Link to the attachment of the current object named file.zip
679 # Source files:
683 # Source files:
680 # source:some/file -> Link to the file located at /some/file in the project's repository
684 # source:some/file -> Link to the file located at /some/file in the project's repository
681 # source:some/file@52 -> Link to the file's revision 52
685 # source:some/file@52 -> Link to the file's revision 52
682 # source:some/file#L120 -> Link to line 120 of the file
686 # source:some/file#L120 -> Link to line 120 of the file
683 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
687 # source:some/file@52#L120 -> Link to line 120 of the file's revision 52
684 # export:some/file -> Force the download of the file
688 # export:some/file -> Force the download of the file
685 # Forum messages:
689 # Forum messages:
686 # message#1218 -> Link to message with id 1218
690 # message#1218 -> Link to message with id 1218
687 #
691 #
688 # Links can refer other objects from other projects, using project identifier:
692 # Links can refer other objects from other projects, using project identifier:
689 # identifier:r52
693 # identifier:r52
690 # identifier:document:"Some document"
694 # identifier:document:"Some document"
691 # identifier:version:1.0.0
695 # identifier:version:1.0.0
692 # identifier:source:some/file
696 # identifier:source:some/file
693 def parse_redmine_links(text, project, obj, attr, only_path, options)
697 def parse_redmine_links(text, project, obj, attr, only_path, options)
694 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
698 text.gsub!(%r{([\s\(,\-\[\>]|^)(!)?(([a-z0-9\-_]+):)?(attachment|document|version|forum|news|message|project|commit|source|export)?(((#)|((([a-z0-9\-]+)\|)?(r)))((\d+)((#note)?-(\d+))?)|(:)([^"\s<>][^\s<>]*?|"[^"]+?"))(?=(?=[[:punct:]][^A-Za-z0-9_/])|,|\s|\]|<|$)}) do |m|
695 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
699 leading, esc, project_prefix, project_identifier, prefix, repo_prefix, repo_identifier, sep, identifier, comment_suffix, comment_id = $1, $2, $3, $4, $5, $10, $11, $8 || $12 || $18, $14 || $19, $15, $17
696 link = nil
700 link = nil
697 if project_identifier
701 if project_identifier
698 project = Project.visible.find_by_identifier(project_identifier)
702 project = Project.visible.find_by_identifier(project_identifier)
699 end
703 end
700 if esc.nil?
704 if esc.nil?
701 if prefix.nil? && sep == 'r'
705 if prefix.nil? && sep == 'r'
702 if project
706 if project
703 repository = nil
707 repository = nil
704 if repo_identifier
708 if repo_identifier
705 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
709 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
706 else
710 else
707 repository = project.repository
711 repository = project.repository
708 end
712 end
709 # project.changesets.visible raises an SQL error because of a double join on repositories
713 # project.changesets.visible raises an SQL error because of a double join on repositories
710 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
714 if repository && (changeset = Changeset.visible.find_by_repository_id_and_revision(repository.id, identifier))
711 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
715 link = link_to(h("#{project_prefix}#{repo_prefix}r#{identifier}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.revision},
712 :class => 'changeset',
716 :class => 'changeset',
713 :title => truncate_single_line(changeset.comments, :length => 100))
717 :title => truncate_single_line(changeset.comments, :length => 100))
714 end
718 end
715 end
719 end
716 elsif sep == '#'
720 elsif sep == '#'
717 oid = identifier.to_i
721 oid = identifier.to_i
718 case prefix
722 case prefix
719 when nil
723 when nil
720 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
724 if oid.to_s == identifier && issue = Issue.visible.find_by_id(oid, :include => :status)
721 anchor = comment_id ? "note-#{comment_id}" : nil
725 anchor = comment_id ? "note-#{comment_id}" : nil
722 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
726 link = link_to("##{oid}", {:only_path => only_path, :controller => 'issues', :action => 'show', :id => oid, :anchor => anchor},
723 :class => issue.css_classes,
727 :class => issue.css_classes,
724 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
728 :title => "#{truncate(issue.subject, :length => 100)} (#{issue.status.name})")
725 end
729 end
726 when 'document'
730 when 'document'
727 if document = Document.visible.find_by_id(oid)
731 if document = Document.visible.find_by_id(oid)
728 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
732 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
729 :class => 'document'
733 :class => 'document'
730 end
734 end
731 when 'version'
735 when 'version'
732 if version = Version.visible.find_by_id(oid)
736 if version = Version.visible.find_by_id(oid)
733 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
737 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
734 :class => 'version'
738 :class => 'version'
735 end
739 end
736 when 'message'
740 when 'message'
737 if message = Message.visible.find_by_id(oid, :include => :parent)
741 if message = Message.visible.find_by_id(oid, :include => :parent)
738 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
742 link = link_to_message(message, {:only_path => only_path}, :class => 'message')
739 end
743 end
740 when 'forum'
744 when 'forum'
741 if board = Board.visible.find_by_id(oid)
745 if board = Board.visible.find_by_id(oid)
742 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
746 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
743 :class => 'board'
747 :class => 'board'
744 end
748 end
745 when 'news'
749 when 'news'
746 if news = News.visible.find_by_id(oid)
750 if news = News.visible.find_by_id(oid)
747 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
751 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
748 :class => 'news'
752 :class => 'news'
749 end
753 end
750 when 'project'
754 when 'project'
751 if p = Project.visible.find_by_id(oid)
755 if p = Project.visible.find_by_id(oid)
752 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
756 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
753 end
757 end
754 end
758 end
755 elsif sep == ':'
759 elsif sep == ':'
756 # removes the double quotes if any
760 # removes the double quotes if any
757 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
761 name = identifier.gsub(%r{^"(.*)"$}, "\\1")
758 case prefix
762 case prefix
759 when 'document'
763 when 'document'
760 if project && document = project.documents.visible.find_by_title(name)
764 if project && document = project.documents.visible.find_by_title(name)
761 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
765 link = link_to h(document.title), {:only_path => only_path, :controller => 'documents', :action => 'show', :id => document},
762 :class => 'document'
766 :class => 'document'
763 end
767 end
764 when 'version'
768 when 'version'
765 if project && version = project.versions.visible.find_by_name(name)
769 if project && version = project.versions.visible.find_by_name(name)
766 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
770 link = link_to h(version.name), {:only_path => only_path, :controller => 'versions', :action => 'show', :id => version},
767 :class => 'version'
771 :class => 'version'
768 end
772 end
769 when 'forum'
773 when 'forum'
770 if project && board = project.boards.visible.find_by_name(name)
774 if project && board = project.boards.visible.find_by_name(name)
771 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
775 link = link_to h(board.name), {:only_path => only_path, :controller => 'boards', :action => 'show', :id => board, :project_id => board.project},
772 :class => 'board'
776 :class => 'board'
773 end
777 end
774 when 'news'
778 when 'news'
775 if project && news = project.news.visible.find_by_title(name)
779 if project && news = project.news.visible.find_by_title(name)
776 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
780 link = link_to h(news.title), {:only_path => only_path, :controller => 'news', :action => 'show', :id => news},
777 :class => 'news'
781 :class => 'news'
778 end
782 end
779 when 'commit', 'source', 'export'
783 when 'commit', 'source', 'export'
780 if project
784 if project
781 repository = nil
785 repository = nil
782 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
786 if name =~ %r{^(([a-z0-9\-]+)\|)(.+)$}
783 repo_prefix, repo_identifier, name = $1, $2, $3
787 repo_prefix, repo_identifier, name = $1, $2, $3
784 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
788 repository = project.repositories.detect {|repo| repo.identifier == repo_identifier}
785 else
789 else
786 repository = project.repository
790 repository = project.repository
787 end
791 end
788 if prefix == 'commit'
792 if prefix == 'commit'
789 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
793 if repository && (changeset = Changeset.visible.find(:first, :conditions => ["repository_id = ? AND scmid LIKE ?", repository.id, "#{name}%"]))
790 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
794 link = link_to h("#{project_prefix}#{repo_prefix}#{name}"), {:only_path => only_path, :controller => 'repositories', :action => 'revision', :id => project, :repository_id => repository.identifier_param, :rev => changeset.identifier},
791 :class => 'changeset',
795 :class => 'changeset',
792 :title => truncate_single_line(h(changeset.comments), :length => 100)
796 :title => truncate_single_line(h(changeset.comments), :length => 100)
793 end
797 end
794 else
798 else
795 if repository && User.current.allowed_to?(:browse_repository, project)
799 if repository && User.current.allowed_to?(:browse_repository, project)
796 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
800 name =~ %r{^[/\\]*(.*?)(@([0-9a-f]+))?(#(L\d+))?$}
797 path, rev, anchor = $1, $3, $5
801 path, rev, anchor = $1, $3, $5
798 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
802 link = link_to h("#{project_prefix}#{prefix}:#{repo_prefix}#{name}"), {:controller => 'repositories', :action => (prefix == 'export' ? 'raw' : 'entry'), :id => project, :repository_id => repository.identifier_param,
799 :path => to_path_param(path),
803 :path => to_path_param(path),
800 :rev => rev,
804 :rev => rev,
801 :anchor => anchor},
805 :anchor => anchor},
802 :class => (prefix == 'export' ? 'source download' : 'source')
806 :class => (prefix == 'export' ? 'source download' : 'source')
803 end
807 end
804 end
808 end
805 repo_prefix = nil
809 repo_prefix = nil
806 end
810 end
807 when 'attachment'
811 when 'attachment'
808 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
812 attachments = options[:attachments] || (obj && obj.respond_to?(:attachments) ? obj.attachments : nil)
809 if attachments && attachment = attachments.detect {|a| a.filename == name }
813 if attachments && attachment = attachments.detect {|a| a.filename == name }
810 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
814 link = link_to h(attachment.filename), {:only_path => only_path, :controller => 'attachments', :action => 'download', :id => attachment},
811 :class => 'attachment'
815 :class => 'attachment'
812 end
816 end
813 when 'project'
817 when 'project'
814 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
818 if p = Project.visible.find(:first, :conditions => ["identifier = :s OR LOWER(name) = :s", {:s => name.downcase}])
815 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
819 link = link_to_project(p, {:only_path => only_path}, :class => 'project')
816 end
820 end
817 end
821 end
818 end
822 end
819 end
823 end
820 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
824 (leading + (link || "#{project_prefix}#{prefix}#{repo_prefix}#{sep}#{identifier}#{comment_suffix}"))
821 end
825 end
822 end
826 end
823
827
824 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
828 HEADING_RE = /(<h(\d)( [^>]+)?>(.+?)<\/h(\d)>)/i unless const_defined?(:HEADING_RE)
825
829
826 def parse_sections(text, project, obj, attr, only_path, options)
830 def parse_sections(text, project, obj, attr, only_path, options)
827 return unless options[:edit_section_links]
831 return unless options[:edit_section_links]
828 text.gsub!(HEADING_RE) do
832 text.gsub!(HEADING_RE) do
829 heading = $1
833 heading = $1
830 @current_section += 1
834 @current_section += 1
831 if @current_section > 1
835 if @current_section > 1
832 content_tag('div',
836 content_tag('div',
833 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
837 link_to(image_tag('edit.png'), options[:edit_section_links].merge(:section => @current_section)),
834 :class => 'contextual',
838 :class => 'contextual',
835 :title => l(:button_edit_section)) + heading.html_safe
839 :title => l(:button_edit_section)) + heading.html_safe
836 else
840 else
837 heading
841 heading
838 end
842 end
839 end
843 end
840 end
844 end
841
845
842 # Headings and TOC
846 # Headings and TOC
843 # Adds ids and links to headings unless options[:headings] is set to false
847 # Adds ids and links to headings unless options[:headings] is set to false
844 def parse_headings(text, project, obj, attr, only_path, options)
848 def parse_headings(text, project, obj, attr, only_path, options)
845 return if options[:headings] == false
849 return if options[:headings] == false
846
850
847 text.gsub!(HEADING_RE) do
851 text.gsub!(HEADING_RE) do
848 level, attrs, content = $2.to_i, $3, $4
852 level, attrs, content = $2.to_i, $3, $4
849 item = strip_tags(content).strip
853 item = strip_tags(content).strip
850 anchor = sanitize_anchor_name(item)
854 anchor = sanitize_anchor_name(item)
851 # used for single-file wiki export
855 # used for single-file wiki export
852 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
856 anchor = "#{obj.page.title}_#{anchor}" if options[:wiki_links] == :anchor && (obj.is_a?(WikiContent) || obj.is_a?(WikiContent::Version))
853 @heading_anchors[anchor] ||= 0
857 @heading_anchors[anchor] ||= 0
854 idx = (@heading_anchors[anchor] += 1)
858 idx = (@heading_anchors[anchor] += 1)
855 if idx > 1
859 if idx > 1
856 anchor = "#{anchor}-#{idx}"
860 anchor = "#{anchor}-#{idx}"
857 end
861 end
858 @parsed_headings << [level, anchor, item]
862 @parsed_headings << [level, anchor, item]
859 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
863 "<a name=\"#{anchor}\"></a>\n<h#{level} #{attrs}>#{content}<a href=\"##{anchor}\" class=\"wiki-anchor\">&para;</a></h#{level}>"
860 end
864 end
861 end
865 end
862
866
863 MACROS_RE = /(
867 MACROS_RE = /(
864 (!)? # escaping
868 (!)? # escaping
865 (
869 (
866 \{\{ # opening tag
870 \{\{ # opening tag
867 ([\w]+) # macro name
871 ([\w]+) # macro name
868 (\(([^\n\r]*?)\))? # optional arguments
872 (\(([^\n\r]*?)\))? # optional arguments
869 ([\n\r].*?[\n\r])? # optional block of text
873 ([\n\r].*?[\n\r])? # optional block of text
870 \}\} # closing tag
874 \}\} # closing tag
871 )
875 )
872 )/mx unless const_defined?(:MACROS_RE)
876 )/mx unless const_defined?(:MACROS_RE)
873
877
874 MACRO_SUB_RE = /(
878 MACRO_SUB_RE = /(
875 \{\{
879 \{\{
876 macro\((\d+)\)
880 macro\((\d+)\)
877 \}\}
881 \}\}
878 )/x unless const_defined?(:MACRO_SUB_RE)
882 )/x unless const_defined?(:MACRO_SUB_RE)
879
883
880 # Extracts macros from text
884 # Extracts macros from text
881 def catch_macros(text)
885 def catch_macros(text)
882 macros = {}
886 macros = {}
883 text.gsub!(MACROS_RE) do
887 text.gsub!(MACROS_RE) do
884 all, macro = $1, $4.downcase
888 all, macro = $1, $4.downcase
885 if macro_exists?(macro) || all =~ MACRO_SUB_RE
889 if macro_exists?(macro) || all =~ MACRO_SUB_RE
886 index = macros.size
890 index = macros.size
887 macros[index] = all
891 macros[index] = all
888 "{{macro(#{index})}}"
892 "{{macro(#{index})}}"
889 else
893 else
890 all
894 all
891 end
895 end
892 end
896 end
893 macros
897 macros
894 end
898 end
895
899
896 # Executes and replaces macros in text
900 # Executes and replaces macros in text
897 def inject_macros(text, obj, macros, execute=true)
901 def inject_macros(text, obj, macros, execute=true)
898 text.gsub!(MACRO_SUB_RE) do
902 text.gsub!(MACRO_SUB_RE) do
899 all, index = $1, $2.to_i
903 all, index = $1, $2.to_i
900 orig = macros.delete(index)
904 orig = macros.delete(index)
901 if execute && orig && orig =~ MACROS_RE
905 if execute && orig && orig =~ MACROS_RE
902 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
906 esc, all, macro, args, block = $2, $3, $4.downcase, $6.to_s, $7.try(:strip)
903 if esc.nil?
907 if esc.nil?
904 h(exec_macro(macro, obj, args, block) || all)
908 h(exec_macro(macro, obj, args, block) || all)
905 else
909 else
906 h(all)
910 h(all)
907 end
911 end
908 elsif orig
912 elsif orig
909 h(orig)
913 h(orig)
910 else
914 else
911 h(all)
915 h(all)
912 end
916 end
913 end
917 end
914 end
918 end
915
919
916 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
920 TOC_RE = /<p>\{\{([<>]?)toc\}\}<\/p>/i unless const_defined?(:TOC_RE)
917
921
918 # Renders the TOC with given headings
922 # Renders the TOC with given headings
919 def replace_toc(text, headings)
923 def replace_toc(text, headings)
920 text.gsub!(TOC_RE) do
924 text.gsub!(TOC_RE) do
921 # Keep only the 4 first levels
925 # Keep only the 4 first levels
922 headings = headings.select{|level, anchor, item| level <= 4}
926 headings = headings.select{|level, anchor, item| level <= 4}
923 if headings.empty?
927 if headings.empty?
924 ''
928 ''
925 else
929 else
926 div_class = 'toc'
930 div_class = 'toc'
927 div_class << ' right' if $1 == '>'
931 div_class << ' right' if $1 == '>'
928 div_class << ' left' if $1 == '<'
932 div_class << ' left' if $1 == '<'
929 out = "<ul class=\"#{div_class}\"><li>"
933 out = "<ul class=\"#{div_class}\"><li>"
930 root = headings.map(&:first).min
934 root = headings.map(&:first).min
931 current = root
935 current = root
932 started = false
936 started = false
933 headings.each do |level, anchor, item|
937 headings.each do |level, anchor, item|
934 if level > current
938 if level > current
935 out << '<ul><li>' * (level - current)
939 out << '<ul><li>' * (level - current)
936 elsif level < current
940 elsif level < current
937 out << "</li></ul>\n" * (current - level) + "</li><li>"
941 out << "</li></ul>\n" * (current - level) + "</li><li>"
938 elsif started
942 elsif started
939 out << '</li><li>'
943 out << '</li><li>'
940 end
944 end
941 out << "<a href=\"##{anchor}\">#{item}</a>"
945 out << "<a href=\"##{anchor}\">#{item}</a>"
942 current = level
946 current = level
943 started = true
947 started = true
944 end
948 end
945 out << '</li></ul>' * (current - root)
949 out << '</li></ul>' * (current - root)
946 out << '</li></ul>'
950 out << '</li></ul>'
947 end
951 end
948 end
952 end
949 end
953 end
950
954
951 # Same as Rails' simple_format helper without using paragraphs
955 # Same as Rails' simple_format helper without using paragraphs
952 def simple_format_without_paragraph(text)
956 def simple_format_without_paragraph(text)
953 text.to_s.
957 text.to_s.
954 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
958 gsub(/\r\n?/, "\n"). # \r\n and \r -> \n
955 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
959 gsub(/\n\n+/, "<br /><br />"). # 2+ newline -> 2 br
956 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
960 gsub(/([^\n]\n)(?=[^\n])/, '\1<br />'). # 1 newline -> br
957 html_safe
961 html_safe
958 end
962 end
959
963
960 def lang_options_for_select(blank=true)
964 def lang_options_for_select(blank=true)
961 (blank ? [["(auto)", ""]] : []) +
965 (blank ? [["(auto)", ""]] : []) +
962 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
966 valid_languages.collect{|lang| [ ll(lang.to_s, :general_lang_name), lang.to_s]}.sort{|x,y| x.last <=> y.last }
963 end
967 end
964
968
965 def label_tag_for(name, option_tags = nil, options = {})
969 def label_tag_for(name, option_tags = nil, options = {})
966 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
970 label_text = l(("field_"+field.to_s.gsub(/\_id$/, "")).to_sym) + (options.delete(:required) ? @template.content_tag("span", " *", :class => "required"): "")
967 content_tag("label", label_text)
971 content_tag("label", label_text)
968 end
972 end
969
973
970 def labelled_form_for(*args, &proc)
974 def labelled_form_for(*args, &proc)
971 args << {} unless args.last.is_a?(Hash)
975 args << {} unless args.last.is_a?(Hash)
972 options = args.last
976 options = args.last
973 if args.first.is_a?(Symbol)
977 if args.first.is_a?(Symbol)
974 options.merge!(:as => args.shift)
978 options.merge!(:as => args.shift)
975 end
979 end
976 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
980 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
977 form_for(*args, &proc)
981 form_for(*args, &proc)
978 end
982 end
979
983
980 def labelled_fields_for(*args, &proc)
984 def labelled_fields_for(*args, &proc)
981 args << {} unless args.last.is_a?(Hash)
985 args << {} unless args.last.is_a?(Hash)
982 options = args.last
986 options = args.last
983 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
987 options.merge!({:builder => Redmine::Views::LabelledFormBuilder})
984 fields_for(*args, &proc)
988 fields_for(*args, &proc)
985 end
989 end
986
990
987 def labelled_remote_form_for(*args, &proc)
991 def labelled_remote_form_for(*args, &proc)
988 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
992 ActiveSupport::Deprecation.warn "ApplicationHelper#labelled_remote_form_for is deprecated and will be removed in Redmine 2.2."
989 args << {} unless args.last.is_a?(Hash)
993 args << {} unless args.last.is_a?(Hash)
990 options = args.last
994 options = args.last
991 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
995 options.merge!({:builder => Redmine::Views::LabelledFormBuilder, :remote => true})
992 form_for(*args, &proc)
996 form_for(*args, &proc)
993 end
997 end
994
998
995 def error_messages_for(*objects)
999 def error_messages_for(*objects)
996 html = ""
1000 html = ""
997 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
1001 objects = objects.map {|o| o.is_a?(String) ? instance_variable_get("@#{o}") : o}.compact
998 errors = objects.map {|o| o.errors.full_messages}.flatten
1002 errors = objects.map {|o| o.errors.full_messages}.flatten
999 if errors.any?
1003 if errors.any?
1000 html << "<div id='errorExplanation'><ul>\n"
1004 html << "<div id='errorExplanation'><ul>\n"
1001 errors.each do |error|
1005 errors.each do |error|
1002 html << "<li>#{h error}</li>\n"
1006 html << "<li>#{h error}</li>\n"
1003 end
1007 end
1004 html << "</ul></div>\n"
1008 html << "</ul></div>\n"
1005 end
1009 end
1006 html.html_safe
1010 html.html_safe
1007 end
1011 end
1008
1012
1009 def delete_link(url, options={})
1013 def delete_link(url, options={})
1010 options = {
1014 options = {
1011 :method => :delete,
1015 :method => :delete,
1012 :data => {:confirm => l(:text_are_you_sure)},
1016 :data => {:confirm => l(:text_are_you_sure)},
1013 :class => 'icon icon-del'
1017 :class => 'icon icon-del'
1014 }.merge(options)
1018 }.merge(options)
1015
1019
1016 link_to l(:button_delete), url, options
1020 link_to l(:button_delete), url, options
1017 end
1021 end
1018
1022
1019 def preview_link(url, form, target='preview', options={})
1023 def preview_link(url, form, target='preview', options={})
1020 content_tag 'a', l(:label_preview), {
1024 content_tag 'a', l(:label_preview), {
1021 :href => "#",
1025 :href => "#",
1022 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1026 :onclick => %|submitPreview("#{escape_javascript url_for(url)}", "#{escape_javascript form}", "#{escape_javascript target}"); return false;|,
1023 :accesskey => accesskey(:preview)
1027 :accesskey => accesskey(:preview)
1024 }.merge(options)
1028 }.merge(options)
1025 end
1029 end
1026
1030
1027 def link_to_function(name, function, html_options={})
1031 def link_to_function(name, function, html_options={})
1028 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1032 content_tag(:a, name, {:href => '#', :onclick => "#{function}; return false;"}.merge(html_options))
1029 end
1033 end
1030
1034
1031 # Helper to render JSON in views
1035 # Helper to render JSON in views
1032 def raw_json(arg)
1036 def raw_json(arg)
1033 arg.to_json.to_s.gsub('/', '\/').html_safe
1037 arg.to_json.to_s.gsub('/', '\/').html_safe
1034 end
1038 end
1035
1039
1036 def back_url
1040 def back_url
1037 url = params[:back_url]
1041 url = params[:back_url]
1038 if url.nil? && referer = request.env['HTTP_REFERER']
1042 if url.nil? && referer = request.env['HTTP_REFERER']
1039 url = CGI.unescape(referer.to_s)
1043 url = CGI.unescape(referer.to_s)
1040 end
1044 end
1041 url
1045 url
1042 end
1046 end
1043
1047
1044 def back_url_hidden_field_tag
1048 def back_url_hidden_field_tag
1045 url = back_url
1049 url = back_url
1046 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1050 hidden_field_tag('back_url', url, :id => nil) unless url.blank?
1047 end
1051 end
1048
1052
1049 def check_all_links(form_name)
1053 def check_all_links(form_name)
1050 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1054 link_to_function(l(:button_check_all), "checkAll('#{form_name}', true)") +
1051 " | ".html_safe +
1055 " | ".html_safe +
1052 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1056 link_to_function(l(:button_uncheck_all), "checkAll('#{form_name}', false)")
1053 end
1057 end
1054
1058
1055 def progress_bar(pcts, options={})
1059 def progress_bar(pcts, options={})
1056 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1060 pcts = [pcts, pcts] unless pcts.is_a?(Array)
1057 pcts = pcts.collect(&:round)
1061 pcts = pcts.collect(&:round)
1058 pcts[1] = pcts[1] - pcts[0]
1062 pcts[1] = pcts[1] - pcts[0]
1059 pcts << (100 - pcts[1] - pcts[0])
1063 pcts << (100 - pcts[1] - pcts[0])
1060 width = options[:width] || '100px;'
1064 width = options[:width] || '100px;'
1061 legend = options[:legend] || ''
1065 legend = options[:legend] || ''
1062 content_tag('table',
1066 content_tag('table',
1063 content_tag('tr',
1067 content_tag('tr',
1064 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1068 (pcts[0] > 0 ? content_tag('td', '', :style => "width: #{pcts[0]}%;", :class => 'closed') : ''.html_safe) +
1065 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1069 (pcts[1] > 0 ? content_tag('td', '', :style => "width: #{pcts[1]}%;", :class => 'done') : ''.html_safe) +
1066 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1070 (pcts[2] > 0 ? content_tag('td', '', :style => "width: #{pcts[2]}%;", :class => 'todo') : ''.html_safe)
1067 ), :class => 'progress', :style => "width: #{width};").html_safe +
1071 ), :class => 'progress', :style => "width: #{width};").html_safe +
1068 content_tag('p', legend, :class => 'pourcent').html_safe
1072 content_tag('p', legend, :class => 'pourcent').html_safe
1069 end
1073 end
1070
1074
1071 def checked_image(checked=true)
1075 def checked_image(checked=true)
1072 if checked
1076 if checked
1073 image_tag 'toggle_check.png'
1077 image_tag 'toggle_check.png'
1074 end
1078 end
1075 end
1079 end
1076
1080
1077 def context_menu(url)
1081 def context_menu(url)
1078 unless @context_menu_included
1082 unless @context_menu_included
1079 content_for :header_tags do
1083 content_for :header_tags do
1080 javascript_include_tag('context_menu') +
1084 javascript_include_tag('context_menu') +
1081 stylesheet_link_tag('context_menu')
1085 stylesheet_link_tag('context_menu')
1082 end
1086 end
1083 if l(:direction) == 'rtl'
1087 if l(:direction) == 'rtl'
1084 content_for :header_tags do
1088 content_for :header_tags do
1085 stylesheet_link_tag('context_menu_rtl')
1089 stylesheet_link_tag('context_menu_rtl')
1086 end
1090 end
1087 end
1091 end
1088 @context_menu_included = true
1092 @context_menu_included = true
1089 end
1093 end
1090 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1094 javascript_tag "contextMenuInit('#{ url_for(url) }')"
1091 end
1095 end
1092
1096
1093 def calendar_for(field_id)
1097 def calendar_for(field_id)
1094 include_calendar_headers_tags
1098 include_calendar_headers_tags
1095 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1099 javascript_tag("$(function() { $('##{field_id}').datepicker(datepickerOptions); });")
1096 end
1100 end
1097
1101
1098 def include_calendar_headers_tags
1102 def include_calendar_headers_tags
1099 unless @calendar_headers_tags_included
1103 unless @calendar_headers_tags_included
1100 @calendar_headers_tags_included = true
1104 @calendar_headers_tags_included = true
1101 content_for :header_tags do
1105 content_for :header_tags do
1102 start_of_week = Setting.start_of_week
1106 start_of_week = Setting.start_of_week
1103 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1107 start_of_week = l(:general_first_day_of_week, :default => '1') if start_of_week.blank?
1104 # Redmine uses 1..7 (monday..sunday) in settings and locales
1108 # Redmine uses 1..7 (monday..sunday) in settings and locales
1105 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1109 # JQuery uses 0..6 (sunday..saturday), 7 needs to be changed to 0
1106 start_of_week = start_of_week.to_i % 7
1110 start_of_week = start_of_week.to_i % 7
1107
1111
1108 tags = javascript_tag(
1112 tags = javascript_tag(
1109 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1113 "var datepickerOptions={dateFormat: 'yy-mm-dd', firstDay: #{start_of_week}, " +
1110 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1114 "showOn: 'button', buttonImageOnly: true, buttonImage: '" +
1111 path_to_image('/images/calendar.png') +
1115 path_to_image('/images/calendar.png') +
1112 "', showButtonPanel: true};")
1116 "', showButtonPanel: true};")
1113 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1117 jquery_locale = l('jquery.locale', :default => current_language.to_s)
1114 unless jquery_locale == 'en'
1118 unless jquery_locale == 'en'
1115 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1119 tags << javascript_include_tag("i18n/jquery.ui.datepicker-#{jquery_locale}.js")
1116 end
1120 end
1117 tags
1121 tags
1118 end
1122 end
1119 end
1123 end
1120 end
1124 end
1121
1125
1122 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1126 # Overrides Rails' stylesheet_link_tag with themes and plugins support.
1123 # Examples:
1127 # Examples:
1124 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1128 # stylesheet_link_tag('styles') # => picks styles.css from the current theme or defaults
1125 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1129 # stylesheet_link_tag('styles', :plugin => 'foo) # => picks styles.css from plugin's assets
1126 #
1130 #
1127 def stylesheet_link_tag(*sources)
1131 def stylesheet_link_tag(*sources)
1128 options = sources.last.is_a?(Hash) ? sources.pop : {}
1132 options = sources.last.is_a?(Hash) ? sources.pop : {}
1129 plugin = options.delete(:plugin)
1133 plugin = options.delete(:plugin)
1130 sources = sources.map do |source|
1134 sources = sources.map do |source|
1131 if plugin
1135 if plugin
1132 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1136 "/plugin_assets/#{plugin}/stylesheets/#{source}"
1133 elsif current_theme && current_theme.stylesheets.include?(source)
1137 elsif current_theme && current_theme.stylesheets.include?(source)
1134 current_theme.stylesheet_path(source)
1138 current_theme.stylesheet_path(source)
1135 else
1139 else
1136 source
1140 source
1137 end
1141 end
1138 end
1142 end
1139 super sources, options
1143 super sources, options
1140 end
1144 end
1141
1145
1142 # Overrides Rails' image_tag with themes and plugins support.
1146 # Overrides Rails' image_tag with themes and plugins support.
1143 # Examples:
1147 # Examples:
1144 # image_tag('image.png') # => picks image.png from the current theme or defaults
1148 # image_tag('image.png') # => picks image.png from the current theme or defaults
1145 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1149 # image_tag('image.png', :plugin => 'foo) # => picks image.png from plugin's assets
1146 #
1150 #
1147 def image_tag(source, options={})
1151 def image_tag(source, options={})
1148 if plugin = options.delete(:plugin)
1152 if plugin = options.delete(:plugin)
1149 source = "/plugin_assets/#{plugin}/images/#{source}"
1153 source = "/plugin_assets/#{plugin}/images/#{source}"
1150 elsif current_theme && current_theme.images.include?(source)
1154 elsif current_theme && current_theme.images.include?(source)
1151 source = current_theme.image_path(source)
1155 source = current_theme.image_path(source)
1152 end
1156 end
1153 super source, options
1157 super source, options
1154 end
1158 end
1155
1159
1156 # Overrides Rails' javascript_include_tag with plugins support
1160 # Overrides Rails' javascript_include_tag with plugins support
1157 # Examples:
1161 # Examples:
1158 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1162 # javascript_include_tag('scripts') # => picks scripts.js from defaults
1159 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1163 # javascript_include_tag('scripts', :plugin => 'foo) # => picks scripts.js from plugin's assets
1160 #
1164 #
1161 def javascript_include_tag(*sources)
1165 def javascript_include_tag(*sources)
1162 options = sources.last.is_a?(Hash) ? sources.pop : {}
1166 options = sources.last.is_a?(Hash) ? sources.pop : {}
1163 if plugin = options.delete(:plugin)
1167 if plugin = options.delete(:plugin)
1164 sources = sources.map do |source|
1168 sources = sources.map do |source|
1165 if plugin
1169 if plugin
1166 "/plugin_assets/#{plugin}/javascripts/#{source}"
1170 "/plugin_assets/#{plugin}/javascripts/#{source}"
1167 else
1171 else
1168 source
1172 source
1169 end
1173 end
1170 end
1174 end
1171 end
1175 end
1172 super sources, options
1176 super sources, options
1173 end
1177 end
1174
1178
1175 def content_for(name, content = nil, &block)
1179 def content_for(name, content = nil, &block)
1176 @has_content ||= {}
1180 @has_content ||= {}
1177 @has_content[name] = true
1181 @has_content[name] = true
1178 super(name, content, &block)
1182 super(name, content, &block)
1179 end
1183 end
1180
1184
1181 def has_content?(name)
1185 def has_content?(name)
1182 (@has_content && @has_content[name]) || false
1186 (@has_content && @has_content[name]) || false
1183 end
1187 end
1184
1188
1185 def sidebar_content?
1189 def sidebar_content?
1186 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1190 has_content?(:sidebar) || view_layouts_base_sidebar_hook_response.present?
1187 end
1191 end
1188
1192
1189 def view_layouts_base_sidebar_hook_response
1193 def view_layouts_base_sidebar_hook_response
1190 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1194 @view_layouts_base_sidebar_hook_response ||= call_hook(:view_layouts_base_sidebar)
1191 end
1195 end
1192
1196
1193 def email_delivery_enabled?
1197 def email_delivery_enabled?
1194 !!ActionMailer::Base.perform_deliveries
1198 !!ActionMailer::Base.perform_deliveries
1195 end
1199 end
1196
1200
1197 # Returns the avatar image tag for the given +user+ if avatars are enabled
1201 # Returns the avatar image tag for the given +user+ if avatars are enabled
1198 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1202 # +user+ can be a User or a string that will be scanned for an email address (eg. 'joe <joe@foo.bar>')
1199 def avatar(user, options = { })
1203 def avatar(user, options = { })
1200 if Setting.gravatar_enabled?
1204 if Setting.gravatar_enabled?
1201 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1205 options.merge!({:ssl => (request && request.ssl?), :default => Setting.gravatar_default})
1202 email = nil
1206 email = nil
1203 if user.respond_to?(:mail)
1207 if user.respond_to?(:mail)
1204 email = user.mail
1208 email = user.mail
1205 elsif user.to_s =~ %r{<(.+?)>}
1209 elsif user.to_s =~ %r{<(.+?)>}
1206 email = $1
1210 email = $1
1207 end
1211 end
1208 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1212 return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
1209 else
1213 else
1210 ''
1214 ''
1211 end
1215 end
1212 end
1216 end
1213
1217
1214 def sanitize_anchor_name(anchor)
1218 def sanitize_anchor_name(anchor)
1215 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1219 if ''.respond_to?(:encoding) || RUBY_PLATFORM == 'java'
1216 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1220 anchor.gsub(%r{[^\p{Word}\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1217 else
1221 else
1218 # TODO: remove when ruby1.8 is no longer supported
1222 # TODO: remove when ruby1.8 is no longer supported
1219 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1223 anchor.gsub(%r{[^\w\s\-]}, '').gsub(%r{\s+(\-+\s*)?}, '-')
1220 end
1224 end
1221 end
1225 end
1222
1226
1223 # Returns the javascript tags that are included in the html layout head
1227 # Returns the javascript tags that are included in the html layout head
1224 def javascript_heads
1228 def javascript_heads
1225 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
1229 tags = javascript_include_tag('jquery-1.7.2-ui-1.8.21-ujs-2.0.3', 'application')
1226 unless User.current.pref.warn_on_leaving_unsaved == '0'
1230 unless User.current.pref.warn_on_leaving_unsaved == '0'
1227 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1231 tags << "\n".html_safe + javascript_tag("$(window).load(function(){ warnLeavingUnsaved('#{escape_javascript l(:text_warn_on_leaving_unsaved)}'); });")
1228 end
1232 end
1229 tags
1233 tags
1230 end
1234 end
1231
1235
1232 def favicon
1236 def favicon
1233 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1237 "<link rel='shortcut icon' href='#{image_path('/favicon.ico')}' />".html_safe
1234 end
1238 end
1235
1239
1236 def robot_exclusion_tag
1240 def robot_exclusion_tag
1237 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1241 '<meta name="robots" content="noindex,follow,noarchive" />'.html_safe
1238 end
1242 end
1239
1243
1240 # Returns true if arg is expected in the API response
1244 # Returns true if arg is expected in the API response
1241 def include_in_api_response?(arg)
1245 def include_in_api_response?(arg)
1242 unless @included_in_api_response
1246 unless @included_in_api_response
1243 param = params[:include]
1247 param = params[:include]
1244 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1248 @included_in_api_response = param.is_a?(Array) ? param.collect(&:to_s) : param.to_s.split(',')
1245 @included_in_api_response.collect!(&:strip)
1249 @included_in_api_response.collect!(&:strip)
1246 end
1250 end
1247 @included_in_api_response.include?(arg.to_s)
1251 @included_in_api_response.include?(arg.to_s)
1248 end
1252 end
1249
1253
1250 # Returns options or nil if nometa param or X-Redmine-Nometa header
1254 # Returns options or nil if nometa param or X-Redmine-Nometa header
1251 # was set in the request
1255 # was set in the request
1252 def api_meta(options)
1256 def api_meta(options)
1253 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1257 if params[:nometa].present? || request.headers['X-Redmine-Nometa']
1254 # compatibility mode for activeresource clients that raise
1258 # compatibility mode for activeresource clients that raise
1255 # an error when unserializing an array with attributes
1259 # an error when unserializing an array with attributes
1256 nil
1260 nil
1257 else
1261 else
1258 options
1262 options
1259 end
1263 end
1260 end
1264 end
1261
1265
1262 private
1266 private
1263
1267
1264 def wiki_helper
1268 def wiki_helper
1265 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1269 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
1266 extend helper
1270 extend helper
1267 return self
1271 return self
1268 end
1272 end
1269
1273
1270 def link_to_content_update(text, url_params = {}, html_options = {})
1274 def link_to_content_update(text, url_params = {}, html_options = {})
1271 link_to(text, url_params, html_options)
1275 link_to(text, url_params, html_options)
1272 end
1276 end
1273 end
1277 end
@@ -1,132 +1,147
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 'zlib'
18 require 'zlib'
19
19
20 class WikiContent < ActiveRecord::Base
20 class WikiContent < ActiveRecord::Base
21 self.locking_column = 'version'
21 self.locking_column = 'version'
22 belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
22 belongs_to :page, :class_name => 'WikiPage', :foreign_key => 'page_id'
23 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 validates_presence_of :text
24 validates_presence_of :text
25 validates_length_of :comments, :maximum => 255, :allow_nil => true
25 validates_length_of :comments, :maximum => 255, :allow_nil => true
26
26
27 acts_as_versioned
27 acts_as_versioned
28
28
29 def visible?(user=User.current)
29 def visible?(user=User.current)
30 page.visible?(user)
30 page.visible?(user)
31 end
31 end
32
32
33 def project
33 def project
34 page.project
34 page.project
35 end
35 end
36
36
37 def attachments
37 def attachments
38 page.nil? ? [] : page.attachments
38 page.nil? ? [] : page.attachments
39 end
39 end
40
40
41 # Returns the mail adresses of users that should be notified
41 # Returns the mail adresses of users that should be notified
42 def recipients
42 def recipients
43 notified = project.notified_users
43 notified = project.notified_users
44 notified.reject! {|user| !visible?(user)}
44 notified.reject! {|user| !visible?(user)}
45 notified.collect(&:mail)
45 notified.collect(&:mail)
46 end
46 end
47
47
48 # Return true if the content is the current page content
48 # Return true if the content is the current page content
49 def current_version?
49 def current_version?
50 true
50 true
51 end
51 end
52
52
53 class Version
53 class Version
54 belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
54 belongs_to :page, :class_name => '::WikiPage', :foreign_key => 'page_id'
55 belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
55 belongs_to :author, :class_name => '::User', :foreign_key => 'author_id'
56 attr_protected :data
56 attr_protected :data
57
57
58 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
58 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki_edit)}: #{o.page.title} (##{o.version})"},
59 :description => :comments,
59 :description => :comments,
60 :datetime => :updated_on,
60 :datetime => :updated_on,
61 :type => 'wiki-page',
61 :type => 'wiki-page',
62 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.page.wiki.project, :id => o.page.title, :version => o.version}}
62 :url => Proc.new {|o| {:controller => 'wiki', :action => 'show', :project_id => o.page.wiki.project, :id => o.page.title, :version => o.version}}
63
63
64 acts_as_activity_provider :type => 'wiki_edits',
64 acts_as_activity_provider :type => 'wiki_edits',
65 :timestamp => "#{WikiContent.versioned_table_name}.updated_on",
65 :timestamp => "#{WikiContent.versioned_table_name}.updated_on",
66 :author_key => "#{WikiContent.versioned_table_name}.author_id",
66 :author_key => "#{WikiContent.versioned_table_name}.author_id",
67 :permission => :view_wiki_edits,
67 :permission => :view_wiki_edits,
68 :find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
68 :find_options => {:select => "#{WikiContent.versioned_table_name}.updated_on, #{WikiContent.versioned_table_name}.comments, " +
69 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
69 "#{WikiContent.versioned_table_name}.#{WikiContent.version_column}, #{WikiPage.table_name}.title, " +
70 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
70 "#{WikiContent.versioned_table_name}.page_id, #{WikiContent.versioned_table_name}.author_id, " +
71 "#{WikiContent.versioned_table_name}.id",
71 "#{WikiContent.versioned_table_name}.id",
72 :joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
72 :joins => "LEFT JOIN #{WikiPage.table_name} ON #{WikiPage.table_name}.id = #{WikiContent.versioned_table_name}.page_id " +
73 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
73 "LEFT JOIN #{Wiki.table_name} ON #{Wiki.table_name}.id = #{WikiPage.table_name}.wiki_id " +
74 "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
74 "LEFT JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Wiki.table_name}.project_id"}
75
75
76 after_destroy :page_update_after_destroy
77
76 def text=(plain)
78 def text=(plain)
77 case Setting.wiki_compression
79 case Setting.wiki_compression
78 when 'gzip'
80 when 'gzip'
79 begin
81 begin
80 self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
82 self.data = Zlib::Deflate.deflate(plain, Zlib::BEST_COMPRESSION)
81 self.compression = 'gzip'
83 self.compression = 'gzip'
82 rescue
84 rescue
83 self.data = plain
85 self.data = plain
84 self.compression = ''
86 self.compression = ''
85 end
87 end
86 else
88 else
87 self.data = plain
89 self.data = plain
88 self.compression = ''
90 self.compression = ''
89 end
91 end
90 plain
92 plain
91 end
93 end
92
94
93 def text
95 def text
94 @text ||= begin
96 @text ||= begin
95 str = case compression
97 str = case compression
96 when 'gzip'
98 when 'gzip'
97 Zlib::Inflate.inflate(data)
99 Zlib::Inflate.inflate(data)
98 else
100 else
99 # uncompressed data
101 # uncompressed data
100 data
102 data
101 end
103 end
102 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
104 str.force_encoding("UTF-8") if str.respond_to?(:force_encoding)
103 str
105 str
104 end
106 end
105 end
107 end
106
108
107 def project
109 def project
108 page.project
110 page.project
109 end
111 end
110
112
111 # Return true if the content is the current page content
113 # Return true if the content is the current page content
112 def current_version?
114 def current_version?
113 page.content.version == self.version
115 page.content.version == self.version
114 end
116 end
115
117
116 # Returns the previous version or nil
118 # Returns the previous version or nil
117 def previous
119 def previous
118 @previous ||= WikiContent::Version.
120 @previous ||= WikiContent::Version.
119 reorder('version DESC').
121 reorder('version DESC').
120 includes(:author).
122 includes(:author).
121 where("wiki_content_id = ? AND version < ?", wiki_content_id, version).first
123 where("wiki_content_id = ? AND version < ?", wiki_content_id, version).first
122 end
124 end
123
125
124 # Returns the next version or nil
126 # Returns the next version or nil
125 def next
127 def next
126 @next ||= WikiContent::Version.
128 @next ||= WikiContent::Version.
127 reorder('version ASC').
129 reorder('version ASC').
128 includes(:author).
130 includes(:author).
129 where("wiki_content_id = ? AND version > ?", wiki_content_id, version).first
131 where("wiki_content_id = ? AND version > ?", wiki_content_id, version).first
130 end
132 end
133
134 private
135
136 # Updates page's content if the latest version is removed
137 # or destroys the page if it was the only version
138 def page_update_after_destroy
139 latest = page.content.versions.reorder("#{self.class.table_name}.version DESC").first
140 if latest && page.content.version != latest.version
141 raise ActiveRecord::Rollback unless page.content.revert_to!(latest)
142 elsif latest.nil?
143 raise ActiveRecord::Rollback unless page.destroy
144 end
145 end
131 end
146 end
132 end
147 end
@@ -1,39 +1,42
1 <%= wiki_page_breadcrumb(@page) %>
1 <%= wiki_page_breadcrumb(@page) %>
2
2
3 <h2><%= h(@page.pretty_title) %></h2>
3 <h2><%= h(@page.pretty_title) %></h2>
4
4
5 <h3><%= l(:label_history) %></h3>
5 <h3><%= l(:label_history) %></h3>
6
6
7 <%= form_tag({:controller => 'wiki', :action => 'diff',
7 <%= form_tag({:controller => 'wiki', :action => 'diff',
8 :project_id => @page.project, :id => @page.title},
8 :project_id => @page.project, :id => @page.title},
9 :method => :get) do %>
9 :method => :get) do %>
10 <table class="list wiki-page-versions">
10 <table class="list wiki-page-versions">
11 <thead><tr>
11 <thead><tr>
12 <th>#</th>
12 <th>#</th>
13 <th></th>
13 <th></th>
14 <th></th>
14 <th></th>
15 <th><%= l(:field_updated_on) %></th>
15 <th><%= l(:field_updated_on) %></th>
16 <th><%= l(:field_author) %></th>
16 <th><%= l(:field_author) %></th>
17 <th><%= l(:field_comments) %></th>
17 <th><%= l(:field_comments) %></th>
18 <th></th>
18 <th></th>
19 </tr></thead>
19 </tr></thead>
20 <tbody>
20 <tbody>
21 <% show_diff = @versions.size > 1 %>
21 <% show_diff = @versions.size > 1 %>
22 <% line_num = 1 %>
22 <% line_num = 1 %>
23 <% @versions.each do |ver| %>
23 <% @versions.each do |ver| %>
24 <tr class="wiki-page-version <%= cycle("odd", "even") %>">
24 <tr class="wiki-page-version <%= cycle("odd", "even") %>">
25 <td class="id"><%= link_to h(ver.version), :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %></td>
25 <td class="id"><%= link_to h(ver.version), :action => 'show', :id => @page.title, :project_id => @page.project, :version => ver.version %></td>
26 <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('#cbto-#{line_num+1}').attr('checked', true);") if show_diff && (line_num < @versions.size) %></td>
26 <td class="checkbox"><%= radio_button_tag('version', ver.version, (line_num==1), :id => "cb-#{line_num}", :onclick => "$('#cbto-#{line_num+1}').attr('checked', true);") if show_diff && (line_num < @versions.size) %></td>
27 <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %></td>
27 <td class="checkbox"><%= radio_button_tag('version_from', ver.version, (line_num==2), :id => "cbto-#{line_num}") if show_diff && (line_num > 1) %></td>
28 <td class="updated_on"><%= format_time(ver.updated_on) %></td>
28 <td class="updated_on"><%= format_time(ver.updated_on) %></td>
29 <td class="author"><%= link_to_user ver.author %></td>
29 <td class="author"><%= link_to_user ver.author %></td>
30 <td class="comments"><%=h ver.comments %></td>
30 <td class="comments"><%=h ver.comments %></td>
31 <td class="buttons"><%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %></td>
31 <td class="buttons">
32 <%= link_to l(:button_annotate), :action => 'annotate', :id => @page.title, :version => ver.version %>
33 <%= delete_link wiki_page_path(@page, :version => ver.version) if User.current.allowed_to?(:delete_wiki_pages, @page.project) && @version_count > 1 %>
34 </td>
32 </tr>
35 </tr>
33 <% line_num += 1 %>
36 <% line_num += 1 %>
34 <% end %>
37 <% end %>
35 </tbody>
38 </tbody>
36 </table>
39 </table>
37 <%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
40 <%= submit_tag l(:label_view_diff), :class => 'small' if show_diff %>
38 <span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span>
41 <span class="pagination"><%= pagination_links_full @version_pages, @version_count, :page_param => :p %></span>
39 <% end %>
42 <% end %>
@@ -1,345 +1,346
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 RedmineApp::Application.routes.draw do
18 RedmineApp::Application.routes.draw do
19 root :to => 'welcome#index', :as => 'home'
19 root :to => 'welcome#index', :as => 'home'
20
20
21 match 'login', :to => 'account#login', :as => 'signin'
21 match 'login', :to => 'account#login', :as => 'signin'
22 match 'logout', :to => 'account#logout', :as => 'signout'
22 match 'logout', :to => 'account#logout', :as => 'signout'
23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
23 match 'account/register', :to => 'account#register', :via => [:get, :post], :as => 'register'
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
24 match 'account/lost_password', :to => 'account#lost_password', :via => [:get, :post], :as => 'lost_password'
25 match 'account/activate', :to => 'account#activate', :via => :get
25 match 'account/activate', :to => 'account#activate', :via => :get
26
26
27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news'
27 match '/news/preview', :controller => 'previews', :action => 'news', :as => 'preview_news'
28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue'
28 match '/issues/preview/new/:project_id', :to => 'previews#issue', :as => 'preview_new_issue'
29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue'
29 match '/issues/preview/edit/:id', :to => 'previews#issue', :as => 'preview_edit_issue'
30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue'
30 match '/issues/preview', :to => 'previews#issue', :as => 'preview_issue'
31
31
32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
32 match 'projects/:id/wiki', :to => 'wikis#edit', :via => :post
33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
33 match 'projects/:id/wiki/destroy', :to => 'wikis#destroy', :via => [:get, :post]
34
34
35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post]
35 match 'boards/:board_id/topics/new', :to => 'messages#new', :via => [:get, :post]
36 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
36 get 'boards/:board_id/topics/:id', :to => 'messages#show', :as => 'board_message'
37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
37 match 'boards/:board_id/topics/quote/:id', :to => 'messages#quote', :via => [:get, :post]
38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
38 get 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
39
39
40 post 'boards/:board_id/topics/preview', :to => 'messages#preview'
40 post 'boards/:board_id/topics/preview', :to => 'messages#preview'
41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
41 post 'boards/:board_id/topics/:id/replies', :to => 'messages#reply'
42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
42 post 'boards/:board_id/topics/:id/edit', :to => 'messages#edit'
43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
43 post 'boards/:board_id/topics/:id/destroy', :to => 'messages#destroy'
44
44
45 # Misc issue routes. TODO: move into resources
45 # Misc issue routes. TODO: move into resources
46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
46 match '/issues/auto_complete', :to => 'auto_completes#issues', :via => :get, :as => 'auto_complete_issues'
47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu'
47 match '/issues/context_menu', :to => 'context_menus#issues', :as => 'issues_context_menu'
48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes'
48 match '/issues/changes', :to => 'journals#index', :as => 'issue_changes'
49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
49 match '/issues/:id/quoted', :to => 'journals#new', :id => /\d+/, :via => :post, :as => 'quoted_issue'
50
50
51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
51 match '/journals/diff/:id', :to => 'journals#diff', :id => /\d+/, :via => :get
52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
52 match '/journals/edit/:id', :to => 'journals#edit', :id => /\d+/, :via => [:get, :post]
53
53
54 match '/projects/:project_id/issues/gantt', :to => 'gantts#show'
54 match '/projects/:project_id/issues/gantt', :to => 'gantts#show'
55 match '/issues/gantt', :to => 'gantts#show'
55 match '/issues/gantt', :to => 'gantts#show'
56
56
57 match '/projects/:project_id/issues/calendar', :to => 'calendars#show'
57 match '/projects/:project_id/issues/calendar', :to => 'calendars#show'
58 match '/issues/calendar', :to => 'calendars#show'
58 match '/issues/calendar', :to => 'calendars#show'
59
59
60 match 'projects/:id/issues/report', :to => 'reports#issue_report', :via => :get
60 match 'projects/:id/issues/report', :to => 'reports#issue_report', :via => :get
61 match 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :via => :get
61 match 'projects/:id/issues/report/:detail', :to => 'reports#issue_report_details', :via => :get
62
62
63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
63 match 'my/account', :controller => 'my', :action => 'account', :via => [:get, :post]
64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
64 match 'my/account/destroy', :controller => 'my', :action => 'destroy', :via => [:get, :post]
65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
65 match 'my/page', :controller => 'my', :action => 'page', :via => :get
66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
66 match 'my', :controller => 'my', :action => 'index', :via => :get # Redirects to my/page
67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
67 match 'my/reset_rss_key', :controller => 'my', :action => 'reset_rss_key', :via => :post
68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
68 match 'my/reset_api_key', :controller => 'my', :action => 'reset_api_key', :via => :post
69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
69 match 'my/password', :controller => 'my', :action => 'password', :via => [:get, :post]
70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
70 match 'my/page_layout', :controller => 'my', :action => 'page_layout', :via => :get
71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
71 match 'my/add_block', :controller => 'my', :action => 'add_block', :via => :post
72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
72 match 'my/remove_block', :controller => 'my', :action => 'remove_block', :via => :post
73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
73 match 'my/order_blocks', :controller => 'my', :action => 'order_blocks', :via => :post
74
74
75 resources :users
75 resources :users
76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
76 match 'users/:id/memberships/:membership_id', :to => 'users#edit_membership', :via => :put, :as => 'user_membership'
77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
77 match 'users/:id/memberships/:membership_id', :to => 'users#destroy_membership', :via => :delete
78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
78 match 'users/:id/memberships', :to => 'users#edit_membership', :via => :post, :as => 'user_memberships'
79
79
80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
80 match 'watchers/new', :controller=> 'watchers', :action => 'new', :via => :get
81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
81 match 'watchers', :controller=> 'watchers', :action => 'create', :via => :post
82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
82 match 'watchers/append', :controller=> 'watchers', :action => 'append', :via => :post
83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
83 match 'watchers/destroy', :controller=> 'watchers', :action => 'destroy', :via => :post
84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
84 match 'watchers/watch', :controller=> 'watchers', :action => 'watch', :via => :post
85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
85 match 'watchers/unwatch', :controller=> 'watchers', :action => 'unwatch', :via => :post
86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
86 match 'watchers/autocomplete_for_user', :controller=> 'watchers', :action => 'autocomplete_for_user', :via => :get
87
87
88 match 'projects/:id/settings/:tab', :to => "projects#settings"
88 match 'projects/:id/settings/:tab', :to => "projects#settings"
89
89
90 resources :projects do
90 resources :projects do
91 member do
91 member do
92 get 'settings'
92 get 'settings'
93 post 'modules'
93 post 'modules'
94 post 'archive'
94 post 'archive'
95 post 'unarchive'
95 post 'unarchive'
96 post 'close'
96 post 'close'
97 post 'reopen'
97 post 'reopen'
98 match 'copy', :via => [:get, :post]
98 match 'copy', :via => [:get, :post]
99 end
99 end
100
100
101 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
101 resources :memberships, :shallow => true, :controller => 'members', :only => [:index, :show, :new, :create, :update, :destroy] do
102 collection do
102 collection do
103 get 'autocomplete'
103 get 'autocomplete'
104 end
104 end
105 end
105 end
106
106
107 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
107 resource :enumerations, :controller => 'project_enumerations', :only => [:update, :destroy]
108
108
109 match 'issues/:copy_from/copy', :to => 'issues#new'
109 match 'issues/:copy_from/copy', :to => 'issues#new'
110 resources :issues, :only => [:index, :new, :create] do
110 resources :issues, :only => [:index, :new, :create] do
111 resources :time_entries, :controller => 'timelog' do
111 resources :time_entries, :controller => 'timelog' do
112 collection do
112 collection do
113 get 'report'
113 get 'report'
114 end
114 end
115 end
115 end
116 end
116 end
117 # issue form update
117 # issue form update
118 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
118 match 'issues/new', :controller => 'issues', :action => 'new', :via => [:put, :post], :as => 'issue_form'
119
119
120 resources :files, :only => [:index, :new, :create]
120 resources :files, :only => [:index, :new, :create]
121
121
122 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
122 resources :versions, :except => [:index, :show, :edit, :update, :destroy] do
123 collection do
123 collection do
124 put 'close_completed'
124 put 'close_completed'
125 end
125 end
126 end
126 end
127 match 'versions.:format', :to => 'versions#index'
127 match 'versions.:format', :to => 'versions#index'
128 match 'roadmap', :to => 'versions#index', :format => false
128 match 'roadmap', :to => 'versions#index', :format => false
129 match 'versions', :to => 'versions#index'
129 match 'versions', :to => 'versions#index'
130
130
131 resources :news, :except => [:show, :edit, :update, :destroy]
131 resources :news, :except => [:show, :edit, :update, :destroy]
132 resources :time_entries, :controller => 'timelog' do
132 resources :time_entries, :controller => 'timelog' do
133 get 'report', :on => :collection
133 get 'report', :on => :collection
134 end
134 end
135 resources :queries, :only => [:new, :create]
135 resources :queries, :only => [:new, :create]
136 resources :issue_categories, :shallow => true
136 resources :issue_categories, :shallow => true
137 resources :documents, :except => [:show, :edit, :update, :destroy]
137 resources :documents, :except => [:show, :edit, :update, :destroy]
138 resources :boards
138 resources :boards
139 resources :repositories, :shallow => true, :except => [:index, :show] do
139 resources :repositories, :shallow => true, :except => [:index, :show] do
140 member do
140 member do
141 match 'committers', :via => [:get, :post]
141 match 'committers', :via => [:get, :post]
142 end
142 end
143 end
143 end
144
144
145 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
145 match 'wiki/index', :controller => 'wiki', :action => 'index', :via => :get
146 resources :wiki, :except => [:index, :new, :create] do
146 resources :wiki, :except => [:index, :new, :create] do
147 member do
147 member do
148 get 'rename'
148 get 'rename'
149 post 'rename'
149 post 'rename'
150 get 'history'
150 get 'history'
151 get 'diff'
151 get 'diff'
152 match 'preview', :via => [:post, :put]
152 match 'preview', :via => [:post, :put]
153 post 'protect'
153 post 'protect'
154 post 'add_attachment'
154 post 'add_attachment'
155 end
155 end
156 collection do
156 collection do
157 get 'export'
157 get 'export'
158 get 'date_index'
158 get 'date_index'
159 end
159 end
160 end
160 end
161 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
161 match 'wiki', :controller => 'wiki', :action => 'show', :via => :get
162 get 'wiki/:id/:version', :to => 'wiki#show'
162 get 'wiki/:id/:version', :to => 'wiki#show'
163 delete 'wiki/:id/:version', :to => 'wiki#destroy_version'
163 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
164 get 'wiki/:id/:version/annotate', :to => 'wiki#annotate'
164 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
165 get 'wiki/:id/:version/diff', :to => 'wiki#diff'
165 end
166 end
166
167
167 resources :issues do
168 resources :issues do
168 collection do
169 collection do
169 match 'bulk_edit', :via => [:get, :post]
170 match 'bulk_edit', :via => [:get, :post]
170 post 'bulk_update'
171 post 'bulk_update'
171 end
172 end
172 resources :time_entries, :controller => 'timelog' do
173 resources :time_entries, :controller => 'timelog' do
173 collection do
174 collection do
174 get 'report'
175 get 'report'
175 end
176 end
176 end
177 end
177 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
178 resources :relations, :shallow => true, :controller => 'issue_relations', :only => [:index, :show, :create, :destroy]
178 end
179 end
179 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
180 match '/issues', :controller => 'issues', :action => 'destroy', :via => :delete
180
181
181 resources :queries, :except => [:show]
182 resources :queries, :except => [:show]
182
183
183 resources :news, :only => [:index, :show, :edit, :update, :destroy]
184 resources :news, :only => [:index, :show, :edit, :update, :destroy]
184 match '/news/:id/comments', :to => 'comments#create', :via => :post
185 match '/news/:id/comments', :to => 'comments#create', :via => :post
185 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
186 match '/news/:id/comments/:comment_id', :to => 'comments#destroy', :via => :delete
186
187
187 resources :versions, :only => [:show, :edit, :update, :destroy] do
188 resources :versions, :only => [:show, :edit, :update, :destroy] do
188 post 'status_by', :on => :member
189 post 'status_by', :on => :member
189 end
190 end
190
191
191 resources :documents, :only => [:show, :edit, :update, :destroy] do
192 resources :documents, :only => [:show, :edit, :update, :destroy] do
192 post 'add_attachment', :on => :member
193 post 'add_attachment', :on => :member
193 end
194 end
194
195
195 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu
196 match '/time_entries/context_menu', :to => 'context_menus#time_entries', :as => :time_entries_context_menu
196
197
197 resources :time_entries, :controller => 'timelog', :except => :destroy do
198 resources :time_entries, :controller => 'timelog', :except => :destroy do
198 collection do
199 collection do
199 get 'report'
200 get 'report'
200 get 'bulk_edit'
201 get 'bulk_edit'
201 post 'bulk_update'
202 post 'bulk_update'
202 end
203 end
203 end
204 end
204 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
205 match '/time_entries/:id', :to => 'timelog#destroy', :via => :delete, :id => /\d+/
205 # TODO: delete /time_entries for bulk deletion
206 # TODO: delete /time_entries for bulk deletion
206 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
207 match '/time_entries/destroy', :to => 'timelog#destroy', :via => :delete
207
208
208 # TODO: port to be part of the resources route(s)
209 # TODO: port to be part of the resources route(s)
209 match 'projects/:id/settings/:tab', :to => 'projects#settings', :via => :get
210 match 'projects/:id/settings/:tab', :to => 'projects#settings', :via => :get
210
211
211 get 'projects/:id/activity', :to => 'activities#index'
212 get 'projects/:id/activity', :to => 'activities#index'
212 get 'projects/:id/activity.:format', :to => 'activities#index'
213 get 'projects/:id/activity.:format', :to => 'activities#index'
213 get 'activity', :to => 'activities#index'
214 get 'activity', :to => 'activities#index'
214
215
215 # repositories routes
216 # repositories routes
216 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
217 get 'projects/:id/repository/:repository_id/statistics', :to => 'repositories#stats'
217 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
218 get 'projects/:id/repository/:repository_id/graph', :to => 'repositories#graph'
218
219
219 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
220 get 'projects/:id/repository/:repository_id/changes(/*path(.:ext))',
220 :to => 'repositories#changes'
221 :to => 'repositories#changes'
221
222
222 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
223 get 'projects/:id/repository/:repository_id/revisions/:rev', :to => 'repositories#revision'
223 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
224 get 'projects/:id/repository/:repository_id/revision', :to => 'repositories#revision'
224 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
225 post 'projects/:id/repository/:repository_id/revisions/:rev/issues', :to => 'repositories#add_related_issue'
225 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
226 delete 'projects/:id/repository/:repository_id/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
226 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
227 get 'projects/:id/repository/:repository_id/revisions', :to => 'repositories#revisions'
227 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
228 get 'projects/:id/repository/:repository_id/revisions/:rev/:action(/*path(.:ext))',
228 :controller => 'repositories',
229 :controller => 'repositories',
229 :format => false,
230 :format => false,
230 :constraints => {
231 :constraints => {
231 :action => /(browse|show|entry|raw|annotate|diff)/,
232 :action => /(browse|show|entry|raw|annotate|diff)/,
232 :rev => /[a-z0-9\.\-_]+/
233 :rev => /[a-z0-9\.\-_]+/
233 }
234 }
234
235
235 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
236 get 'projects/:id/repository/statistics', :to => 'repositories#stats'
236 get 'projects/:id/repository/graph', :to => 'repositories#graph'
237 get 'projects/:id/repository/graph', :to => 'repositories#graph'
237
238
238 get 'projects/:id/repository/changes(/*path(.:ext))',
239 get 'projects/:id/repository/changes(/*path(.:ext))',
239 :to => 'repositories#changes'
240 :to => 'repositories#changes'
240
241
241 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
242 get 'projects/:id/repository/revisions', :to => 'repositories#revisions'
242 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
243 get 'projects/:id/repository/revisions/:rev', :to => 'repositories#revision'
243 get 'projects/:id/repository/revision', :to => 'repositories#revision'
244 get 'projects/:id/repository/revision', :to => 'repositories#revision'
244 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
245 post 'projects/:id/repository/revisions/:rev/issues', :to => 'repositories#add_related_issue'
245 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
246 delete 'projects/:id/repository/revisions/:rev/issues/:issue_id', :to => 'repositories#remove_related_issue'
246 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
247 get 'projects/:id/repository/revisions/:rev/:action(/*path(.:ext))',
247 :controller => 'repositories',
248 :controller => 'repositories',
248 :format => false,
249 :format => false,
249 :constraints => {
250 :constraints => {
250 :action => /(browse|show|entry|raw|annotate|diff)/,
251 :action => /(browse|show|entry|raw|annotate|diff)/,
251 :rev => /[a-z0-9\.\-_]+/
252 :rev => /[a-z0-9\.\-_]+/
252 }
253 }
253 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
254 get 'projects/:id/repository/:repository_id/:action(/*path(.:ext))',
254 :controller => 'repositories',
255 :controller => 'repositories',
255 :action => /(browse|show|entry|raw|changes|annotate|diff)/
256 :action => /(browse|show|entry|raw|changes|annotate|diff)/
256 get 'projects/:id/repository/:action(/*path(.:ext))',
257 get 'projects/:id/repository/:action(/*path(.:ext))',
257 :controller => 'repositories',
258 :controller => 'repositories',
258 :action => /(browse|show|entry|raw|changes|annotate|diff)/
259 :action => /(browse|show|entry|raw|changes|annotate|diff)/
259
260
260 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
261 get 'projects/:id/repository/:repository_id', :to => 'repositories#show', :path => nil
261 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
262 get 'projects/:id/repository', :to => 'repositories#show', :path => nil
262
263
263 # additional routes for having the file name at the end of url
264 # additional routes for having the file name at the end of url
264 match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
265 match 'attachments/:id/:filename', :controller => 'attachments', :action => 'show', :id => /\d+/, :filename => /.*/, :via => :get
265 match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
266 match 'attachments/download/:id/:filename', :controller => 'attachments', :action => 'download', :id => /\d+/, :filename => /.*/, :via => :get
266 match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
267 match 'attachments/download/:id', :controller => 'attachments', :action => 'download', :id => /\d+/, :via => :get
267 match 'attachments/thumbnail/:id(/:size)', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get, :size => /\d+/
268 match 'attachments/thumbnail/:id(/:size)', :controller => 'attachments', :action => 'thumbnail', :id => /\d+/, :via => :get, :size => /\d+/
268 resources :attachments, :only => [:show, :destroy]
269 resources :attachments, :only => [:show, :destroy]
269
270
270 resources :groups do
271 resources :groups do
271 member do
272 member do
272 get 'autocomplete_for_user'
273 get 'autocomplete_for_user'
273 end
274 end
274 end
275 end
275
276
276 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
277 match 'groups/:id/users', :controller => 'groups', :action => 'add_users', :id => /\d+/, :via => :post, :as => 'group_users'
277 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
278 match 'groups/:id/users/:user_id', :controller => 'groups', :action => 'remove_user', :id => /\d+/, :via => :delete, :as => 'group_user'
278 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
279 match 'groups/destroy_membership/:id', :controller => 'groups', :action => 'destroy_membership', :id => /\d+/, :via => :post
279 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
280 match 'groups/edit_membership/:id', :controller => 'groups', :action => 'edit_membership', :id => /\d+/, :via => :post
280
281
281 resources :trackers, :except => :show do
282 resources :trackers, :except => :show do
282 collection do
283 collection do
283 match 'fields', :via => [:get, :post]
284 match 'fields', :via => [:get, :post]
284 end
285 end
285 end
286 end
286 resources :issue_statuses, :except => :show do
287 resources :issue_statuses, :except => :show do
287 collection do
288 collection do
288 post 'update_issue_done_ratio'
289 post 'update_issue_done_ratio'
289 end
290 end
290 end
291 end
291 resources :custom_fields, :except => :show
292 resources :custom_fields, :except => :show
292 resources :roles do
293 resources :roles do
293 collection do
294 collection do
294 match 'permissions', :via => [:get, :post]
295 match 'permissions', :via => [:get, :post]
295 end
296 end
296 end
297 end
297 resources :enumerations, :except => :show
298 resources :enumerations, :except => :show
298 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
299 match 'enumerations/:type', :to => 'enumerations#index', :via => :get
299
300
300 get 'projects/:id/search', :controller => 'search', :action => 'index'
301 get 'projects/:id/search', :controller => 'search', :action => 'index'
301 get 'search', :controller => 'search', :action => 'index'
302 get 'search', :controller => 'search', :action => 'index'
302
303
303 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
304 match 'mail_handler', :controller => 'mail_handler', :action => 'index', :via => :post
304
305
305 match 'admin', :controller => 'admin', :action => 'index', :via => :get
306 match 'admin', :controller => 'admin', :action => 'index', :via => :get
306 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
307 match 'admin/projects', :controller => 'admin', :action => 'projects', :via => :get
307 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
308 match 'admin/plugins', :controller => 'admin', :action => 'plugins', :via => :get
308 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
309 match 'admin/info', :controller => 'admin', :action => 'info', :via => :get
309 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
310 match 'admin/test_email', :controller => 'admin', :action => 'test_email', :via => :get
310 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
311 match 'admin/default_configuration', :controller => 'admin', :action => 'default_configuration', :via => :post
311
312
312 resources :auth_sources do
313 resources :auth_sources do
313 member do
314 member do
314 get 'test_connection'
315 get 'test_connection'
315 end
316 end
316 end
317 end
317
318
318 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
319 match 'workflows', :controller => 'workflows', :action => 'index', :via => :get
319 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
320 match 'workflows/edit', :controller => 'workflows', :action => 'edit', :via => [:get, :post]
320 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
321 match 'workflows/permissions', :controller => 'workflows', :action => 'permissions', :via => [:get, :post]
321 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
322 match 'workflows/copy', :controller => 'workflows', :action => 'copy', :via => [:get, :post]
322 match 'settings', :controller => 'settings', :action => 'index', :via => :get
323 match 'settings', :controller => 'settings', :action => 'index', :via => :get
323 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
324 match 'settings/edit', :controller => 'settings', :action => 'edit', :via => [:get, :post]
324 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post]
325 match 'settings/plugin/:id', :controller => 'settings', :action => 'plugin', :via => [:get, :post]
325
326
326 match 'sys/projects', :to => 'sys#projects', :via => :get
327 match 'sys/projects', :to => 'sys#projects', :via => :get
327 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
328 match 'sys/projects/:id/repository', :to => 'sys#create_project_repository', :via => :post
328 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
329 match 'sys/fetch_changesets', :to => 'sys#fetch_changesets', :via => :get
329
330
330 match 'uploads', :to => 'attachments#upload', :via => :post
331 match 'uploads', :to => 'attachments#upload', :via => :post
331
332
332 get 'robots.txt', :to => 'welcome#robots'
333 get 'robots.txt', :to => 'welcome#robots'
333
334
334 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
335 Dir.glob File.expand_path("plugins/*", Rails.root) do |plugin_dir|
335 file = File.join(plugin_dir, "config/routes.rb")
336 file = File.join(plugin_dir, "config/routes.rb")
336 if File.exists?(file)
337 if File.exists?(file)
337 begin
338 begin
338 instance_eval File.read(file)
339 instance_eval File.read(file)
339 rescue Exception => e
340 rescue Exception => e
340 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
341 puts "An error occurred while loading the routes definition of #{File.basename(plugin_dir)} plugin (#{file}): #{e.message}."
341 exit 1
342 exit 1
342 end
343 end
343 end
344 end
344 end
345 end
345 end
346 end
@@ -1,566 +1,566
1 # Copyright (c) 2005 Rick Olson
1 # Copyright (c) 2005 Rick Olson
2 #
2 #
3 # Permission is hereby granted, free of charge, to any person obtaining
3 # Permission is hereby granted, free of charge, to any person obtaining
4 # a copy of this software and associated documentation files (the
4 # a copy of this software and associated documentation files (the
5 # "Software"), to deal in the Software without restriction, including
5 # "Software"), to deal in the Software without restriction, including
6 # without limitation the rights to use, copy, modify, merge, publish,
6 # without limitation the rights to use, copy, modify, merge, publish,
7 # distribute, sublicense, and/or sell copies of the Software, and to
7 # distribute, sublicense, and/or sell copies of the Software, and to
8 # permit persons to whom the Software is furnished to do so, subject to
8 # permit persons to whom the Software is furnished to do so, subject to
9 # the following conditions:
9 # the following conditions:
10 #
10 #
11 # The above copyright notice and this permission notice shall be
11 # The above copyright notice and this permission notice shall be
12 # included in all copies or substantial portions of the Software.
12 # included in all copies or substantial portions of the Software.
13 #
13 #
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
15 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
16 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
17 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
18 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
19 # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20 # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
21
22 module ActiveRecord #:nodoc:
22 module ActiveRecord #:nodoc:
23 module Acts #:nodoc:
23 module Acts #:nodoc:
24 # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
24 # Specify this act if you want to save a copy of the row in a versioned table. This assumes there is a
25 # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
25 # versioned table ready and that your model has a version field. This works with optimistic locking if the lock_version
26 # column is present as well.
26 # column is present as well.
27 #
27 #
28 # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
28 # The class for the versioned model is derived the first time it is seen. Therefore, if you change your database schema you have to restart
29 # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
29 # your container for the changes to be reflected. In development mode this usually means restarting WEBrick.
30 #
30 #
31 # class Page < ActiveRecord::Base
31 # class Page < ActiveRecord::Base
32 # # assumes pages_versions table
32 # # assumes pages_versions table
33 # acts_as_versioned
33 # acts_as_versioned
34 # end
34 # end
35 #
35 #
36 # Example:
36 # Example:
37 #
37 #
38 # page = Page.create(:title => 'hello world!')
38 # page = Page.create(:title => 'hello world!')
39 # page.version # => 1
39 # page.version # => 1
40 #
40 #
41 # page.title = 'hello world'
41 # page.title = 'hello world'
42 # page.save
42 # page.save
43 # page.version # => 2
43 # page.version # => 2
44 # page.versions.size # => 2
44 # page.versions.size # => 2
45 #
45 #
46 # page.revert_to(1) # using version number
46 # page.revert_to(1) # using version number
47 # page.title # => 'hello world!'
47 # page.title # => 'hello world!'
48 #
48 #
49 # page.revert_to(page.versions.last) # using versioned instance
49 # page.revert_to(page.versions.last) # using versioned instance
50 # page.title # => 'hello world'
50 # page.title # => 'hello world'
51 #
51 #
52 # page.versions.earliest # efficient query to find the first version
52 # page.versions.earliest # efficient query to find the first version
53 # page.versions.latest # efficient query to find the most recently created version
53 # page.versions.latest # efficient query to find the most recently created version
54 #
54 #
55 #
55 #
56 # Simple Queries to page between versions
56 # Simple Queries to page between versions
57 #
57 #
58 # page.versions.before(version)
58 # page.versions.before(version)
59 # page.versions.after(version)
59 # page.versions.after(version)
60 #
60 #
61 # Access the previous/next versions from the versioned model itself
61 # Access the previous/next versions from the versioned model itself
62 #
62 #
63 # version = page.versions.latest
63 # version = page.versions.latest
64 # version.previous # go back one version
64 # version.previous # go back one version
65 # version.next # go forward one version
65 # version.next # go forward one version
66 #
66 #
67 # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
67 # See ActiveRecord::Acts::Versioned::ClassMethods#acts_as_versioned for configuration options
68 module Versioned
68 module Versioned
69 CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
69 CALLBACKS = [:set_new_version, :save_version_on_create, :save_version?, :clear_altered_attributes]
70 def self.included(base) # :nodoc:
70 def self.included(base) # :nodoc:
71 base.extend ClassMethods
71 base.extend ClassMethods
72 end
72 end
73
73
74 module ClassMethods
74 module ClassMethods
75 # == Configuration options
75 # == Configuration options
76 #
76 #
77 # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
77 # * <tt>class_name</tt> - versioned model class name (default: PageVersion in the above example)
78 # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
78 # * <tt>table_name</tt> - versioned model table name (default: page_versions in the above example)
79 # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
79 # * <tt>foreign_key</tt> - foreign key used to relate the versioned model to the original model (default: page_id in the above example)
80 # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
80 # * <tt>inheritance_column</tt> - name of the column to save the model's inheritance_column value for STI. (default: versioned_type)
81 # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
81 # * <tt>version_column</tt> - name of the column in the model that keeps the version number (default: version)
82 # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
82 # * <tt>sequence_name</tt> - name of the custom sequence to be used by the versioned model.
83 # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
83 # * <tt>limit</tt> - number of revisions to keep, defaults to unlimited
84 # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
84 # * <tt>if</tt> - symbol of method to check before saving a new version. If this method returns false, a new version is not saved.
85 # For finer control, pass either a Proc or modify Model#version_condition_met?
85 # For finer control, pass either a Proc or modify Model#version_condition_met?
86 #
86 #
87 # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
87 # acts_as_versioned :if => Proc.new { |auction| !auction.expired? }
88 #
88 #
89 # or...
89 # or...
90 #
90 #
91 # class Auction
91 # class Auction
92 # def version_condition_met? # totally bypasses the <tt>:if</tt> option
92 # def version_condition_met? # totally bypasses the <tt>:if</tt> option
93 # !expired?
93 # !expired?
94 # end
94 # end
95 # end
95 # end
96 #
96 #
97 # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
97 # * <tt>if_changed</tt> - Simple way of specifying attributes that are required to be changed before saving a model. This takes
98 # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
98 # either a symbol or array of symbols. WARNING - This will attempt to overwrite any attribute setters you may have.
99 # Use this instead if you want to write your own attribute setters (and ignore if_changed):
99 # Use this instead if you want to write your own attribute setters (and ignore if_changed):
100 #
100 #
101 # def name=(new_name)
101 # def name=(new_name)
102 # write_changed_attribute :name, new_name
102 # write_changed_attribute :name, new_name
103 # end
103 # end
104 #
104 #
105 # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
105 # * <tt>extend</tt> - Lets you specify a module to be mixed in both the original and versioned models. You can also just pass a block
106 # to create an anonymous mixin:
106 # to create an anonymous mixin:
107 #
107 #
108 # class Auction
108 # class Auction
109 # acts_as_versioned do
109 # acts_as_versioned do
110 # def started?
110 # def started?
111 # !started_at.nil?
111 # !started_at.nil?
112 # end
112 # end
113 # end
113 # end
114 # end
114 # end
115 #
115 #
116 # or...
116 # or...
117 #
117 #
118 # module AuctionExtension
118 # module AuctionExtension
119 # def started?
119 # def started?
120 # !started_at.nil?
120 # !started_at.nil?
121 # end
121 # end
122 # end
122 # end
123 # class Auction
123 # class Auction
124 # acts_as_versioned :extend => AuctionExtension
124 # acts_as_versioned :extend => AuctionExtension
125 # end
125 # end
126 #
126 #
127 # Example code:
127 # Example code:
128 #
128 #
129 # @auction = Auction.find(1)
129 # @auction = Auction.find(1)
130 # @auction.started?
130 # @auction.started?
131 # @auction.versions.first.started?
131 # @auction.versions.first.started?
132 #
132 #
133 # == Database Schema
133 # == Database Schema
134 #
134 #
135 # The model that you're versioning needs to have a 'version' attribute. The model is versioned
135 # The model that you're versioning needs to have a 'version' attribute. The model is versioned
136 # into a table called #{model}_versions where the model name is singlular. The _versions table should
136 # into a table called #{model}_versions where the model name is singlular. The _versions table should
137 # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
137 # contain all the fields you want versioned, the same version column, and a #{model}_id foreign key field.
138 #
138 #
139 # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
139 # A lock_version field is also accepted if your model uses Optimistic Locking. If your table uses Single Table inheritance,
140 # then that field is reflected in the versioned model as 'versioned_type' by default.
140 # then that field is reflected in the versioned model as 'versioned_type' by default.
141 #
141 #
142 # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
142 # Acts_as_versioned comes prepared with the ActiveRecord::Acts::Versioned::ActMethods::ClassMethods#create_versioned_table
143 # method, perfect for a migration. It will also create the version column if the main model does not already have it.
143 # method, perfect for a migration. It will also create the version column if the main model does not already have it.
144 #
144 #
145 # class AddVersions < ActiveRecord::Migration
145 # class AddVersions < ActiveRecord::Migration
146 # def self.up
146 # def self.up
147 # # create_versioned_table takes the same options hash
147 # # create_versioned_table takes the same options hash
148 # # that create_table does
148 # # that create_table does
149 # Post.create_versioned_table
149 # Post.create_versioned_table
150 # end
150 # end
151 #
151 #
152 # def self.down
152 # def self.down
153 # Post.drop_versioned_table
153 # Post.drop_versioned_table
154 # end
154 # end
155 # end
155 # end
156 #
156 #
157 # == Changing What Fields Are Versioned
157 # == Changing What Fields Are Versioned
158 #
158 #
159 # By default, acts_as_versioned will version all but these fields:
159 # By default, acts_as_versioned will version all but these fields:
160 #
160 #
161 # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
161 # [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
162 #
162 #
163 # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
163 # You can add or change those by modifying #non_versioned_columns. Note that this takes strings and not symbols.
164 #
164 #
165 # class Post < ActiveRecord::Base
165 # class Post < ActiveRecord::Base
166 # acts_as_versioned
166 # acts_as_versioned
167 # self.non_versioned_columns << 'comments_count'
167 # self.non_versioned_columns << 'comments_count'
168 # end
168 # end
169 #
169 #
170 def acts_as_versioned(options = {}, &extension)
170 def acts_as_versioned(options = {}, &extension)
171 # don't allow multiple calls
171 # don't allow multiple calls
172 return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
172 return if self.included_modules.include?(ActiveRecord::Acts::Versioned::ActMethods)
173
173
174 send :include, ActiveRecord::Acts::Versioned::ActMethods
174 send :include, ActiveRecord::Acts::Versioned::ActMethods
175
175
176 cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
176 cattr_accessor :versioned_class_name, :versioned_foreign_key, :versioned_table_name, :versioned_inheritance_column,
177 :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
177 :version_column, :max_version_limit, :track_altered_attributes, :version_condition, :version_sequence_name, :non_versioned_columns,
178 :version_association_options
178 :version_association_options
179
179
180 # legacy
180 # legacy
181 alias_method :non_versioned_fields, :non_versioned_columns
181 alias_method :non_versioned_fields, :non_versioned_columns
182 alias_method :non_versioned_fields=, :non_versioned_columns=
182 alias_method :non_versioned_fields=, :non_versioned_columns=
183
183
184 class << self
184 class << self
185 alias_method :non_versioned_fields, :non_versioned_columns
185 alias_method :non_versioned_fields, :non_versioned_columns
186 alias_method :non_versioned_fields=, :non_versioned_columns=
186 alias_method :non_versioned_fields=, :non_versioned_columns=
187 end
187 end
188
188
189 send :attr_accessor, :altered_attributes
189 send :attr_accessor, :altered_attributes
190
190
191 self.versioned_class_name = options[:class_name] || "Version"
191 self.versioned_class_name = options[:class_name] || "Version"
192 self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
192 self.versioned_foreign_key = options[:foreign_key] || self.to_s.foreign_key
193 self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
193 self.versioned_table_name = options[:table_name] || "#{table_name_prefix}#{base_class.name.demodulize.underscore}_versions#{table_name_suffix}"
194 self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
194 self.versioned_inheritance_column = options[:inheritance_column] || "versioned_#{inheritance_column}"
195 self.version_column = options[:version_column] || 'version'
195 self.version_column = options[:version_column] || 'version'
196 self.version_sequence_name = options[:sequence_name]
196 self.version_sequence_name = options[:sequence_name]
197 self.max_version_limit = options[:limit].to_i
197 self.max_version_limit = options[:limit].to_i
198 self.version_condition = options[:if] || true
198 self.version_condition = options[:if] || true
199 self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
199 self.non_versioned_columns = [self.primary_key, inheritance_column, 'version', 'lock_version', versioned_inheritance_column]
200 self.version_association_options = {
200 self.version_association_options = {
201 :class_name => "#{self.to_s}::#{versioned_class_name}",
201 :class_name => "#{self.to_s}::#{versioned_class_name}",
202 :foreign_key => versioned_foreign_key,
202 :foreign_key => versioned_foreign_key,
203 :dependent => :delete_all
203 :dependent => :delete_all
204 }.merge(options[:association_options] || {})
204 }.merge(options[:association_options] || {})
205
205
206 if block_given?
206 if block_given?
207 extension_module_name = "#{versioned_class_name}Extension"
207 extension_module_name = "#{versioned_class_name}Extension"
208 silence_warnings do
208 silence_warnings do
209 self.const_set(extension_module_name, Module.new(&extension))
209 self.const_set(extension_module_name, Module.new(&extension))
210 end
210 end
211
211
212 options[:extend] = self.const_get(extension_module_name)
212 options[:extend] = self.const_get(extension_module_name)
213 end
213 end
214
214
215 class_eval do
215 class_eval do
216 has_many :versions, version_association_options do
216 has_many :versions, version_association_options do
217 # finds earliest version of this record
217 # finds earliest version of this record
218 def earliest
218 def earliest
219 @earliest ||= find(:first, :order => 'version')
219 @earliest ||= find(:first, :order => 'version')
220 end
220 end
221
221
222 # find latest version of this record
222 # find latest version of this record
223 def latest
223 def latest
224 @latest ||= find(:first, :order => 'version desc')
224 @latest ||= find(:first, :order => 'version desc')
225 end
225 end
226 end
226 end
227 before_save :set_new_version
227 before_save :set_new_version
228 after_create :save_version_on_create
228 after_create :save_version_on_create
229 after_update :save_version
229 after_update :save_version
230 after_save :clear_old_versions
230 after_save :clear_old_versions
231 after_save :clear_altered_attributes
231 after_save :clear_altered_attributes
232
232
233 unless options[:if_changed].nil?
233 unless options[:if_changed].nil?
234 self.track_altered_attributes = true
234 self.track_altered_attributes = true
235 options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
235 options[:if_changed] = [options[:if_changed]] unless options[:if_changed].is_a?(Array)
236 options[:if_changed].each do |attr_name|
236 options[:if_changed].each do |attr_name|
237 define_method("#{attr_name}=") do |value|
237 define_method("#{attr_name}=") do |value|
238 write_changed_attribute attr_name, value
238 write_changed_attribute attr_name, value
239 end
239 end
240 end
240 end
241 end
241 end
242
242
243 include options[:extend] if options[:extend].is_a?(Module)
243 include options[:extend] if options[:extend].is_a?(Module)
244 end
244 end
245
245
246 # create the dynamic versioned model
246 # create the dynamic versioned model
247 const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
247 const_set(versioned_class_name, Class.new(ActiveRecord::Base)).class_eval do
248 def self.reloadable? ; false ; end
248 def self.reloadable? ; false ; end
249 # find first version before the given version
249 # find first version before the given version
250 def self.before(version)
250 def self.before(version)
251 find :first, :order => 'version desc',
251 find :first, :order => 'version desc',
252 :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
252 :conditions => ["#{original_class.versioned_foreign_key} = ? and version < ?", version.send(original_class.versioned_foreign_key), version.version]
253 end
253 end
254
254
255 # find first version after the given version.
255 # find first version after the given version.
256 def self.after(version)
256 def self.after(version)
257 find :first, :order => 'version',
257 find :first, :order => 'version',
258 :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
258 :conditions => ["#{original_class.versioned_foreign_key} = ? and version > ?", version.send(original_class.versioned_foreign_key), version.version]
259 end
259 end
260
260
261 def previous
261 def previous
262 self.class.before(self)
262 self.class.before(self)
263 end
263 end
264
264
265 def next
265 def next
266 self.class.after(self)
266 self.class.after(self)
267 end
267 end
268
268
269 def versions_count
269 def versions_count
270 page.version
270 page.version
271 end
271 end
272 end
272 end
273
273
274 versioned_class.cattr_accessor :original_class
274 versioned_class.cattr_accessor :original_class
275 versioned_class.original_class = self
275 versioned_class.original_class = self
276 versioned_class.table_name = versioned_table_name
276 versioned_class.table_name = versioned_table_name
277 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
277 versioned_class.belongs_to self.to_s.demodulize.underscore.to_sym,
278 :class_name => "::#{self.to_s}",
278 :class_name => "::#{self.to_s}",
279 :foreign_key => versioned_foreign_key
279 :foreign_key => versioned_foreign_key
280 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
280 versioned_class.send :include, options[:extend] if options[:extend].is_a?(Module)
281 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
281 versioned_class.set_sequence_name version_sequence_name if version_sequence_name
282 end
282 end
283 end
283 end
284
284
285 module ActMethods
285 module ActMethods
286 def self.included(base) # :nodoc:
286 def self.included(base) # :nodoc:
287 base.extend ClassMethods
287 base.extend ClassMethods
288 end
288 end
289
289
290 # Finds a specific version of this record
290 # Finds a specific version of this record
291 def find_version(version = nil)
291 def find_version(version = nil)
292 self.class.find_version(id, version)
292 self.class.find_version(id, version)
293 end
293 end
294
294
295 # Saves a version of the model if applicable
295 # Saves a version of the model if applicable
296 def save_version
296 def save_version
297 save_version_on_create if save_version?
297 save_version_on_create if save_version?
298 end
298 end
299
299
300 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
300 # Saves a version of the model in the versioned table. This is called in the after_save callback by default
301 def save_version_on_create
301 def save_version_on_create
302 rev = self.class.versioned_class.new
302 rev = self.class.versioned_class.new
303 self.clone_versioned_model(self, rev)
303 self.clone_versioned_model(self, rev)
304 rev.version = send(self.class.version_column)
304 rev.version = send(self.class.version_column)
305 rev.send("#{self.class.versioned_foreign_key}=", self.id)
305 rev.send("#{self.class.versioned_foreign_key}=", self.id)
306 rev.save
306 rev.save
307 end
307 end
308
308
309 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
309 # Clears old revisions if a limit is set with the :limit option in <tt>acts_as_versioned</tt>.
310 # Override this method to set your own criteria for clearing old versions.
310 # Override this method to set your own criteria for clearing old versions.
311 def clear_old_versions
311 def clear_old_versions
312 return if self.class.max_version_limit == 0
312 return if self.class.max_version_limit == 0
313 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
313 excess_baggage = send(self.class.version_column).to_i - self.class.max_version_limit
314 if excess_baggage > 0
314 if excess_baggage > 0
315 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
315 sql = "DELETE FROM #{self.class.versioned_table_name} WHERE version <= #{excess_baggage} AND #{self.class.versioned_foreign_key} = #{self.id}"
316 self.class.versioned_class.connection.execute sql
316 self.class.versioned_class.connection.execute sql
317 end
317 end
318 end
318 end
319
319
320 def versions_count
320 def versions_count
321 version
321 version
322 end
322 end
323
323
324 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
324 # Reverts a model to a given version. Takes either a version number or an instance of the versioned model
325 def revert_to(version)
325 def revert_to(version)
326 if version.is_a?(self.class.versioned_class)
326 if version.is_a?(self.class.versioned_class)
327 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
327 return false unless version.send(self.class.versioned_foreign_key) == self.id and !version.new_record?
328 else
328 else
329 return false unless version = versions.find_by_version(version)
329 return false unless version = versions.find_by_version(version)
330 end
330 end
331 self.clone_versioned_model(version, self)
331 self.clone_versioned_model(version, self)
332 self.send("#{self.class.version_column}=", version.version)
332 self.send("#{self.class.version_column}=", version.version)
333 true
333 true
334 end
334 end
335
335
336 # Reverts a model to a given version and saves the model.
336 # Reverts a model to a given version and saves the model.
337 # Takes either a version number or an instance of the versioned model
337 # Takes either a version number or an instance of the versioned model
338 def revert_to!(version)
338 def revert_to!(version)
339 revert_to(version) ? save_without_revision : false
339 revert_to(version) ? save_without_revision : false
340 end
340 end
341
341
342 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
342 # Temporarily turns off Optimistic Locking while saving. Used when reverting so that a new version is not created.
343 def save_without_revision
343 def save_without_revision
344 save_without_revision!
344 save_without_revision!
345 true
345 true
346 rescue
346 rescue
347 false
347 false
348 end
348 end
349
349
350 def save_without_revision!
350 def save_without_revision!
351 without_locking do
351 without_locking do
352 without_revision do
352 without_revision do
353 save!
353 save!
354 end
354 end
355 end
355 end
356 end
356 end
357
357
358 # Returns an array of attribute keys that are versioned. See non_versioned_columns
358 # Returns an array of attribute keys that are versioned. See non_versioned_columns
359 def versioned_attributes
359 def versioned_attributes
360 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
360 self.attributes.keys.select { |k| !self.class.non_versioned_columns.include?(k) }
361 end
361 end
362
362
363 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
363 # If called with no parameters, gets whether the current model has changed and needs to be versioned.
364 # If called with a single parameter, gets whether the parameter has changed.
364 # If called with a single parameter, gets whether the parameter has changed.
365 def changed?(attr_name = nil)
365 def changed?(attr_name = nil)
366 attr_name.nil? ?
366 attr_name.nil? ?
367 (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
367 (!self.class.track_altered_attributes || (altered_attributes && altered_attributes.length > 0)) :
368 (altered_attributes && altered_attributes.include?(attr_name.to_s))
368 (altered_attributes && altered_attributes.include?(attr_name.to_s))
369 end
369 end
370
370
371 # keep old dirty? method
371 # keep old dirty? method
372 alias_method :dirty?, :changed?
372 alias_method :dirty?, :changed?
373
373
374 # Clones a model. Used when saving a new version or reverting a model's version.
374 # Clones a model. Used when saving a new version or reverting a model's version.
375 def clone_versioned_model(orig_model, new_model)
375 def clone_versioned_model(orig_model, new_model)
376 self.versioned_attributes.each do |key|
376 self.versioned_attributes.each do |key|
377 new_model.send("#{key}=", orig_model.send(key)) if orig_model.has_attribute?(key)
377 new_model.send("#{key}=", orig_model.send(key)) if orig_model.respond_to?(key)
378 end
378 end
379
379
380 if self.class.columns_hash.include?(self.class.inheritance_column)
380 if self.class.columns_hash.include?(self.class.inheritance_column)
381 if orig_model.is_a?(self.class.versioned_class)
381 if orig_model.is_a?(self.class.versioned_class)
382 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
382 new_model[new_model.class.inheritance_column] = orig_model[self.class.versioned_inheritance_column]
383 elsif new_model.is_a?(self.class.versioned_class)
383 elsif new_model.is_a?(self.class.versioned_class)
384 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
384 new_model[self.class.versioned_inheritance_column] = orig_model[orig_model.class.inheritance_column]
385 end
385 end
386 end
386 end
387 end
387 end
388
388
389 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
389 # Checks whether a new version shall be saved or not. Calls <tt>version_condition_met?</tt> and <tt>changed?</tt>.
390 def save_version?
390 def save_version?
391 version_condition_met? && changed?
391 version_condition_met? && changed?
392 end
392 end
393
393
394 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
394 # Checks condition set in the :if option to check whether a revision should be created or not. Override this for
395 # custom version condition checking.
395 # custom version condition checking.
396 def version_condition_met?
396 def version_condition_met?
397 case
397 case
398 when version_condition.is_a?(Symbol)
398 when version_condition.is_a?(Symbol)
399 send(version_condition)
399 send(version_condition)
400 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
400 when version_condition.respond_to?(:call) && (version_condition.arity == 1 || version_condition.arity == -1)
401 version_condition.call(self)
401 version_condition.call(self)
402 else
402 else
403 version_condition
403 version_condition
404 end
404 end
405 end
405 end
406
406
407 # Executes the block with the versioning callbacks disabled.
407 # Executes the block with the versioning callbacks disabled.
408 #
408 #
409 # @foo.without_revision do
409 # @foo.without_revision do
410 # @foo.save
410 # @foo.save
411 # end
411 # end
412 #
412 #
413 def without_revision(&block)
413 def without_revision(&block)
414 self.class.without_revision(&block)
414 self.class.without_revision(&block)
415 end
415 end
416
416
417 # Turns off optimistic locking for the duration of the block
417 # Turns off optimistic locking for the duration of the block
418 #
418 #
419 # @foo.without_locking do
419 # @foo.without_locking do
420 # @foo.save
420 # @foo.save
421 # end
421 # end
422 #
422 #
423 def without_locking(&block)
423 def without_locking(&block)
424 self.class.without_locking(&block)
424 self.class.without_locking(&block)
425 end
425 end
426
426
427 def empty_callback() end #:nodoc:
427 def empty_callback() end #:nodoc:
428
428
429 protected
429 protected
430 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
430 # sets the new version before saving, unless you're using optimistic locking. In that case, let it take care of the version.
431 def set_new_version
431 def set_new_version
432 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
432 self.send("#{self.class.version_column}=", self.next_version) if new_record? || (!locking_enabled? && save_version?)
433 end
433 end
434
434
435 # Gets the next available version for the current record, or 1 for a new record
435 # Gets the next available version for the current record, or 1 for a new record
436 def next_version
436 def next_version
437 return 1 if new_record?
437 return 1 if new_record?
438 (versions.calculate(:max, :version) || 0) + 1
438 (versions.calculate(:max, :version) || 0) + 1
439 end
439 end
440
440
441 # clears current changed attributes. Called after save.
441 # clears current changed attributes. Called after save.
442 def clear_altered_attributes
442 def clear_altered_attributes
443 self.altered_attributes = []
443 self.altered_attributes = []
444 end
444 end
445
445
446 def write_changed_attribute(attr_name, attr_value)
446 def write_changed_attribute(attr_name, attr_value)
447 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
447 # Convert to db type for comparison. Avoids failing Float<=>String comparisons.
448 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
448 attr_value_for_db = self.class.columns_hash[attr_name.to_s].type_cast(attr_value)
449 (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
449 (self.altered_attributes ||= []) << attr_name.to_s unless self.changed?(attr_name) || self.send(attr_name) == attr_value_for_db
450 write_attribute(attr_name, attr_value_for_db)
450 write_attribute(attr_name, attr_value_for_db)
451 end
451 end
452
452
453 module ClassMethods
453 module ClassMethods
454 # Finds a specific version of a specific row of this model
454 # Finds a specific version of a specific row of this model
455 def find_version(id, version = nil)
455 def find_version(id, version = nil)
456 return find(id) unless version
456 return find(id) unless version
457
457
458 conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
458 conditions = ["#{versioned_foreign_key} = ? AND version = ?", id, version]
459 options = { :conditions => conditions, :limit => 1 }
459 options = { :conditions => conditions, :limit => 1 }
460
460
461 if result = find_versions(id, options).first
461 if result = find_versions(id, options).first
462 result
462 result
463 else
463 else
464 raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
464 raise RecordNotFound, "Couldn't find #{name} with ID=#{id} and VERSION=#{version}"
465 end
465 end
466 end
466 end
467
467
468 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
468 # Finds versions of a specific model. Takes an options hash like <tt>find</tt>
469 def find_versions(id, options = {})
469 def find_versions(id, options = {})
470 versioned_class.find :all, {
470 versioned_class.find :all, {
471 :conditions => ["#{versioned_foreign_key} = ?", id],
471 :conditions => ["#{versioned_foreign_key} = ?", id],
472 :order => 'version' }.merge(options)
472 :order => 'version' }.merge(options)
473 end
473 end
474
474
475 # Returns an array of columns that are versioned. See non_versioned_columns
475 # Returns an array of columns that are versioned. See non_versioned_columns
476 def versioned_columns
476 def versioned_columns
477 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
477 self.columns.select { |c| !non_versioned_columns.include?(c.name) }
478 end
478 end
479
479
480 # Returns an instance of the dynamic versioned model
480 # Returns an instance of the dynamic versioned model
481 def versioned_class
481 def versioned_class
482 const_get versioned_class_name
482 const_get versioned_class_name
483 end
483 end
484
484
485 # Rake migration task to create the versioned table using options passed to acts_as_versioned
485 # Rake migration task to create the versioned table using options passed to acts_as_versioned
486 def create_versioned_table(create_table_options = {})
486 def create_versioned_table(create_table_options = {})
487 # create version column in main table if it does not exist
487 # create version column in main table if it does not exist
488 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
488 if !self.content_columns.find { |c| %w(version lock_version).include? c.name }
489 self.connection.add_column table_name, :version, :integer
489 self.connection.add_column table_name, :version, :integer
490 end
490 end
491
491
492 self.connection.create_table(versioned_table_name, create_table_options) do |t|
492 self.connection.create_table(versioned_table_name, create_table_options) do |t|
493 t.column versioned_foreign_key, :integer
493 t.column versioned_foreign_key, :integer
494 t.column :version, :integer
494 t.column :version, :integer
495 end
495 end
496
496
497 updated_col = nil
497 updated_col = nil
498 self.versioned_columns.each do |col|
498 self.versioned_columns.each do |col|
499 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
499 updated_col = col if !updated_col && %(updated_at updated_on).include?(col.name)
500 self.connection.add_column versioned_table_name, col.name, col.type,
500 self.connection.add_column versioned_table_name, col.name, col.type,
501 :limit => col.limit,
501 :limit => col.limit,
502 :default => col.default,
502 :default => col.default,
503 :scale => col.scale,
503 :scale => col.scale,
504 :precision => col.precision
504 :precision => col.precision
505 end
505 end
506
506
507 if type_col = self.columns_hash[inheritance_column]
507 if type_col = self.columns_hash[inheritance_column]
508 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
508 self.connection.add_column versioned_table_name, versioned_inheritance_column, type_col.type,
509 :limit => type_col.limit,
509 :limit => type_col.limit,
510 :default => type_col.default,
510 :default => type_col.default,
511 :scale => type_col.scale,
511 :scale => type_col.scale,
512 :precision => type_col.precision
512 :precision => type_col.precision
513 end
513 end
514
514
515 if updated_col.nil?
515 if updated_col.nil?
516 self.connection.add_column versioned_table_name, :updated_at, :timestamp
516 self.connection.add_column versioned_table_name, :updated_at, :timestamp
517 end
517 end
518 end
518 end
519
519
520 # Rake migration task to drop the versioned table
520 # Rake migration task to drop the versioned table
521 def drop_versioned_table
521 def drop_versioned_table
522 self.connection.drop_table versioned_table_name
522 self.connection.drop_table versioned_table_name
523 end
523 end
524
524
525 # Executes the block with the versioning callbacks disabled.
525 # Executes the block with the versioning callbacks disabled.
526 #
526 #
527 # Foo.without_revision do
527 # Foo.without_revision do
528 # @foo.save
528 # @foo.save
529 # end
529 # end
530 #
530 #
531 def without_revision(&block)
531 def without_revision(&block)
532 class_eval do
532 class_eval do
533 CALLBACKS.each do |attr_name|
533 CALLBACKS.each do |attr_name|
534 alias_method "orig_#{attr_name}".to_sym, attr_name
534 alias_method "orig_#{attr_name}".to_sym, attr_name
535 alias_method attr_name, :empty_callback
535 alias_method attr_name, :empty_callback
536 end
536 end
537 end
537 end
538 block.call
538 block.call
539 ensure
539 ensure
540 class_eval do
540 class_eval do
541 CALLBACKS.each do |attr_name|
541 CALLBACKS.each do |attr_name|
542 alias_method attr_name, "orig_#{attr_name}".to_sym
542 alias_method attr_name, "orig_#{attr_name}".to_sym
543 end
543 end
544 end
544 end
545 end
545 end
546
546
547 # Turns off optimistic locking for the duration of the block
547 # Turns off optimistic locking for the duration of the block
548 #
548 #
549 # Foo.without_locking do
549 # Foo.without_locking do
550 # @foo.save
550 # @foo.save
551 # end
551 # end
552 #
552 #
553 def without_locking(&block)
553 def without_locking(&block)
554 current = ActiveRecord::Base.lock_optimistically
554 current = ActiveRecord::Base.lock_optimistically
555 ActiveRecord::Base.lock_optimistically = false if current
555 ActiveRecord::Base.lock_optimistically = false if current
556 result = block.call
556 result = block.call
557 ActiveRecord::Base.lock_optimistically = true if current
557 ActiveRecord::Base.lock_optimistically = true if current
558 result
558 result
559 end
559 end
560 end
560 end
561 end
561 end
562 end
562 end
563 end
563 end
564 end
564 end
565
565
566 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned No newline at end of file
566 ActiveRecord::Base.send :include, ActiveRecord::Acts::Versioned
@@ -1,242 +1,242
1 require 'redmine/access_control'
1 require 'redmine/access_control'
2 require 'redmine/menu_manager'
2 require 'redmine/menu_manager'
3 require 'redmine/activity'
3 require 'redmine/activity'
4 require 'redmine/search'
4 require 'redmine/search'
5 require 'redmine/custom_field_format'
5 require 'redmine/custom_field_format'
6 require 'redmine/mime_type'
6 require 'redmine/mime_type'
7 require 'redmine/core_ext'
7 require 'redmine/core_ext'
8 require 'redmine/themes'
8 require 'redmine/themes'
9 require 'redmine/hook'
9 require 'redmine/hook'
10 require 'redmine/plugin'
10 require 'redmine/plugin'
11 require 'redmine/notifiable'
11 require 'redmine/notifiable'
12 require 'redmine/wiki_formatting'
12 require 'redmine/wiki_formatting'
13 require 'redmine/scm/base'
13 require 'redmine/scm/base'
14
14
15 begin
15 begin
16 require 'RMagick' unless Object.const_defined?(:Magick)
16 require 'RMagick' unless Object.const_defined?(:Magick)
17 rescue LoadError
17 rescue LoadError
18 # RMagick is not available
18 # RMagick is not available
19 end
19 end
20
20
21 if RUBY_VERSION < '1.9'
21 if RUBY_VERSION < '1.9'
22 require 'fastercsv'
22 require 'fastercsv'
23 else
23 else
24 require 'csv'
24 require 'csv'
25 FCSV = CSV
25 FCSV = CSV
26 end
26 end
27
27
28 Redmine::Scm::Base.add "Subversion"
28 Redmine::Scm::Base.add "Subversion"
29 Redmine::Scm::Base.add "Darcs"
29 Redmine::Scm::Base.add "Darcs"
30 Redmine::Scm::Base.add "Mercurial"
30 Redmine::Scm::Base.add "Mercurial"
31 Redmine::Scm::Base.add "Cvs"
31 Redmine::Scm::Base.add "Cvs"
32 Redmine::Scm::Base.add "Bazaar"
32 Redmine::Scm::Base.add "Bazaar"
33 Redmine::Scm::Base.add "Git"
33 Redmine::Scm::Base.add "Git"
34 Redmine::Scm::Base.add "Filesystem"
34 Redmine::Scm::Base.add "Filesystem"
35
35
36 Redmine::CustomFieldFormat.map do |fields|
36 Redmine::CustomFieldFormat.map do |fields|
37 fields.register 'string'
37 fields.register 'string'
38 fields.register 'text'
38 fields.register 'text'
39 fields.register 'int', :label => :label_integer
39 fields.register 'int', :label => :label_integer
40 fields.register 'float'
40 fields.register 'float'
41 fields.register 'list'
41 fields.register 'list'
42 fields.register 'date'
42 fields.register 'date'
43 fields.register 'bool', :label => :label_boolean
43 fields.register 'bool', :label => :label_boolean
44 fields.register 'user', :only => %w(Issue TimeEntry Version Project), :edit_as => 'list'
44 fields.register 'user', :only => %w(Issue TimeEntry Version Project), :edit_as => 'list'
45 fields.register 'version', :only => %w(Issue TimeEntry Version Project), :edit_as => 'list'
45 fields.register 'version', :only => %w(Issue TimeEntry Version Project), :edit_as => 'list'
46 end
46 end
47
47
48 # Permissions
48 # Permissions
49 Redmine::AccessControl.map do |map|
49 Redmine::AccessControl.map do |map|
50 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
50 map.permission :view_project, {:projects => [:show], :activities => [:index]}, :public => true, :read => true
51 map.permission :search_project, {:search => :index}, :public => true, :read => true
51 map.permission :search_project, {:search => :index}, :public => true, :read => true
52 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
52 map.permission :add_project, {:projects => [:new, :create]}, :require => :loggedin
53 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
53 map.permission :edit_project, {:projects => [:settings, :edit, :update]}, :require => :member
54 map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
54 map.permission :close_project, {:projects => [:close, :reopen]}, :require => :member, :read => true
55 map.permission :select_project_modules, {:projects => :modules}, :require => :member
55 map.permission :select_project_modules, {:projects => :modules}, :require => :member
56 map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :create, :update, :destroy, :autocomplete]}, :require => :member
56 map.permission :manage_members, {:projects => :settings, :members => [:index, :show, :create, :update, :destroy, :autocomplete]}, :require => :member
57 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
57 map.permission :manage_versions, {:projects => :settings, :versions => [:new, :create, :edit, :update, :close_completed, :destroy]}, :require => :member
58 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
58 map.permission :add_subprojects, {:projects => [:new, :create]}, :require => :member
59
59
60 map.project_module :issue_tracking do |map|
60 map.project_module :issue_tracking do |map|
61 # Issue categories
61 # Issue categories
62 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
62 map.permission :manage_categories, {:projects => :settings, :issue_categories => [:index, :show, :new, :create, :edit, :update, :destroy]}, :require => :member
63 # Issues
63 # Issues
64 map.permission :view_issues, {:issues => [:index, :show],
64 map.permission :view_issues, {:issues => [:index, :show],
65 :auto_complete => [:issues],
65 :auto_complete => [:issues],
66 :context_menus => [:issues],
66 :context_menus => [:issues],
67 :versions => [:index, :show, :status_by],
67 :versions => [:index, :show, :status_by],
68 :journals => [:index, :diff],
68 :journals => [:index, :diff],
69 :queries => :index,
69 :queries => :index,
70 :reports => [:issue_report, :issue_report_details]},
70 :reports => [:issue_report, :issue_report_details]},
71 :read => true
71 :read => true
72 map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload}
72 map.permission :add_issues, {:issues => [:new, :create, :update_form], :attachments => :upload}
73 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload}
73 map.permission :edit_issues, {:issues => [:edit, :update, :bulk_edit, :bulk_update, :update_form], :journals => [:new], :attachments => :upload}
74 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
74 map.permission :manage_issue_relations, {:issue_relations => [:index, :show, :create, :destroy]}
75 map.permission :manage_subtasks, {}
75 map.permission :manage_subtasks, {}
76 map.permission :set_issues_private, {}
76 map.permission :set_issues_private, {}
77 map.permission :set_own_issues_private, {}, :require => :loggedin
77 map.permission :set_own_issues_private, {}, :require => :loggedin
78 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload}
78 map.permission :add_issue_notes, {:issues => [:edit, :update], :journals => [:new], :attachments => :upload}
79 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
79 map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
80 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
80 map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
81 map.permission :view_private_notes, {}, :read => true, :require => :member
81 map.permission :view_private_notes, {}, :read => true, :require => :member
82 map.permission :set_notes_private, {}, :require => :member
82 map.permission :set_notes_private, {}, :require => :member
83 map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin
83 map.permission :move_issues, {:issues => [:bulk_edit, :bulk_update]}, :require => :loggedin
84 map.permission :delete_issues, {:issues => :destroy}, :require => :member
84 map.permission :delete_issues, {:issues => :destroy}, :require => :member
85 # Queries
85 # Queries
86 map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
86 map.permission :manage_public_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :member
87 map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
87 map.permission :save_queries, {:queries => [:new, :create, :edit, :update, :destroy]}, :require => :loggedin
88 # Watchers
88 # Watchers
89 map.permission :view_issue_watchers, {}, :read => true
89 map.permission :view_issue_watchers, {}, :read => true
90 map.permission :add_issue_watchers, {:watchers => :new}
90 map.permission :add_issue_watchers, {:watchers => :new}
91 map.permission :delete_issue_watchers, {:watchers => :destroy}
91 map.permission :delete_issue_watchers, {:watchers => :destroy}
92 end
92 end
93
93
94 map.project_module :time_tracking do |map|
94 map.project_module :time_tracking do |map|
95 map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
95 map.permission :log_time, {:timelog => [:new, :create]}, :require => :loggedin
96 map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
96 map.permission :view_time_entries, {:timelog => [:index, :report, :show]}, :read => true
97 map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
97 map.permission :edit_time_entries, {:timelog => [:edit, :update, :destroy, :bulk_edit, :bulk_update]}, :require => :member
98 map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
98 map.permission :edit_own_time_entries, {:timelog => [:edit, :update, :destroy,:bulk_edit, :bulk_update]}, :require => :loggedin
99 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
99 map.permission :manage_project_activities, {:project_enumerations => [:update, :destroy]}, :require => :member
100 end
100 end
101
101
102 map.project_module :news do |map|
102 map.project_module :news do |map|
103 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
103 map.permission :manage_news, {:news => [:new, :create, :edit, :update, :destroy], :comments => [:destroy]}, :require => :member
104 map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true
104 map.permission :view_news, {:news => [:index, :show]}, :public => true, :read => true
105 map.permission :comment_news, {:comments => :create}
105 map.permission :comment_news, {:comments => :create}
106 end
106 end
107
107
108 map.project_module :documents do |map|
108 map.project_module :documents do |map|
109 map.permission :manage_documents, {:documents => [:new, :create, :edit, :update, :destroy, :add_attachment]}, :require => :loggedin
109 map.permission :manage_documents, {:documents => [:new, :create, :edit, :update, :destroy, :add_attachment]}, :require => :loggedin
110 map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true
110 map.permission :view_documents, {:documents => [:index, :show, :download]}, :read => true
111 end
111 end
112
112
113 map.project_module :files do |map|
113 map.project_module :files do |map|
114 map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
114 map.permission :manage_files, {:files => [:new, :create]}, :require => :loggedin
115 map.permission :view_files, {:files => :index, :versions => :download}, :read => true
115 map.permission :view_files, {:files => :index, :versions => :download}, :read => true
116 end
116 end
117
117
118 map.project_module :wiki do |map|
118 map.project_module :wiki do |map|
119 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
119 map.permission :manage_wiki, {:wikis => [:edit, :destroy]}, :require => :member
120 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
120 map.permission :rename_wiki_pages, {:wiki => :rename}, :require => :member
121 map.permission :delete_wiki_pages, {:wiki => :destroy}, :require => :member
121 map.permission :delete_wiki_pages, {:wiki => [:destroy, :destroy_version]}, :require => :member
122 map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true
122 map.permission :view_wiki_pages, {:wiki => [:index, :show, :special, :date_index]}, :read => true
123 map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true
123 map.permission :export_wiki_pages, {:wiki => [:export]}, :read => true
124 map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true
124 map.permission :view_wiki_edits, {:wiki => [:history, :diff, :annotate]}, :read => true
125 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
125 map.permission :edit_wiki_pages, :wiki => [:edit, :update, :preview, :add_attachment]
126 map.permission :delete_wiki_pages_attachments, {}
126 map.permission :delete_wiki_pages_attachments, {}
127 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
127 map.permission :protect_wiki_pages, {:wiki => :protect}, :require => :member
128 end
128 end
129
129
130 map.project_module :repository do |map|
130 map.project_module :repository do |map|
131 map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
131 map.permission :manage_repository, {:repositories => [:new, :create, :edit, :update, :committers, :destroy]}, :require => :member
132 map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true
132 map.permission :browse_repository, {:repositories => [:show, :browse, :entry, :raw, :annotate, :changes, :diff, :stats, :graph]}, :read => true
133 map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true
133 map.permission :view_changesets, {:repositories => [:show, :revisions, :revision]}, :read => true
134 map.permission :commit_access, {}
134 map.permission :commit_access, {}
135 map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
135 map.permission :manage_related_issues, {:repositories => [:add_related_issue, :remove_related_issue]}
136 end
136 end
137
137
138 map.project_module :boards do |map|
138 map.project_module :boards do |map|
139 map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
139 map.permission :manage_boards, {:boards => [:new, :create, :edit, :update, :destroy]}, :require => :member
140 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true
140 map.permission :view_messages, {:boards => [:index, :show], :messages => [:show]}, :public => true, :read => true
141 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
141 map.permission :add_messages, {:messages => [:new, :reply, :quote]}
142 map.permission :edit_messages, {:messages => :edit}, :require => :member
142 map.permission :edit_messages, {:messages => :edit}, :require => :member
143 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
143 map.permission :edit_own_messages, {:messages => :edit}, :require => :loggedin
144 map.permission :delete_messages, {:messages => :destroy}, :require => :member
144 map.permission :delete_messages, {:messages => :destroy}, :require => :member
145 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
145 map.permission :delete_own_messages, {:messages => :destroy}, :require => :loggedin
146 end
146 end
147
147
148 map.project_module :calendar do |map|
148 map.project_module :calendar do |map|
149 map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true
149 map.permission :view_calendar, {:calendars => [:show, :update]}, :read => true
150 end
150 end
151
151
152 map.project_module :gantt do |map|
152 map.project_module :gantt do |map|
153 map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true
153 map.permission :view_gantt, {:gantts => [:show, :update]}, :read => true
154 end
154 end
155 end
155 end
156
156
157 Redmine::MenuManager.map :top_menu do |menu|
157 Redmine::MenuManager.map :top_menu do |menu|
158 menu.push :home, :home_path
158 menu.push :home, :home_path
159 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
159 menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
160 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
160 menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
161 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
161 menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
162 menu.push :help, Redmine::Info.help_url, :last => true
162 menu.push :help, Redmine::Info.help_url, :last => true
163 end
163 end
164
164
165 Redmine::MenuManager.map :account_menu do |menu|
165 Redmine::MenuManager.map :account_menu do |menu|
166 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
166 menu.push :login, :signin_path, :if => Proc.new { !User.current.logged? }
167 menu.push :register, :register_path, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
167 menu.push :register, :register_path, :if => Proc.new { !User.current.logged? && Setting.self_registration? }
168 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
168 menu.push :my_account, { :controller => 'my', :action => 'account' }, :if => Proc.new { User.current.logged? }
169 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
169 menu.push :logout, :signout_path, :if => Proc.new { User.current.logged? }
170 end
170 end
171
171
172 Redmine::MenuManager.map :application_menu do |menu|
172 Redmine::MenuManager.map :application_menu do |menu|
173 # Empty
173 # Empty
174 end
174 end
175
175
176 Redmine::MenuManager.map :admin_menu do |menu|
176 Redmine::MenuManager.map :admin_menu do |menu|
177 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
177 menu.push :projects, {:controller => 'admin', :action => 'projects'}, :caption => :label_project_plural
178 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
178 menu.push :users, {:controller => 'users'}, :caption => :label_user_plural
179 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
179 menu.push :groups, {:controller => 'groups'}, :caption => :label_group_plural
180 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
180 menu.push :roles, {:controller => 'roles'}, :caption => :label_role_and_permissions
181 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
181 menu.push :trackers, {:controller => 'trackers'}, :caption => :label_tracker_plural
182 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
182 menu.push :issue_statuses, {:controller => 'issue_statuses'}, :caption => :label_issue_status_plural,
183 :html => {:class => 'issue_statuses'}
183 :html => {:class => 'issue_statuses'}
184 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
184 menu.push :workflows, {:controller => 'workflows', :action => 'edit'}, :caption => :label_workflow
185 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
185 menu.push :custom_fields, {:controller => 'custom_fields'}, :caption => :label_custom_field_plural,
186 :html => {:class => 'custom_fields'}
186 :html => {:class => 'custom_fields'}
187 menu.push :enumerations, {:controller => 'enumerations'}
187 menu.push :enumerations, {:controller => 'enumerations'}
188 menu.push :settings, {:controller => 'settings'}
188 menu.push :settings, {:controller => 'settings'}
189 menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'},
189 menu.push :ldap_authentication, {:controller => 'auth_sources', :action => 'index'},
190 :html => {:class => 'server_authentication'}
190 :html => {:class => 'server_authentication'}
191 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
191 menu.push :plugins, {:controller => 'admin', :action => 'plugins'}, :last => true
192 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
192 menu.push :info, {:controller => 'admin', :action => 'info'}, :caption => :label_information_plural, :last => true
193 end
193 end
194
194
195 Redmine::MenuManager.map :project_menu do |menu|
195 Redmine::MenuManager.map :project_menu do |menu|
196 menu.push :overview, { :controller => 'projects', :action => 'show' }
196 menu.push :overview, { :controller => 'projects', :action => 'show' }
197 menu.push :activity, { :controller => 'activities', :action => 'index' }
197 menu.push :activity, { :controller => 'activities', :action => 'index' }
198 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
198 menu.push :roadmap, { :controller => 'versions', :action => 'index' }, :param => :project_id,
199 :if => Proc.new { |p| p.shared_versions.any? }
199 :if => Proc.new { |p| p.shared_versions.any? }
200 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
200 menu.push :issues, { :controller => 'issues', :action => 'index' }, :param => :project_id, :caption => :label_issue_plural
201 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
201 menu.push :new_issue, { :controller => 'issues', :action => 'new' }, :param => :project_id, :caption => :label_issue_new,
202 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
202 :html => { :accesskey => Redmine::AccessKeys.key_for(:new_issue) }
203 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
203 menu.push :gantt, { :controller => 'gantts', :action => 'show' }, :param => :project_id, :caption => :label_gantt
204 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
204 menu.push :calendar, { :controller => 'calendars', :action => 'show' }, :param => :project_id, :caption => :label_calendar
205 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
205 menu.push :news, { :controller => 'news', :action => 'index' }, :param => :project_id, :caption => :label_news_plural
206 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
206 menu.push :documents, { :controller => 'documents', :action => 'index' }, :param => :project_id, :caption => :label_document_plural
207 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
207 menu.push :wiki, { :controller => 'wiki', :action => 'show', :id => nil }, :param => :project_id,
208 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
208 :if => Proc.new { |p| p.wiki && !p.wiki.new_record? }
209 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
209 menu.push :boards, { :controller => 'boards', :action => 'index', :id => nil }, :param => :project_id,
210 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
210 :if => Proc.new { |p| p.boards.any? }, :caption => :label_board_plural
211 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
211 menu.push :files, { :controller => 'files', :action => 'index' }, :caption => :label_file_plural, :param => :project_id
212 menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil },
212 menu.push :repository, { :controller => 'repositories', :action => 'show', :repository_id => nil, :path => nil, :rev => nil },
213 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
213 :if => Proc.new { |p| p.repository && !p.repository.new_record? }
214 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
214 menu.push :settings, { :controller => 'projects', :action => 'settings' }, :last => true
215 end
215 end
216
216
217 Redmine::Activity.map do |activity|
217 Redmine::Activity.map do |activity|
218 activity.register :issues, :class_name => %w(Issue Journal)
218 activity.register :issues, :class_name => %w(Issue Journal)
219 activity.register :changesets
219 activity.register :changesets
220 activity.register :news
220 activity.register :news
221 activity.register :documents, :class_name => %w(Document Attachment)
221 activity.register :documents, :class_name => %w(Document Attachment)
222 activity.register :files, :class_name => 'Attachment'
222 activity.register :files, :class_name => 'Attachment'
223 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
223 activity.register :wiki_edits, :class_name => 'WikiContent::Version', :default => false
224 activity.register :messages, :default => false
224 activity.register :messages, :default => false
225 activity.register :time_entries, :default => false
225 activity.register :time_entries, :default => false
226 end
226 end
227
227
228 Redmine::Search.map do |search|
228 Redmine::Search.map do |search|
229 search.register :issues
229 search.register :issues
230 search.register :news
230 search.register :news
231 search.register :documents
231 search.register :documents
232 search.register :changesets
232 search.register :changesets
233 search.register :wiki_pages
233 search.register :wiki_pages
234 search.register :messages
234 search.register :messages
235 search.register :projects
235 search.register :projects
236 end
236 end
237
237
238 Redmine::WikiFormatting.map do |format|
238 Redmine::WikiFormatting.map do |format|
239 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
239 format.register :textile, Redmine::WikiFormatting::Textile::Formatter, Redmine::WikiFormatting::Textile::Helper
240 end
240 end
241
241
242 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
242 ActionView::Template.register_template_handler :rsb, Redmine::Views::ApiTemplateHandler
@@ -1,885 +1,905
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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,
25 fixtures :projects, :users, :roles, :members, :member_roles,
26 :enabled_modules, :wikis, :wiki_pages, :wiki_contents,
26 :enabled_modules, :wikis, :wiki_pages, :wiki_contents,
27 :wiki_content_versions, :attachments
27 :wiki_content_versions, :attachments
28
28
29 def setup
29 def setup
30 @controller = WikiController.new
30 @controller = WikiController.new
31 @request = ActionController::TestRequest.new
31 @request = ActionController::TestRequest.new
32 @response = ActionController::TestResponse.new
32 @response = ActionController::TestResponse.new
33 User.current = nil
33 User.current = nil
34 end
34 end
35
35
36 def test_show_start_page
36 def test_show_start_page
37 get :show, :project_id => 'ecookbook'
37 get :show, :project_id => 'ecookbook'
38 assert_response :success
38 assert_response :success
39 assert_template 'show'
39 assert_template 'show'
40 assert_tag :tag => 'h1', :content => /CookBook documentation/
40 assert_tag :tag => 'h1', :content => /CookBook documentation/
41
41
42 # child_pages macro
42 # child_pages macro
43 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
43 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
44 :child => { :tag => 'li',
44 :child => { :tag => 'li',
45 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
45 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
46 :content => 'Page with an inline image' } }
46 :content => 'Page with an inline image' } }
47 end
47 end
48
48
49 def test_export_link
49 def test_export_link
50 Role.anonymous.add_permission! :export_wiki_pages
50 Role.anonymous.add_permission! :export_wiki_pages
51 get :show, :project_id => 'ecookbook'
51 get :show, :project_id => 'ecookbook'
52 assert_response :success
52 assert_response :success
53 assert_tag 'a', :attributes => {:href => '/projects/ecookbook/wiki/CookBook_documentation.txt'}
53 assert_tag 'a', :attributes => {:href => '/projects/ecookbook/wiki/CookBook_documentation.txt'}
54 end
54 end
55
55
56 def test_show_page_with_name
56 def test_show_page_with_name
57 get :show, :project_id => 1, :id => 'Another_page'
57 get :show, :project_id => 1, :id => 'Another_page'
58 assert_response :success
58 assert_response :success
59 assert_template 'show'
59 assert_template 'show'
60 assert_tag :tag => 'h1', :content => /Another page/
60 assert_tag :tag => 'h1', :content => /Another page/
61 # Included page with an inline image
61 # Included page with an inline image
62 assert_tag :tag => 'p', :content => /This is an inline image/
62 assert_tag :tag => 'p', :content => /This is an inline image/
63 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
63 assert_tag :tag => 'img', :attributes => { :src => '/attachments/download/3',
64 :alt => 'This is a logo' }
64 :alt => 'This is a logo' }
65 end
65 end
66
66
67 def test_show_old_version
67 def test_show_old_version
68 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
68 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '2'
69 assert_response :success
69 assert_response :success
70 assert_template 'show'
70 assert_template 'show'
71
71
72 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/1', :text => /Previous/
72 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/1', :text => /Previous/
73 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/diff', :text => /diff/
73 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/diff', :text => /diff/
74 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/3', :text => /Next/
74 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/3', :text => /Next/
75 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
75 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
76 end
76 end
77
77
78 def test_show_first_version
78 def test_show_first_version
79 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '1'
79 get :show, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => '1'
80 assert_response :success
80 assert_response :success
81 assert_template 'show'
81 assert_template 'show'
82
82
83 assert_select 'a', :text => /Previous/, :count => 0
83 assert_select 'a', :text => /Previous/, :count => 0
84 assert_select 'a', :text => /diff/, :count => 0
84 assert_select 'a', :text => /diff/, :count => 0
85 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => /Next/
85 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => /Next/
86 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
86 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation', :text => /Current version/
87 end
87 end
88
88
89 def test_show_redirected_page
89 def test_show_redirected_page
90 WikiRedirect.create!(:wiki_id => 1, :title => 'Old_title', :redirects_to => 'Another_page')
90 WikiRedirect.create!(:wiki_id => 1, :title => 'Old_title', :redirects_to => 'Another_page')
91
91
92 get :show, :project_id => 'ecookbook', :id => 'Old_title'
92 get :show, :project_id => 'ecookbook', :id => 'Old_title'
93 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
93 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
94 end
94 end
95
95
96 def test_show_with_sidebar
96 def test_show_with_sidebar
97 page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
97 page = Project.find(1).wiki.pages.new(:title => 'Sidebar')
98 page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
98 page.content = WikiContent.new(:text => 'Side bar content for test_show_with_sidebar')
99 page.save!
99 page.save!
100
100
101 get :show, :project_id => 1, :id => 'Another_page'
101 get :show, :project_id => 1, :id => 'Another_page'
102 assert_response :success
102 assert_response :success
103 assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
103 assert_tag :tag => 'div', :attributes => {:id => 'sidebar'},
104 :content => /Side bar content for test_show_with_sidebar/
104 :content => /Side bar content for test_show_with_sidebar/
105 end
105 end
106
106
107 def test_show_should_display_section_edit_links
107 def test_show_should_display_section_edit_links
108 @request.session[:user_id] = 2
108 @request.session[:user_id] = 2
109 get :show, :project_id => 1, :id => 'Page with sections'
109 get :show, :project_id => 1, :id => 'Page with sections'
110 assert_no_tag 'a', :attributes => {
110 assert_no_tag 'a', :attributes => {
111 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=1'
111 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=1'
112 }
112 }
113 assert_tag 'a', :attributes => {
113 assert_tag 'a', :attributes => {
114 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
114 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
115 }
115 }
116 assert_tag 'a', :attributes => {
116 assert_tag 'a', :attributes => {
117 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=3'
117 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=3'
118 }
118 }
119 end
119 end
120
120
121 def test_show_current_version_should_display_section_edit_links
121 def test_show_current_version_should_display_section_edit_links
122 @request.session[:user_id] = 2
122 @request.session[:user_id] = 2
123 get :show, :project_id => 1, :id => 'Page with sections', :version => 3
123 get :show, :project_id => 1, :id => 'Page with sections', :version => 3
124
124
125 assert_tag 'a', :attributes => {
125 assert_tag 'a', :attributes => {
126 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
126 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
127 }
127 }
128 end
128 end
129
129
130 def test_show_old_version_should_not_display_section_edit_links
130 def test_show_old_version_should_not_display_section_edit_links
131 @request.session[:user_id] = 2
131 @request.session[:user_id] = 2
132 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
132 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
133
133
134 assert_no_tag 'a', :attributes => {
134 assert_no_tag 'a', :attributes => {
135 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
135 :href => '/projects/ecookbook/wiki/Page_with_sections/edit?section=2'
136 }
136 }
137 end
137 end
138
138
139 def test_show_unexistent_page_without_edit_right
139 def test_show_unexistent_page_without_edit_right
140 get :show, :project_id => 1, :id => 'Unexistent page'
140 get :show, :project_id => 1, :id => 'Unexistent page'
141 assert_response 404
141 assert_response 404
142 end
142 end
143
143
144 def test_show_unexistent_page_with_edit_right
144 def test_show_unexistent_page_with_edit_right
145 @request.session[:user_id] = 2
145 @request.session[:user_id] = 2
146 get :show, :project_id => 1, :id => 'Unexistent page'
146 get :show, :project_id => 1, :id => 'Unexistent page'
147 assert_response :success
147 assert_response :success
148 assert_template 'edit'
148 assert_template 'edit'
149 end
149 end
150
150
151 def test_show_unexistent_page_with_parent_should_preselect_parent
151 def test_show_unexistent_page_with_parent_should_preselect_parent
152 @request.session[:user_id] = 2
152 @request.session[:user_id] = 2
153 get :show, :project_id => 1, :id => 'Unexistent page', :parent => 'Another_page'
153 get :show, :project_id => 1, :id => 'Unexistent page', :parent => 'Another_page'
154 assert_response :success
154 assert_response :success
155 assert_template 'edit'
155 assert_template 'edit'
156 assert_tag 'select', :attributes => {:name => 'wiki_page[parent_id]'},
156 assert_tag 'select', :attributes => {:name => 'wiki_page[parent_id]'},
157 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}}
157 :child => {:tag => 'option', :attributes => {:value => '2', :selected => 'selected'}}
158 end
158 end
159
159
160 def test_show_should_not_show_history_without_permission
160 def test_show_should_not_show_history_without_permission
161 Role.anonymous.remove_permission! :view_wiki_edits
161 Role.anonymous.remove_permission! :view_wiki_edits
162 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
162 get :show, :project_id => 1, :id => 'Page with sections', :version => 2
163
163
164 assert_response 302
164 assert_response 302
165 end
165 end
166
166
167 def test_create_page
167 def test_create_page
168 @request.session[:user_id] = 2
168 @request.session[:user_id] = 2
169 assert_difference 'WikiPage.count' do
169 assert_difference 'WikiPage.count' do
170 assert_difference 'WikiContent.count' do
170 assert_difference 'WikiContent.count' do
171 put :update, :project_id => 1,
171 put :update, :project_id => 1,
172 :id => 'New page',
172 :id => 'New page',
173 :content => {:comments => 'Created the page',
173 :content => {:comments => 'Created the page',
174 :text => "h1. New page\n\nThis is a new page",
174 :text => "h1. New page\n\nThis is a new page",
175 :version => 0}
175 :version => 0}
176 end
176 end
177 end
177 end
178 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
178 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'New_page'
179 page = Project.find(1).wiki.find_page('New page')
179 page = Project.find(1).wiki.find_page('New page')
180 assert !page.new_record?
180 assert !page.new_record?
181 assert_not_nil page.content
181 assert_not_nil page.content
182 assert_nil page.parent
182 assert_nil page.parent
183 assert_equal 'Created the page', page.content.comments
183 assert_equal 'Created the page', page.content.comments
184 end
184 end
185
185
186 def test_create_page_with_attachments
186 def test_create_page_with_attachments
187 @request.session[:user_id] = 2
187 @request.session[:user_id] = 2
188 assert_difference 'WikiPage.count' do
188 assert_difference 'WikiPage.count' do
189 assert_difference 'Attachment.count' do
189 assert_difference 'Attachment.count' do
190 put :update, :project_id => 1,
190 put :update, :project_id => 1,
191 :id => 'New page',
191 :id => 'New page',
192 :content => {:comments => 'Created the page',
192 :content => {:comments => 'Created the page',
193 :text => "h1. New page\n\nThis is a new page",
193 :text => "h1. New page\n\nThis is a new page",
194 :version => 0},
194 :version => 0},
195 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
195 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain')}}
196 end
196 end
197 end
197 end
198 page = Project.find(1).wiki.find_page('New page')
198 page = Project.find(1).wiki.find_page('New page')
199 assert_equal 1, page.attachments.count
199 assert_equal 1, page.attachments.count
200 assert_equal 'testfile.txt', page.attachments.first.filename
200 assert_equal 'testfile.txt', page.attachments.first.filename
201 end
201 end
202
202
203 def test_create_page_with_parent
203 def test_create_page_with_parent
204 @request.session[:user_id] = 2
204 @request.session[:user_id] = 2
205 assert_difference 'WikiPage.count' do
205 assert_difference 'WikiPage.count' do
206 put :update, :project_id => 1, :id => 'New page',
206 put :update, :project_id => 1, :id => 'New page',
207 :content => {:text => "h1. New page\n\nThis is a new page", :version => 0},
207 :content => {:text => "h1. New page\n\nThis is a new page", :version => 0},
208 :wiki_page => {:parent_id => 2}
208 :wiki_page => {:parent_id => 2}
209 end
209 end
210 page = Project.find(1).wiki.find_page('New page')
210 page = Project.find(1).wiki.find_page('New page')
211 assert_equal WikiPage.find(2), page.parent
211 assert_equal WikiPage.find(2), page.parent
212 end
212 end
213
213
214 def test_edit_page
214 def test_edit_page
215 @request.session[:user_id] = 2
215 @request.session[:user_id] = 2
216 get :edit, :project_id => 'ecookbook', :id => 'Another_page'
216 get :edit, :project_id => 'ecookbook', :id => 'Another_page'
217
217
218 assert_response :success
218 assert_response :success
219 assert_template 'edit'
219 assert_template 'edit'
220
220
221 assert_tag 'textarea',
221 assert_tag 'textarea',
222 :attributes => { :name => 'content[text]' },
222 :attributes => { :name => 'content[text]' },
223 :content => "\n"+WikiPage.find_by_title('Another_page').content.text
223 :content => "\n"+WikiPage.find_by_title('Another_page').content.text
224 end
224 end
225
225
226 def test_edit_section
226 def test_edit_section
227 @request.session[:user_id] = 2
227 @request.session[:user_id] = 2
228 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2
228 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 2
229
229
230 assert_response :success
230 assert_response :success
231 assert_template 'edit'
231 assert_template 'edit'
232
232
233 page = WikiPage.find_by_title('Page_with_sections')
233 page = WikiPage.find_by_title('Page_with_sections')
234 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
234 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
235
235
236 assert_tag 'textarea',
236 assert_tag 'textarea',
237 :attributes => { :name => 'content[text]' },
237 :attributes => { :name => 'content[text]' },
238 :content => "\n"+section
238 :content => "\n"+section
239 assert_tag 'input',
239 assert_tag 'input',
240 :attributes => { :name => 'section', :type => 'hidden', :value => '2' }
240 :attributes => { :name => 'section', :type => 'hidden', :value => '2' }
241 assert_tag 'input',
241 assert_tag 'input',
242 :attributes => { :name => 'section_hash', :type => 'hidden', :value => hash }
242 :attributes => { :name => 'section_hash', :type => 'hidden', :value => hash }
243 end
243 end
244
244
245 def test_edit_invalid_section_should_respond_with_404
245 def test_edit_invalid_section_should_respond_with_404
246 @request.session[:user_id] = 2
246 @request.session[:user_id] = 2
247 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10
247 get :edit, :project_id => 'ecookbook', :id => 'Page_with_sections', :section => 10
248
248
249 assert_response 404
249 assert_response 404
250 end
250 end
251
251
252 def test_update_page
252 def test_update_page
253 @request.session[:user_id] = 2
253 @request.session[:user_id] = 2
254 assert_no_difference 'WikiPage.count' do
254 assert_no_difference 'WikiPage.count' do
255 assert_no_difference 'WikiContent.count' do
255 assert_no_difference 'WikiContent.count' do
256 assert_difference 'WikiContent::Version.count' do
256 assert_difference 'WikiContent::Version.count' do
257 put :update, :project_id => 1,
257 put :update, :project_id => 1,
258 :id => 'Another_page',
258 :id => 'Another_page',
259 :content => {
259 :content => {
260 :comments => "my comments",
260 :comments => "my comments",
261 :text => "edited",
261 :text => "edited",
262 :version => 1
262 :version => 1
263 }
263 }
264 end
264 end
265 end
265 end
266 end
266 end
267 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
267 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
268
268
269 page = Wiki.find(1).pages.find_by_title('Another_page')
269 page = Wiki.find(1).pages.find_by_title('Another_page')
270 assert_equal "edited", page.content.text
270 assert_equal "edited", page.content.text
271 assert_equal 2, page.content.version
271 assert_equal 2, page.content.version
272 assert_equal "my comments", page.content.comments
272 assert_equal "my comments", page.content.comments
273 end
273 end
274
274
275 def test_update_page_with_parent
275 def test_update_page_with_parent
276 @request.session[:user_id] = 2
276 @request.session[:user_id] = 2
277 assert_no_difference 'WikiPage.count' do
277 assert_no_difference 'WikiPage.count' do
278 assert_no_difference 'WikiContent.count' do
278 assert_no_difference 'WikiContent.count' do
279 assert_difference 'WikiContent::Version.count' do
279 assert_difference 'WikiContent::Version.count' do
280 put :update, :project_id => 1,
280 put :update, :project_id => 1,
281 :id => 'Another_page',
281 :id => 'Another_page',
282 :content => {
282 :content => {
283 :comments => "my comments",
283 :comments => "my comments",
284 :text => "edited",
284 :text => "edited",
285 :version => 1
285 :version => 1
286 },
286 },
287 :wiki_page => {:parent_id => '1'}
287 :wiki_page => {:parent_id => '1'}
288 end
288 end
289 end
289 end
290 end
290 end
291 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
291 assert_redirected_to '/projects/ecookbook/wiki/Another_page'
292
292
293 page = Wiki.find(1).pages.find_by_title('Another_page')
293 page = Wiki.find(1).pages.find_by_title('Another_page')
294 assert_equal "edited", page.content.text
294 assert_equal "edited", page.content.text
295 assert_equal 2, page.content.version
295 assert_equal 2, page.content.version
296 assert_equal "my comments", page.content.comments
296 assert_equal "my comments", page.content.comments
297 assert_equal WikiPage.find(1), page.parent
297 assert_equal WikiPage.find(1), page.parent
298 end
298 end
299
299
300 def test_update_page_with_failure
300 def test_update_page_with_failure
301 @request.session[:user_id] = 2
301 @request.session[:user_id] = 2
302 assert_no_difference 'WikiPage.count' do
302 assert_no_difference 'WikiPage.count' do
303 assert_no_difference 'WikiContent.count' do
303 assert_no_difference 'WikiContent.count' do
304 assert_no_difference 'WikiContent::Version.count' do
304 assert_no_difference 'WikiContent::Version.count' do
305 put :update, :project_id => 1,
305 put :update, :project_id => 1,
306 :id => 'Another_page',
306 :id => 'Another_page',
307 :content => {
307 :content => {
308 :comments => 'a' * 300, # failure here, comment is too long
308 :comments => 'a' * 300, # failure here, comment is too long
309 :text => 'edited',
309 :text => 'edited',
310 :version => 1
310 :version => 1
311 }
311 }
312 end
312 end
313 end
313 end
314 end
314 end
315 assert_response :success
315 assert_response :success
316 assert_template 'edit'
316 assert_template 'edit'
317
317
318 assert_error_tag :descendant => {:content => /Comment is too long/}
318 assert_error_tag :descendant => {:content => /Comment is too long/}
319 assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => "\nedited"
319 assert_tag :tag => 'textarea', :attributes => {:id => 'content_text'}, :content => "\nedited"
320 assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'}
320 assert_tag :tag => 'input', :attributes => {:id => 'content_version', :value => '1'}
321 end
321 end
322
322
323 def test_update_page_with_parent_change_only_should_not_create_content_version
323 def test_update_page_with_parent_change_only_should_not_create_content_version
324 @request.session[:user_id] = 2
324 @request.session[:user_id] = 2
325 assert_no_difference 'WikiPage.count' do
325 assert_no_difference 'WikiPage.count' do
326 assert_no_difference 'WikiContent.count' do
326 assert_no_difference 'WikiContent.count' do
327 assert_no_difference 'WikiContent::Version.count' do
327 assert_no_difference 'WikiContent::Version.count' do
328 put :update, :project_id => 1,
328 put :update, :project_id => 1,
329 :id => 'Another_page',
329 :id => 'Another_page',
330 :content => {
330 :content => {
331 :comments => '',
331 :comments => '',
332 :text => Wiki.find(1).find_page('Another_page').content.text,
332 :text => Wiki.find(1).find_page('Another_page').content.text,
333 :version => 1
333 :version => 1
334 },
334 },
335 :wiki_page => {:parent_id => '1'}
335 :wiki_page => {:parent_id => '1'}
336 end
336 end
337 end
337 end
338 end
338 end
339 page = Wiki.find(1).pages.find_by_title('Another_page')
339 page = Wiki.find(1).pages.find_by_title('Another_page')
340 assert_equal 1, page.content.version
340 assert_equal 1, page.content.version
341 assert_equal WikiPage.find(1), page.parent
341 assert_equal WikiPage.find(1), page.parent
342 end
342 end
343
343
344 def test_update_page_with_attachments_only_should_not_create_content_version
344 def test_update_page_with_attachments_only_should_not_create_content_version
345 @request.session[:user_id] = 2
345 @request.session[:user_id] = 2
346 assert_no_difference 'WikiPage.count' do
346 assert_no_difference 'WikiPage.count' do
347 assert_no_difference 'WikiContent.count' do
347 assert_no_difference 'WikiContent.count' do
348 assert_no_difference 'WikiContent::Version.count' do
348 assert_no_difference 'WikiContent::Version.count' do
349 assert_difference 'Attachment.count' do
349 assert_difference 'Attachment.count' do
350 put :update, :project_id => 1,
350 put :update, :project_id => 1,
351 :id => 'Another_page',
351 :id => 'Another_page',
352 :content => {
352 :content => {
353 :comments => '',
353 :comments => '',
354 :text => Wiki.find(1).find_page('Another_page').content.text,
354 :text => Wiki.find(1).find_page('Another_page').content.text,
355 :version => 1
355 :version => 1
356 },
356 },
357 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
357 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
358 end
358 end
359 end
359 end
360 end
360 end
361 end
361 end
362 page = Wiki.find(1).pages.find_by_title('Another_page')
362 page = Wiki.find(1).pages.find_by_title('Another_page')
363 assert_equal 1, page.content.version
363 assert_equal 1, page.content.version
364 end
364 end
365
365
366 def test_update_stale_page_should_not_raise_an_error
366 def test_update_stale_page_should_not_raise_an_error
367 @request.session[:user_id] = 2
367 @request.session[:user_id] = 2
368 c = Wiki.find(1).find_page('Another_page').content
368 c = Wiki.find(1).find_page('Another_page').content
369 c.text = 'Previous text'
369 c.text = 'Previous text'
370 c.save!
370 c.save!
371 assert_equal 2, c.version
371 assert_equal 2, c.version
372
372
373 assert_no_difference 'WikiPage.count' do
373 assert_no_difference 'WikiPage.count' do
374 assert_no_difference 'WikiContent.count' do
374 assert_no_difference 'WikiContent.count' do
375 assert_no_difference 'WikiContent::Version.count' do
375 assert_no_difference 'WikiContent::Version.count' do
376 put :update, :project_id => 1,
376 put :update, :project_id => 1,
377 :id => 'Another_page',
377 :id => 'Another_page',
378 :content => {
378 :content => {
379 :comments => 'My comments',
379 :comments => 'My comments',
380 :text => 'Text should not be lost',
380 :text => 'Text should not be lost',
381 :version => 1
381 :version => 1
382 }
382 }
383 end
383 end
384 end
384 end
385 end
385 end
386 assert_response :success
386 assert_response :success
387 assert_template 'edit'
387 assert_template 'edit'
388 assert_tag :div,
388 assert_tag :div,
389 :attributes => { :class => /error/ },
389 :attributes => { :class => /error/ },
390 :content => /Data has been updated by another user/
390 :content => /Data has been updated by another user/
391 assert_tag 'textarea',
391 assert_tag 'textarea',
392 :attributes => { :name => 'content[text]' },
392 :attributes => { :name => 'content[text]' },
393 :content => /Text should not be lost/
393 :content => /Text should not be lost/
394 assert_tag 'input',
394 assert_tag 'input',
395 :attributes => { :name => 'content[comments]', :value => 'My comments' }
395 :attributes => { :name => 'content[comments]', :value => 'My comments' }
396
396
397 c.reload
397 c.reload
398 assert_equal 'Previous text', c.text
398 assert_equal 'Previous text', c.text
399 assert_equal 2, c.version
399 assert_equal 2, c.version
400 end
400 end
401
401
402 def test_update_section
402 def test_update_section
403 @request.session[:user_id] = 2
403 @request.session[:user_id] = 2
404 page = WikiPage.find_by_title('Page_with_sections')
404 page = WikiPage.find_by_title('Page_with_sections')
405 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
405 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
406 text = page.content.text
406 text = page.content.text
407
407
408 assert_no_difference 'WikiPage.count' do
408 assert_no_difference 'WikiPage.count' do
409 assert_no_difference 'WikiContent.count' do
409 assert_no_difference 'WikiContent.count' do
410 assert_difference 'WikiContent::Version.count' do
410 assert_difference 'WikiContent::Version.count' do
411 put :update, :project_id => 1, :id => 'Page_with_sections',
411 put :update, :project_id => 1, :id => 'Page_with_sections',
412 :content => {
412 :content => {
413 :text => "New section content",
413 :text => "New section content",
414 :version => 3
414 :version => 3
415 },
415 },
416 :section => 2,
416 :section => 2,
417 :section_hash => hash
417 :section_hash => hash
418 end
418 end
419 end
419 end
420 end
420 end
421 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
421 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
422 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text
422 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.reload.content.text
423 end
423 end
424
424
425 def test_update_section_should_allow_stale_page_update
425 def test_update_section_should_allow_stale_page_update
426 @request.session[:user_id] = 2
426 @request.session[:user_id] = 2
427 page = WikiPage.find_by_title('Page_with_sections')
427 page = WikiPage.find_by_title('Page_with_sections')
428 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
428 section, hash = Redmine::WikiFormatting::Textile::Formatter.new(page.content.text).get_section(2)
429 text = page.content.text
429 text = page.content.text
430
430
431 assert_no_difference 'WikiPage.count' do
431 assert_no_difference 'WikiPage.count' do
432 assert_no_difference 'WikiContent.count' do
432 assert_no_difference 'WikiContent.count' do
433 assert_difference 'WikiContent::Version.count' do
433 assert_difference 'WikiContent::Version.count' do
434 put :update, :project_id => 1, :id => 'Page_with_sections',
434 put :update, :project_id => 1, :id => 'Page_with_sections',
435 :content => {
435 :content => {
436 :text => "New section content",
436 :text => "New section content",
437 :version => 2 # Current version is 3
437 :version => 2 # Current version is 3
438 },
438 },
439 :section => 2,
439 :section => 2,
440 :section_hash => hash
440 :section_hash => hash
441 end
441 end
442 end
442 end
443 end
443 end
444 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
444 assert_redirected_to '/projects/ecookbook/wiki/Page_with_sections'
445 page.reload
445 page.reload
446 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text
446 assert_equal Redmine::WikiFormatting::Textile::Formatter.new(text).update_section(2, "New section content"), page.content.text
447 assert_equal 4, page.content.version
447 assert_equal 4, page.content.version
448 end
448 end
449
449
450 def test_update_section_should_not_allow_stale_section_update
450 def test_update_section_should_not_allow_stale_section_update
451 @request.session[:user_id] = 2
451 @request.session[:user_id] = 2
452
452
453 assert_no_difference 'WikiPage.count' do
453 assert_no_difference 'WikiPage.count' do
454 assert_no_difference 'WikiContent.count' do
454 assert_no_difference 'WikiContent.count' do
455 assert_no_difference 'WikiContent::Version.count' do
455 assert_no_difference 'WikiContent::Version.count' do
456 put :update, :project_id => 1, :id => 'Page_with_sections',
456 put :update, :project_id => 1, :id => 'Page_with_sections',
457 :content => {
457 :content => {
458 :comments => 'My comments',
458 :comments => 'My comments',
459 :text => "Text should not be lost",
459 :text => "Text should not be lost",
460 :version => 3
460 :version => 3
461 },
461 },
462 :section => 2,
462 :section => 2,
463 :section_hash => Digest::MD5.hexdigest("wrong hash")
463 :section_hash => Digest::MD5.hexdigest("wrong hash")
464 end
464 end
465 end
465 end
466 end
466 end
467 assert_response :success
467 assert_response :success
468 assert_template 'edit'
468 assert_template 'edit'
469 assert_tag :div,
469 assert_tag :div,
470 :attributes => { :class => /error/ },
470 :attributes => { :class => /error/ },
471 :content => /Data has been updated by another user/
471 :content => /Data has been updated by another user/
472 assert_tag 'textarea',
472 assert_tag 'textarea',
473 :attributes => { :name => 'content[text]' },
473 :attributes => { :name => 'content[text]' },
474 :content => /Text should not be lost/
474 :content => /Text should not be lost/
475 assert_tag 'input',
475 assert_tag 'input',
476 :attributes => { :name => 'content[comments]', :value => 'My comments' }
476 :attributes => { :name => 'content[comments]', :value => 'My comments' }
477 end
477 end
478
478
479 def test_preview
479 def test_preview
480 @request.session[:user_id] = 2
480 @request.session[:user_id] = 2
481 xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
481 xhr :post, :preview, :project_id => 1, :id => 'CookBook_documentation',
482 :content => { :comments => '',
482 :content => { :comments => '',
483 :text => 'this is a *previewed text*',
483 :text => 'this is a *previewed text*',
484 :version => 3 }
484 :version => 3 }
485 assert_response :success
485 assert_response :success
486 assert_template 'common/_preview'
486 assert_template 'common/_preview'
487 assert_tag :tag => 'strong', :content => /previewed text/
487 assert_tag :tag => 'strong', :content => /previewed text/
488 end
488 end
489
489
490 def test_preview_new_page
490 def test_preview_new_page
491 @request.session[:user_id] = 2
491 @request.session[:user_id] = 2
492 xhr :post, :preview, :project_id => 1, :id => 'New page',
492 xhr :post, :preview, :project_id => 1, :id => 'New page',
493 :content => { :text => 'h1. New page',
493 :content => { :text => 'h1. New page',
494 :comments => '',
494 :comments => '',
495 :version => 0 }
495 :version => 0 }
496 assert_response :success
496 assert_response :success
497 assert_template 'common/_preview'
497 assert_template 'common/_preview'
498 assert_tag :tag => 'h1', :content => /New page/
498 assert_tag :tag => 'h1', :content => /New page/
499 end
499 end
500
500
501 def test_history
501 def test_history
502 @request.session[:user_id] = 2
502 get :history, :project_id => 'ecookbook', :id => 'CookBook_documentation'
503 get :history, :project_id => 'ecookbook', :id => 'CookBook_documentation'
503 assert_response :success
504 assert_response :success
504 assert_template 'history'
505 assert_template 'history'
505 assert_not_nil assigns(:versions)
506 assert_not_nil assigns(:versions)
506 assert_equal 3, assigns(:versions).size
507 assert_equal 3, assigns(:versions).size
507
508
508 assert_select "input[type=submit][name=commit]"
509 assert_select "input[type=submit][name=commit]"
509 assert_select 'td' do
510 assert_select 'td' do
510 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => '2'
511 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => '2'
511 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/annotate'
512 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2/annotate', :text => 'Annotate'
513 assert_select 'a[href=?]', '/projects/ecookbook/wiki/CookBook_documentation/2', :text => 'Delete'
512 end
514 end
513 end
515 end
514
516
515 def test_history_with_one_version
517 def test_history_with_one_version
516 get :history, :project_id => 1, :id => 'Another_page'
518 @request.session[:user_id] = 2
519 get :history, :project_id => 'ecookbook', :id => 'Another_page'
517 assert_response :success
520 assert_response :success
518 assert_template 'history'
521 assert_template 'history'
519 assert_not_nil assigns(:versions)
522 assert_not_nil assigns(:versions)
520 assert_equal 1, assigns(:versions).size
523 assert_equal 1, assigns(:versions).size
521 assert_select "input[type=submit][name=commit]", false
524 assert_select "input[type=submit][name=commit]", false
525 assert_select 'td' do
526 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => '1'
527 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1/annotate', :text => 'Annotate'
528 assert_select 'a[href=?]', '/projects/ecookbook/wiki/Another_page/1', :text => 'Delete', :count => 0
529 end
522 end
530 end
523
531
524 def test_diff
532 def test_diff
525 content = WikiPage.find(1).content
533 content = WikiPage.find(1).content
526 assert_difference 'WikiContent::Version.count', 2 do
534 assert_difference 'WikiContent::Version.count', 2 do
527 content.text = "Line removed\nThis is a sample text for testing diffs"
535 content.text = "Line removed\nThis is a sample text for testing diffs"
528 content.save!
536 content.save!
529 content.text = "This is a sample text for testing diffs\nLine added"
537 content.text = "This is a sample text for testing diffs\nLine added"
530 content.save!
538 content.save!
531 end
539 end
532
540
533 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => content.version, :version_from => (content.version - 1)
541 get :diff, :project_id => 1, :id => 'CookBook_documentation', :version => content.version, :version_from => (content.version - 1)
534 assert_response :success
542 assert_response :success
535 assert_template 'diff'
543 assert_template 'diff'
536 assert_select 'span.diff_out', :text => 'Line removed'
544 assert_select 'span.diff_out', :text => 'Line removed'
537 assert_select 'span.diff_in', :text => 'Line added'
545 assert_select 'span.diff_in', :text => 'Line added'
538 end
546 end
539
547
540 def test_annotate
548 def test_annotate
541 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2
549 get :annotate, :project_id => 1, :id => 'CookBook_documentation', :version => 2
542 assert_response :success
550 assert_response :success
543 assert_template 'annotate'
551 assert_template 'annotate'
544
552
545 # Line 1
553 # Line 1
546 assert_tag :tag => 'tr', :child => {
554 assert_tag :tag => 'tr', :child => {
547 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1', :sibling => {
555 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '1', :sibling => {
548 :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/, :sibling => {
556 :tag => 'td', :attributes => {:class => 'author'}, :content => /John Smith/, :sibling => {
549 :tag => 'td', :content => /h1\. CookBook documentation/
557 :tag => 'td', :content => /h1\. CookBook documentation/
550 }
558 }
551 }
559 }
552 }
560 }
553
561
554 # Line 5
562 # Line 5
555 assert_tag :tag => 'tr', :child => {
563 assert_tag :tag => 'tr', :child => {
556 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => {
564 :tag => 'th', :attributes => {:class => 'line-num'}, :content => '5', :sibling => {
557 :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/, :sibling => {
565 :tag => 'td', :attributes => {:class => 'author'}, :content => /redMine Admin/, :sibling => {
558 :tag => 'td', :content => /Some updated \[\[documentation\]\] here/
566 :tag => 'td', :content => /Some updated \[\[documentation\]\] here/
559 }
567 }
560 }
568 }
561 }
569 }
562 end
570 end
563
571
564 def test_get_rename
572 def test_get_rename
565 @request.session[:user_id] = 2
573 @request.session[:user_id] = 2
566 get :rename, :project_id => 1, :id => 'Another_page'
574 get :rename, :project_id => 1, :id => 'Another_page'
567 assert_response :success
575 assert_response :success
568 assert_template 'rename'
576 assert_template 'rename'
569 assert_tag 'option',
577 assert_tag 'option',
570 :attributes => {:value => ''},
578 :attributes => {:value => ''},
571 :content => '',
579 :content => '',
572 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
580 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
573 assert_no_tag 'option',
581 assert_no_tag 'option',
574 :attributes => {:selected => 'selected'},
582 :attributes => {:selected => 'selected'},
575 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
583 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
576 end
584 end
577
585
578 def test_get_rename_child_page
586 def test_get_rename_child_page
579 @request.session[:user_id] = 2
587 @request.session[:user_id] = 2
580 get :rename, :project_id => 1, :id => 'Child_1'
588 get :rename, :project_id => 1, :id => 'Child_1'
581 assert_response :success
589 assert_response :success
582 assert_template 'rename'
590 assert_template 'rename'
583 assert_tag 'option',
591 assert_tag 'option',
584 :attributes => {:value => ''},
592 :attributes => {:value => ''},
585 :content => '',
593 :content => '',
586 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
594 :parent => {:tag => 'select', :attributes => {:name => 'wiki_page[parent_id]'}}
587 assert_tag 'option',
595 assert_tag 'option',
588 :attributes => {:value => '2', :selected => 'selected'},
596 :attributes => {:value => '2', :selected => 'selected'},
589 :content => /Another page/,
597 :content => /Another page/,
590 :parent => {
598 :parent => {
591 :tag => 'select',
599 :tag => 'select',
592 :attributes => {:name => 'wiki_page[parent_id]'}
600 :attributes => {:name => 'wiki_page[parent_id]'}
593 }
601 }
594 end
602 end
595
603
596 def test_rename_with_redirect
604 def test_rename_with_redirect
597 @request.session[:user_id] = 2
605 @request.session[:user_id] = 2
598 post :rename, :project_id => 1, :id => 'Another_page',
606 post :rename, :project_id => 1, :id => 'Another_page',
599 :wiki_page => { :title => 'Another renamed page',
607 :wiki_page => { :title => 'Another renamed page',
600 :redirect_existing_links => 1 }
608 :redirect_existing_links => 1 }
601 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
609 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
602 wiki = Project.find(1).wiki
610 wiki = Project.find(1).wiki
603 # Check redirects
611 # Check redirects
604 assert_not_nil wiki.find_page('Another page')
612 assert_not_nil wiki.find_page('Another page')
605 assert_nil wiki.find_page('Another page', :with_redirect => false)
613 assert_nil wiki.find_page('Another page', :with_redirect => false)
606 end
614 end
607
615
608 def test_rename_without_redirect
616 def test_rename_without_redirect
609 @request.session[:user_id] = 2
617 @request.session[:user_id] = 2
610 post :rename, :project_id => 1, :id => 'Another_page',
618 post :rename, :project_id => 1, :id => 'Another_page',
611 :wiki_page => { :title => 'Another renamed page',
619 :wiki_page => { :title => 'Another renamed page',
612 :redirect_existing_links => "0" }
620 :redirect_existing_links => "0" }
613 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
621 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_renamed_page'
614 wiki = Project.find(1).wiki
622 wiki = Project.find(1).wiki
615 # Check that there's no redirects
623 # Check that there's no redirects
616 assert_nil wiki.find_page('Another page')
624 assert_nil wiki.find_page('Another page')
617 end
625 end
618
626
619 def test_rename_with_parent_assignment
627 def test_rename_with_parent_assignment
620 @request.session[:user_id] = 2
628 @request.session[:user_id] = 2
621 post :rename, :project_id => 1, :id => 'Another_page',
629 post :rename, :project_id => 1, :id => 'Another_page',
622 :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' }
630 :wiki_page => { :title => 'Another page', :redirect_existing_links => "0", :parent_id => '4' }
623 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
631 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
624 assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent
632 assert_equal WikiPage.find(4), WikiPage.find_by_title('Another_page').parent
625 end
633 end
626
634
627 def test_rename_with_parent_unassignment
635 def test_rename_with_parent_unassignment
628 @request.session[:user_id] = 2
636 @request.session[:user_id] = 2
629 post :rename, :project_id => 1, :id => 'Child_1',
637 post :rename, :project_id => 1, :id => 'Child_1',
630 :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' }
638 :wiki_page => { :title => 'Child 1', :redirect_existing_links => "0", :parent_id => '' }
631 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1'
639 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Child_1'
632 assert_nil WikiPage.find_by_title('Child_1').parent
640 assert_nil WikiPage.find_by_title('Child_1').parent
633 end
641 end
634
642
635 def test_destroy_a_page_without_children_should_not_ask_confirmation
643 def test_destroy_a_page_without_children_should_not_ask_confirmation
636 @request.session[:user_id] = 2
644 @request.session[:user_id] = 2
637 delete :destroy, :project_id => 1, :id => 'Child_2'
645 delete :destroy, :project_id => 1, :id => 'Child_2'
638 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
646 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
639 end
647 end
640
648
641 def test_destroy_parent_should_ask_confirmation
649 def test_destroy_parent_should_ask_confirmation
642 @request.session[:user_id] = 2
650 @request.session[:user_id] = 2
643 assert_no_difference('WikiPage.count') do
651 assert_no_difference('WikiPage.count') do
644 delete :destroy, :project_id => 1, :id => 'Another_page'
652 delete :destroy, :project_id => 1, :id => 'Another_page'
645 end
653 end
646 assert_response :success
654 assert_response :success
647 assert_template 'destroy'
655 assert_template 'destroy'
648 assert_select 'form' do
656 assert_select 'form' do
649 assert_select 'input[name=todo][value=nullify]'
657 assert_select 'input[name=todo][value=nullify]'
650 assert_select 'input[name=todo][value=destroy]'
658 assert_select 'input[name=todo][value=destroy]'
651 assert_select 'input[name=todo][value=reassign]'
659 assert_select 'input[name=todo][value=reassign]'
652 end
660 end
653 end
661 end
654
662
655 def test_destroy_parent_with_nullify_should_delete_parent_only
663 def test_destroy_parent_with_nullify_should_delete_parent_only
656 @request.session[:user_id] = 2
664 @request.session[:user_id] = 2
657 assert_difference('WikiPage.count', -1) do
665 assert_difference('WikiPage.count', -1) do
658 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify'
666 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'nullify'
659 end
667 end
660 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
668 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
661 assert_nil WikiPage.find_by_id(2)
669 assert_nil WikiPage.find_by_id(2)
662 end
670 end
663
671
664 def test_destroy_parent_with_cascade_should_delete_descendants
672 def test_destroy_parent_with_cascade_should_delete_descendants
665 @request.session[:user_id] = 2
673 @request.session[:user_id] = 2
666 assert_difference('WikiPage.count', -4) do
674 assert_difference('WikiPage.count', -4) do
667 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy'
675 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'destroy'
668 end
676 end
669 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
677 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
670 assert_nil WikiPage.find_by_id(2)
678 assert_nil WikiPage.find_by_id(2)
671 assert_nil WikiPage.find_by_id(5)
679 assert_nil WikiPage.find_by_id(5)
672 end
680 end
673
681
674 def test_destroy_parent_with_reassign
682 def test_destroy_parent_with_reassign
675 @request.session[:user_id] = 2
683 @request.session[:user_id] = 2
676 assert_difference('WikiPage.count', -1) do
684 assert_difference('WikiPage.count', -1) do
677 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
685 delete :destroy, :project_id => 1, :id => 'Another_page', :todo => 'reassign', :reassign_to_id => 1
678 end
686 end
679 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
687 assert_redirected_to :action => 'index', :project_id => 'ecookbook'
680 assert_nil WikiPage.find_by_id(2)
688 assert_nil WikiPage.find_by_id(2)
681 assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
689 assert_equal WikiPage.find(1), WikiPage.find_by_id(5).parent
682 end
690 end
683
691
692 def test_destroy_version
693 @request.session[:user_id] = 2
694 assert_difference 'WikiContent::Version.count', -1 do
695 assert_no_difference 'WikiContent.count' do
696 assert_no_difference 'WikiPage.count' do
697 delete :destroy_version, :project_id => 'ecookbook', :id => 'CookBook_documentation', :version => 2
698 assert_redirected_to '/projects/ecookbook/wiki/CookBook_documentation/history'
699 end
700 end
701 end
702 end
703
684 def test_index
704 def test_index
685 get :index, :project_id => 'ecookbook'
705 get :index, :project_id => 'ecookbook'
686 assert_response :success
706 assert_response :success
687 assert_template 'index'
707 assert_template 'index'
688 pages = assigns(:pages)
708 pages = assigns(:pages)
689 assert_not_nil pages
709 assert_not_nil pages
690 assert_equal Project.find(1).wiki.pages.size, pages.size
710 assert_equal Project.find(1).wiki.pages.size, pages.size
691 assert_equal pages.first.content.updated_on, pages.first.updated_on
711 assert_equal pages.first.content.updated_on, pages.first.updated_on
692
712
693 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
713 assert_tag :ul, :attributes => { :class => 'pages-hierarchy' },
694 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
714 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/CookBook_documentation' },
695 :content => 'CookBook documentation' },
715 :content => 'CookBook documentation' },
696 :child => { :tag => 'ul',
716 :child => { :tag => 'ul',
697 :child => { :tag => 'li',
717 :child => { :tag => 'li',
698 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
718 :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Page_with_an_inline_image' },
699 :content => 'Page with an inline image' } } } },
719 :content => 'Page with an inline image' } } } },
700 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
720 :child => { :tag => 'li', :child => { :tag => 'a', :attributes => { :href => '/projects/ecookbook/wiki/Another_page' },
701 :content => 'Another page' } }
721 :content => 'Another page' } }
702 end
722 end
703
723
704 def test_index_should_include_atom_link
724 def test_index_should_include_atom_link
705 get :index, :project_id => 'ecookbook'
725 get :index, :project_id => 'ecookbook'
706 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
726 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
707 end
727 end
708
728
709 def test_export_to_html
729 def test_export_to_html
710 @request.session[:user_id] = 2
730 @request.session[:user_id] = 2
711 get :export, :project_id => 'ecookbook'
731 get :export, :project_id => 'ecookbook'
712
732
713 assert_response :success
733 assert_response :success
714 assert_not_nil assigns(:pages)
734 assert_not_nil assigns(:pages)
715 assert assigns(:pages).any?
735 assert assigns(:pages).any?
716 assert_equal "text/html", @response.content_type
736 assert_equal "text/html", @response.content_type
717
737
718 assert_select "a[name=?]", "CookBook_documentation"
738 assert_select "a[name=?]", "CookBook_documentation"
719 assert_select "a[name=?]", "Another_page"
739 assert_select "a[name=?]", "Another_page"
720 assert_select "a[name=?]", "Page_with_an_inline_image"
740 assert_select "a[name=?]", "Page_with_an_inline_image"
721 end
741 end
722
742
723 def test_export_to_pdf
743 def test_export_to_pdf
724 @request.session[:user_id] = 2
744 @request.session[:user_id] = 2
725 get :export, :project_id => 'ecookbook', :format => 'pdf'
745 get :export, :project_id => 'ecookbook', :format => 'pdf'
726
746
727 assert_response :success
747 assert_response :success
728 assert_not_nil assigns(:pages)
748 assert_not_nil assigns(:pages)
729 assert assigns(:pages).any?
749 assert assigns(:pages).any?
730 assert_equal 'application/pdf', @response.content_type
750 assert_equal 'application/pdf', @response.content_type
731 assert_equal 'attachment; filename="ecookbook.pdf"', @response.headers['Content-Disposition']
751 assert_equal 'attachment; filename="ecookbook.pdf"', @response.headers['Content-Disposition']
732 assert @response.body.starts_with?('%PDF')
752 assert @response.body.starts_with?('%PDF')
733 end
753 end
734
754
735 def test_export_without_permission_should_be_denied
755 def test_export_without_permission_should_be_denied
736 @request.session[:user_id] = 2
756 @request.session[:user_id] = 2
737 Role.find_by_name('Manager').remove_permission! :export_wiki_pages
757 Role.find_by_name('Manager').remove_permission! :export_wiki_pages
738 get :export, :project_id => 'ecookbook'
758 get :export, :project_id => 'ecookbook'
739
759
740 assert_response 403
760 assert_response 403
741 end
761 end
742
762
743 def test_date_index
763 def test_date_index
744 get :date_index, :project_id => 'ecookbook'
764 get :date_index, :project_id => 'ecookbook'
745
765
746 assert_response :success
766 assert_response :success
747 assert_template 'date_index'
767 assert_template 'date_index'
748 assert_not_nil assigns(:pages)
768 assert_not_nil assigns(:pages)
749 assert_not_nil assigns(:pages_by_date)
769 assert_not_nil assigns(:pages_by_date)
750
770
751 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
771 assert_tag 'a', :attributes => { :href => '/projects/ecookbook/activity.atom?show_wiki_edits=1'}
752 end
772 end
753
773
754 def test_not_found
774 def test_not_found
755 get :show, :project_id => 999
775 get :show, :project_id => 999
756 assert_response 404
776 assert_response 404
757 end
777 end
758
778
759 def test_protect_page
779 def test_protect_page
760 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
780 page = WikiPage.find_by_wiki_id_and_title(1, 'Another_page')
761 assert !page.protected?
781 assert !page.protected?
762 @request.session[:user_id] = 2
782 @request.session[:user_id] = 2
763 post :protect, :project_id => 1, :id => page.title, :protected => '1'
783 post :protect, :project_id => 1, :id => page.title, :protected => '1'
764 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
784 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'Another_page'
765 assert page.reload.protected?
785 assert page.reload.protected?
766 end
786 end
767
787
768 def test_unprotect_page
788 def test_unprotect_page
769 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
789 page = WikiPage.find_by_wiki_id_and_title(1, 'CookBook_documentation')
770 assert page.protected?
790 assert page.protected?
771 @request.session[:user_id] = 2
791 @request.session[:user_id] = 2
772 post :protect, :project_id => 1, :id => page.title, :protected => '0'
792 post :protect, :project_id => 1, :id => page.title, :protected => '0'
773 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation'
793 assert_redirected_to :action => 'show', :project_id => 'ecookbook', :id => 'CookBook_documentation'
774 assert !page.reload.protected?
794 assert !page.reload.protected?
775 end
795 end
776
796
777 def test_show_page_with_edit_link
797 def test_show_page_with_edit_link
778 @request.session[:user_id] = 2
798 @request.session[:user_id] = 2
779 get :show, :project_id => 1
799 get :show, :project_id => 1
780 assert_response :success
800 assert_response :success
781 assert_template 'show'
801 assert_template 'show'
782 assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
802 assert_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
783 end
803 end
784
804
785 def test_show_page_without_edit_link
805 def test_show_page_without_edit_link
786 @request.session[:user_id] = 4
806 @request.session[:user_id] = 4
787 get :show, :project_id => 1
807 get :show, :project_id => 1
788 assert_response :success
808 assert_response :success
789 assert_template 'show'
809 assert_template 'show'
790 assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
810 assert_no_tag :tag => 'a', :attributes => { :href => '/projects/1/wiki/CookBook_documentation/edit' }
791 end
811 end
792
812
793 def test_show_pdf
813 def test_show_pdf
794 @request.session[:user_id] = 2
814 @request.session[:user_id] = 2
795 get :show, :project_id => 1, :format => 'pdf'
815 get :show, :project_id => 1, :format => 'pdf'
796 assert_response :success
816 assert_response :success
797 assert_not_nil assigns(:page)
817 assert_not_nil assigns(:page)
798 assert_equal 'application/pdf', @response.content_type
818 assert_equal 'application/pdf', @response.content_type
799 assert_equal 'attachment; filename="CookBook_documentation.pdf"',
819 assert_equal 'attachment; filename="CookBook_documentation.pdf"',
800 @response.headers['Content-Disposition']
820 @response.headers['Content-Disposition']
801 end
821 end
802
822
803 def test_show_html
823 def test_show_html
804 @request.session[:user_id] = 2
824 @request.session[:user_id] = 2
805 get :show, :project_id => 1, :format => 'html'
825 get :show, :project_id => 1, :format => 'html'
806 assert_response :success
826 assert_response :success
807 assert_not_nil assigns(:page)
827 assert_not_nil assigns(:page)
808 assert_equal 'text/html', @response.content_type
828 assert_equal 'text/html', @response.content_type
809 assert_equal 'attachment; filename="CookBook_documentation.html"',
829 assert_equal 'attachment; filename="CookBook_documentation.html"',
810 @response.headers['Content-Disposition']
830 @response.headers['Content-Disposition']
811 assert_tag 'h1', :content => 'CookBook documentation'
831 assert_tag 'h1', :content => 'CookBook documentation'
812 end
832 end
813
833
814 def test_show_versioned_html
834 def test_show_versioned_html
815 @request.session[:user_id] = 2
835 @request.session[:user_id] = 2
816 get :show, :project_id => 1, :format => 'html', :version => 2
836 get :show, :project_id => 1, :format => 'html', :version => 2
817 assert_response :success
837 assert_response :success
818 assert_not_nil assigns(:content)
838 assert_not_nil assigns(:content)
819 assert_equal 2, assigns(:content).version
839 assert_equal 2, assigns(:content).version
820 assert_equal 'text/html', @response.content_type
840 assert_equal 'text/html', @response.content_type
821 assert_equal 'attachment; filename="CookBook_documentation.html"',
841 assert_equal 'attachment; filename="CookBook_documentation.html"',
822 @response.headers['Content-Disposition']
842 @response.headers['Content-Disposition']
823 assert_tag 'h1', :content => 'CookBook documentation'
843 assert_tag 'h1', :content => 'CookBook documentation'
824 end
844 end
825
845
826 def test_show_txt
846 def test_show_txt
827 @request.session[:user_id] = 2
847 @request.session[:user_id] = 2
828 get :show, :project_id => 1, :format => 'txt'
848 get :show, :project_id => 1, :format => 'txt'
829 assert_response :success
849 assert_response :success
830 assert_not_nil assigns(:page)
850 assert_not_nil assigns(:page)
831 assert_equal 'text/plain', @response.content_type
851 assert_equal 'text/plain', @response.content_type
832 assert_equal 'attachment; filename="CookBook_documentation.txt"',
852 assert_equal 'attachment; filename="CookBook_documentation.txt"',
833 @response.headers['Content-Disposition']
853 @response.headers['Content-Disposition']
834 assert_include 'h1. CookBook documentation', @response.body
854 assert_include 'h1. CookBook documentation', @response.body
835 end
855 end
836
856
837 def test_show_versioned_txt
857 def test_show_versioned_txt
838 @request.session[:user_id] = 2
858 @request.session[:user_id] = 2
839 get :show, :project_id => 1, :format => 'txt', :version => 2
859 get :show, :project_id => 1, :format => 'txt', :version => 2
840 assert_response :success
860 assert_response :success
841 assert_not_nil assigns(:content)
861 assert_not_nil assigns(:content)
842 assert_equal 2, assigns(:content).version
862 assert_equal 2, assigns(:content).version
843 assert_equal 'text/plain', @response.content_type
863 assert_equal 'text/plain', @response.content_type
844 assert_equal 'attachment; filename="CookBook_documentation.txt"',
864 assert_equal 'attachment; filename="CookBook_documentation.txt"',
845 @response.headers['Content-Disposition']
865 @response.headers['Content-Disposition']
846 assert_include 'h1. CookBook documentation', @response.body
866 assert_include 'h1. CookBook documentation', @response.body
847 end
867 end
848
868
849 def test_edit_unprotected_page
869 def test_edit_unprotected_page
850 # Non members can edit unprotected wiki pages
870 # Non members can edit unprotected wiki pages
851 @request.session[:user_id] = 4
871 @request.session[:user_id] = 4
852 get :edit, :project_id => 1, :id => 'Another_page'
872 get :edit, :project_id => 1, :id => 'Another_page'
853 assert_response :success
873 assert_response :success
854 assert_template 'edit'
874 assert_template 'edit'
855 end
875 end
856
876
857 def test_edit_protected_page_by_nonmember
877 def test_edit_protected_page_by_nonmember
858 # Non members can't edit protected wiki pages
878 # Non members can't edit protected wiki pages
859 @request.session[:user_id] = 4
879 @request.session[:user_id] = 4
860 get :edit, :project_id => 1, :id => 'CookBook_documentation'
880 get :edit, :project_id => 1, :id => 'CookBook_documentation'
861 assert_response 403
881 assert_response 403
862 end
882 end
863
883
864 def test_edit_protected_page_by_member
884 def test_edit_protected_page_by_member
865 @request.session[:user_id] = 2
885 @request.session[:user_id] = 2
866 get :edit, :project_id => 1, :id => 'CookBook_documentation'
886 get :edit, :project_id => 1, :id => 'CookBook_documentation'
867 assert_response :success
887 assert_response :success
868 assert_template 'edit'
888 assert_template 'edit'
869 end
889 end
870
890
871 def test_history_of_non_existing_page_should_return_404
891 def test_history_of_non_existing_page_should_return_404
872 get :history, :project_id => 1, :id => 'Unknown_page'
892 get :history, :project_id => 1, :id => 'Unknown_page'
873 assert_response 404
893 assert_response 404
874 end
894 end
875
895
876 def test_add_attachment
896 def test_add_attachment
877 @request.session[:user_id] = 2
897 @request.session[:user_id] = 2
878 assert_difference 'Attachment.count' do
898 assert_difference 'Attachment.count' do
879 post :add_attachment, :project_id => 1, :id => 'CookBook_documentation',
899 post :add_attachment, :project_id => 1, :id => 'CookBook_documentation',
880 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
900 :attachments => {'1' => {'file' => uploaded_test_file('testfile.txt', 'text/plain'), 'description' => 'test file'}}
881 end
901 end
882 attachment = Attachment.first(:order => 'id DESC')
902 attachment = Attachment.first(:order => 'id DESC')
883 assert_equal Wiki.find(1).find_page('CookBook_documentation'), attachment.container
903 assert_equal Wiki.find(1).find_page('CookBook_documentation'), attachment.container
884 end
904 end
885 end
905 end
@@ -1,121 +1,131
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 RoutingWikiTest < ActionController::IntegrationTest
20 class RoutingWikiTest < ActionController::IntegrationTest
21 def test_wiki_matching
21 def test_wiki_matching
22 assert_routing(
22 assert_routing(
23 { :method => 'get', :path => "/projects/567/wiki" },
23 { :method => 'get', :path => "/projects/567/wiki" },
24 { :controller => 'wiki', :action => 'show', :project_id => '567' }
24 { :controller => 'wiki', :action => 'show', :project_id => '567' }
25 )
25 )
26 assert_routing(
26 assert_routing(
27 { :method => 'get', :path => "/projects/567/wiki/lalala" },
27 { :method => 'get', :path => "/projects/567/wiki/lalala" },
28 { :controller => 'wiki', :action => 'show', :project_id => '567',
28 { :controller => 'wiki', :action => 'show', :project_id => '567',
29 :id => 'lalala' }
29 :id => 'lalala' }
30 )
30 )
31 assert_routing(
31 assert_routing(
32 { :method => 'get', :path => "/projects/567/wiki/lalala.pdf" },
32 { :method => 'get', :path => "/projects/567/wiki/lalala.pdf" },
33 { :controller => 'wiki', :action => 'show', :project_id => '567',
33 { :controller => 'wiki', :action => 'show', :project_id => '567',
34 :id => 'lalala', :format => 'pdf' }
34 :id => 'lalala', :format => 'pdf' }
35 )
35 )
36 assert_routing(
36 assert_routing(
37 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/diff" },
37 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/diff" },
38 { :controller => 'wiki', :action => 'diff', :project_id => '1',
38 { :controller => 'wiki', :action => 'diff', :project_id => '1',
39 :id => 'CookBook_documentation' }
39 :id => 'CookBook_documentation' }
40 )
40 )
41 assert_routing(
41 assert_routing(
42 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2" },
43 { :controller => 'wiki', :action => 'show', :project_id => '1',
44 :id => 'CookBook_documentation', :version => '2' }
45 )
46 assert_routing(
42 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/diff" },
47 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/diff" },
43 { :controller => 'wiki', :action => 'diff', :project_id => '1',
48 { :controller => 'wiki', :action => 'diff', :project_id => '1',
44 :id => 'CookBook_documentation', :version => '2' }
49 :id => 'CookBook_documentation', :version => '2' }
45 )
50 )
46 assert_routing(
51 assert_routing(
47 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/annotate" },
52 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/2/annotate" },
48 { :controller => 'wiki', :action => 'annotate', :project_id => '1',
53 { :controller => 'wiki', :action => 'annotate', :project_id => '1',
49 :id => 'CookBook_documentation', :version => '2' }
54 :id => 'CookBook_documentation', :version => '2' }
50 )
55 )
51 end
56 end
52
57
53 def test_wiki_misc
58 def test_wiki_misc
54 assert_routing(
59 assert_routing(
55 { :method => 'get', :path => "/projects/567/wiki/date_index" },
60 { :method => 'get', :path => "/projects/567/wiki/date_index" },
56 { :controller => 'wiki', :action => 'date_index', :project_id => '567' }
61 { :controller => 'wiki', :action => 'date_index', :project_id => '567' }
57 )
62 )
58 assert_routing(
63 assert_routing(
59 { :method => 'get', :path => "/projects/567/wiki/export" },
64 { :method => 'get', :path => "/projects/567/wiki/export" },
60 { :controller => 'wiki', :action => 'export', :project_id => '567' }
65 { :controller => 'wiki', :action => 'export', :project_id => '567' }
61 )
66 )
62 assert_routing(
67 assert_routing(
63 { :method => 'get', :path => "/projects/567/wiki/export.pdf" },
68 { :method => 'get', :path => "/projects/567/wiki/export.pdf" },
64 { :controller => 'wiki', :action => 'export', :project_id => '567', :format => 'pdf' }
69 { :controller => 'wiki', :action => 'export', :project_id => '567', :format => 'pdf' }
65 )
70 )
66 assert_routing(
71 assert_routing(
67 { :method => 'get', :path => "/projects/567/wiki/index" },
72 { :method => 'get', :path => "/projects/567/wiki/index" },
68 { :controller => 'wiki', :action => 'index', :project_id => '567' }
73 { :controller => 'wiki', :action => 'index', :project_id => '567' }
69 )
74 )
70 end
75 end
71
76
72 def test_wiki_resources
77 def test_wiki_resources
73 assert_routing(
78 assert_routing(
74 { :method => 'get', :path => "/projects/567/wiki/my_page/edit" },
79 { :method => 'get', :path => "/projects/567/wiki/my_page/edit" },
75 { :controller => 'wiki', :action => 'edit', :project_id => '567',
80 { :controller => 'wiki', :action => 'edit', :project_id => '567',
76 :id => 'my_page' }
81 :id => 'my_page' }
77 )
82 )
78 assert_routing(
83 assert_routing(
79 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/history" },
84 { :method => 'get', :path => "/projects/1/wiki/CookBook_documentation/history" },
80 { :controller => 'wiki', :action => 'history', :project_id => '1',
85 { :controller => 'wiki', :action => 'history', :project_id => '1',
81 :id => 'CookBook_documentation' }
86 :id => 'CookBook_documentation' }
82 )
87 )
83 assert_routing(
88 assert_routing(
84 { :method => 'get', :path => "/projects/22/wiki/ladida/rename" },
89 { :method => 'get', :path => "/projects/22/wiki/ladida/rename" },
85 { :controller => 'wiki', :action => 'rename', :project_id => '22',
90 { :controller => 'wiki', :action => 'rename', :project_id => '22',
86 :id => 'ladida' }
91 :id => 'ladida' }
87 )
92 )
88 ["post", "put"].each do |method|
93 ["post", "put"].each do |method|
89 assert_routing(
94 assert_routing(
90 { :method => method, :path => "/projects/567/wiki/CookBook_documentation/preview" },
95 { :method => method, :path => "/projects/567/wiki/CookBook_documentation/preview" },
91 { :controller => 'wiki', :action => 'preview', :project_id => '567',
96 { :controller => 'wiki', :action => 'preview', :project_id => '567',
92 :id => 'CookBook_documentation' }
97 :id => 'CookBook_documentation' }
93 )
98 )
94 end
99 end
95 assert_routing(
100 assert_routing(
96 { :method => 'post', :path => "/projects/22/wiki/ladida/rename" },
101 { :method => 'post', :path => "/projects/22/wiki/ladida/rename" },
97 { :controller => 'wiki', :action => 'rename', :project_id => '22',
102 { :controller => 'wiki', :action => 'rename', :project_id => '22',
98 :id => 'ladida' }
103 :id => 'ladida' }
99 )
104 )
100 assert_routing(
105 assert_routing(
101 { :method => 'post', :path => "/projects/22/wiki/ladida/protect" },
106 { :method => 'post', :path => "/projects/22/wiki/ladida/protect" },
102 { :controller => 'wiki', :action => 'protect', :project_id => '22',
107 { :controller => 'wiki', :action => 'protect', :project_id => '22',
103 :id => 'ladida' }
108 :id => 'ladida' }
104 )
109 )
105 assert_routing(
110 assert_routing(
106 { :method => 'post', :path => "/projects/22/wiki/ladida/add_attachment" },
111 { :method => 'post', :path => "/projects/22/wiki/ladida/add_attachment" },
107 { :controller => 'wiki', :action => 'add_attachment', :project_id => '22',
112 { :controller => 'wiki', :action => 'add_attachment', :project_id => '22',
108 :id => 'ladida' }
113 :id => 'ladida' }
109 )
114 )
110 assert_routing(
115 assert_routing(
111 { :method => 'put', :path => "/projects/567/wiki/my_page" },
116 { :method => 'put', :path => "/projects/567/wiki/my_page" },
112 { :controller => 'wiki', :action => 'update', :project_id => '567',
117 { :controller => 'wiki', :action => 'update', :project_id => '567',
113 :id => 'my_page' }
118 :id => 'my_page' }
114 )
119 )
115 assert_routing(
120 assert_routing(
116 { :method => 'delete', :path => "/projects/22/wiki/ladida" },
121 { :method => 'delete', :path => "/projects/22/wiki/ladida" },
117 { :controller => 'wiki', :action => 'destroy', :project_id => '22',
122 { :controller => 'wiki', :action => 'destroy', :project_id => '22',
118 :id => 'ladida' }
123 :id => 'ladida' }
119 )
124 )
125 assert_routing(
126 { :method => 'delete', :path => "/projects/22/wiki/ladida/3" },
127 { :controller => 'wiki', :action => 'destroy_version', :project_id => '22',
128 :id => 'ladida', :version => '3' }
129 )
120 end
130 end
121 end
131 end
General Comments 0
You need to be logged in to leave comments. Login now