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