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