##// END OF EJS Templates
Ability to watch a wiki or a single wiki page (#413)....
Jean-Philippe Lang -
r2666:85ce903cfa5f
parent child
Show More
@@ -1,233 +1,234
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 class WikiController < ApplicationController
21 21 before_filter :find_wiki, :authorize
22 22 before_filter :find_existing_page, :only => [:rename, :protect, :history, :diff, :annotate, :add_attachment, :destroy]
23 23
24 24 verify :method => :post, :only => [:destroy, :protect], :redirect_to => { :action => :index }
25 25
26 26 helper :attachments
27 27 include AttachmentsHelper
28 helper :watchers
28 29
29 30 # display a page (in editing mode if it doesn't exist)
30 31 def index
31 32 page_title = params[:page]
32 33 @page = @wiki.find_or_new_page(page_title)
33 34 if @page.new_record?
34 35 if User.current.allowed_to?(:edit_wiki_pages, @project)
35 36 edit
36 37 render :action => 'edit'
37 38 else
38 39 render_404
39 40 end
40 41 return
41 42 end
42 43 if params[:version] && !User.current.allowed_to?(:view_wiki_edits, @project)
43 44 # Redirects user to the current version if he's not allowed to view previous versions
44 45 redirect_to :version => nil
45 46 return
46 47 end
47 48 @content = @page.content_for_version(params[:version])
48 49 if params[:format] == 'html'
49 50 export = render_to_string :action => 'export', :layout => false
50 51 send_data(export, :type => 'text/html', :filename => "#{@page.title}.html")
51 52 return
52 53 elsif params[:format] == 'txt'
53 54 send_data(@content.text, :type => 'text/plain', :filename => "#{@page.title}.txt")
54 55 return
55 56 end
56 57 @editable = editable?
57 58 render :action => 'show'
58 59 end
59 60
60 61 # edit an existing page or a new one
61 62 def edit
62 63 @page = @wiki.find_or_new_page(params[:page])
63 64 return render_403 unless editable?
64 65 @page.content = WikiContent.new(:page => @page) if @page.new_record?
65 66
66 67 @content = @page.content_for_version(params[:version])
67 68 @content.text = initial_page_content(@page) if @content.text.blank?
68 69 # don't keep previous comment
69 70 @content.comments = nil
70 71 if request.get?
71 72 # To prevent StaleObjectError exception when reverting to a previous version
72 73 @content.version = @page.content.version
73 74 else
74 75 if !@page.new_record? && @content.text == params[:content][:text]
75 76 # don't save if text wasn't changed
76 77 redirect_to :action => 'index', :id => @project, :page => @page.title
77 78 return
78 79 end
79 80 #@content.text = params[:content][:text]
80 81 #@content.comments = params[:content][:comments]
81 82 @content.attributes = params[:content]
82 83 @content.author = User.current
83 84 # if page is new @page.save will also save content, but not if page isn't a new record
84 85 if (@page.new_record? ? @page.save : @content.save)
85 86 redirect_to :action => 'index', :id => @project, :page => @page.title
86 87 end
87 88 end
88 89 rescue ActiveRecord::StaleObjectError
89 90 # Optimistic locking exception
90 91 flash[:error] = l(:notice_locking_conflict)
91 92 end
92 93
93 94 # rename a page
94 95 def rename
95 96 return render_403 unless editable?
96 97 @page.redirect_existing_links = true
97 98 # used to display the *original* title if some AR validation errors occur
98 99 @original_title = @page.pretty_title
99 100 if request.post? && @page.update_attributes(params[:wiki_page])
100 101 flash[:notice] = l(:notice_successful_update)
101 102 redirect_to :action => 'index', :id => @project, :page => @page.title
102 103 end
103 104 end
104 105
105 106 def protect
106 107 @page.update_attribute :protected, params[:protected]
107 108 redirect_to :action => 'index', :id => @project, :page => @page.title
108 109 end
109 110
110 111 # show page history
111 112 def history
112 113 @version_count = @page.content.versions.count
113 114 @version_pages = Paginator.new self, @version_count, per_page_option, params['p']
114 115 # don't load text
115 116 @versions = @page.content.versions.find :all,
116 117 :select => "id, author_id, comments, updated_on, version",
117 118 :order => 'version DESC',
118 119 :limit => @version_pages.items_per_page + 1,
119 120 :offset => @version_pages.current.offset
120 121
121 122 render :layout => false if request.xhr?
122 123 end
123 124
124 125 def diff
125 126 @diff = @page.diff(params[:version], params[:version_from])
126 127 render_404 unless @diff
127 128 end
128 129
129 130 def annotate
130 131 @annotate = @page.annotate(params[:version])
131 132 render_404 unless @annotate
132 133 end
133 134
134 135 # Removes a wiki page and its history
135 136 # Children can be either set as root pages, removed or reassigned to another parent page
136 137 def destroy
137 138 return render_403 unless editable?
138 139
139 140 @descendants_count = @page.descendants.size
140 141 if @descendants_count > 0
141 142 case params[:todo]
142 143 when 'nullify'
143 144 # Nothing to do
144 145 when 'destroy'
145 146 # Removes all its descendants
146 147 @page.descendants.each(&:destroy)
147 148 when 'reassign'
148 149 # Reassign children to another parent page
149 150 reassign_to = @wiki.pages.find_by_id(params[:reassign_to_id].to_i)
150 151 return unless reassign_to
151 152 @page.children.each do |child|
152 153 child.update_attribute(:parent, reassign_to)
153 154 end
154 155 else
155 156 @reassignable_to = @wiki.pages - @page.self_and_descendants
156 157 return
157 158 end
158 159 end
159 160 @page.destroy
160 161 redirect_to :action => 'special', :id => @project, :page => 'Page_index'
161 162 end
162 163
163 164 # display special pages
164 165 def special
165 166 page_title = params[:page].downcase
166 167 case page_title
167 168 # show pages index, sorted by title
168 169 when 'page_index', 'date_index'
169 170 # eager load information about last updates, without loading text
170 171 @pages = @wiki.pages.find :all, :select => "#{WikiPage.table_name}.*, #{WikiContent.table_name}.updated_on",
171 172 :joins => "LEFT JOIN #{WikiContent.table_name} ON #{WikiContent.table_name}.page_id = #{WikiPage.table_name}.id",
172 173 :order => 'title'
173 174 @pages_by_date = @pages.group_by {|p| p.updated_on.to_date}
174 175 @pages_by_parent_id = @pages.group_by(&:parent_id)
175 176 # export wiki to a single html file
176 177 when 'export'
177 178 @pages = @wiki.pages.find :all, :order => 'title'
178 179 export = render_to_string :action => 'export_multiple', :layout => false
179 180 send_data(export, :type => 'text/html', :filename => "wiki.html")
180 181 return
181 182 else
182 183 # requested special page doesn't exist, redirect to default page
183 184 redirect_to :action => 'index', :id => @project, :page => nil and return
184 185 end
185 186 render :action => "special_#{page_title}"
186 187 end
187 188
188 189 def preview
189 190 page = @wiki.find_page(params[:page])
190 191 # page is nil when previewing a new page
191 192 return render_403 unless page.nil? || editable?(page)
192 193 if page
193 194 @attachements = page.attachments
194 195 @previewed = page.content
195 196 end
196 197 @text = params[:content][:text]
197 198 render :partial => 'common/preview'
198 199 end
199 200
200 201 def add_attachment
201 202 return render_403 unless editable?
202 203 attach_files(@page, params[:attachments])
203 204 redirect_to :action => 'index', :page => @page.title
204 205 end
205 206
206 207 private
207 208
208 209 def find_wiki
209 210 @project = Project.find(params[:id])
210 211 @wiki = @project.wiki
211 212 render_404 unless @wiki
212 213 rescue ActiveRecord::RecordNotFound
213 214 render_404
214 215 end
215 216
216 217 # Finds the requested page and returns a 404 error if it doesn't exist
217 218 def find_existing_page
218 219 @page = @wiki.find_page(params[:page])
219 220 render_404 if @page.nil?
220 221 end
221 222
222 223 # Returns true if the current user is allowed to edit the page, otherwise false
223 224 def editable?(page = @page)
224 225 page.editable_by?(User.current)
225 226 end
226 227
227 228 # Returns the default content of a new wiki page
228 229 def initial_page_content(page)
229 230 helper = Redmine::WikiFormatting.helper_for(Setting.text_formatting)
230 231 extend helper unless self.instance_of?(helper)
231 232 helper.instance_method(:initial_page_content).bind(self).call(page)
232 233 end
233 234 end
@@ -1,385 +1,387
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 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 class Mailer < ActionMailer::Base
19 19 helper :application
20 20 helper :issues
21 21 helper :custom_fields
22 22
23 23 include ActionController::UrlWriter
24 24 include Redmine::I18n
25 25
26 26 def self.default_url_options
27 27 h = Setting.host_name
28 28 h = h.to_s.gsub(%r{\/.*$}, '') unless Redmine::Utils.relative_url_root.blank?
29 29 { :host => h, :protocol => Setting.protocol }
30 30 end
31 31
32 32 # Builds a tmail object used to email recipients of the added issue.
33 33 #
34 34 # Example:
35 35 # issue_add(issue) => tmail object
36 36 # Mailer.deliver_issue_add(issue) => sends an email to issue recipients
37 37 def issue_add(issue)
38 38 redmine_headers 'Project' => issue.project.identifier,
39 39 'Issue-Id' => issue.id,
40 40 'Issue-Author' => issue.author.login
41 41 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
42 42 message_id issue
43 43 recipients issue.recipients
44 44 cc(issue.watcher_recipients - @recipients)
45 45 subject "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] (#{issue.status.name}) #{issue.subject}"
46 46 body :issue => issue,
47 47 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
48 48 end
49 49
50 50 # Builds a tmail object used to email recipients of the edited issue.
51 51 #
52 52 # Example:
53 53 # issue_edit(journal) => tmail object
54 54 # Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
55 55 def issue_edit(journal)
56 56 issue = journal.journalized.reload
57 57 redmine_headers 'Project' => issue.project.identifier,
58 58 'Issue-Id' => issue.id,
59 59 'Issue-Author' => issue.author.login
60 60 redmine_headers 'Issue-Assignee' => issue.assigned_to.login if issue.assigned_to
61 61 message_id journal
62 62 references issue
63 63 @author = journal.user
64 64 recipients issue.recipients
65 65 # Watchers in cc
66 66 cc(issue.watcher_recipients - @recipients)
67 67 s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
68 68 s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
69 69 s << issue.subject
70 70 subject s
71 71 body :issue => issue,
72 72 :journal => journal,
73 73 :issue_url => url_for(:controller => 'issues', :action => 'show', :id => issue)
74 74 end
75 75
76 76 def reminder(user, issues, days)
77 77 set_language_if_valid user.language
78 78 recipients user.mail
79 79 subject l(:mail_subject_reminder, issues.size)
80 80 body :issues => issues,
81 81 :days => days,
82 82 :issues_url => url_for(:controller => 'issues', :action => 'index', :set_filter => 1, :assigned_to_id => user.id, :sort_key => 'due_date', :sort_order => 'asc')
83 83 end
84 84
85 85 # Builds a tmail object used to email users belonging to the added document's project.
86 86 #
87 87 # Example:
88 88 # document_added(document) => tmail object
89 89 # Mailer.deliver_document_added(document) => sends an email to the document's project recipients
90 90 def document_added(document)
91 91 redmine_headers 'Project' => document.project.identifier
92 92 recipients document.project.recipients
93 93 subject "[#{document.project.name}] #{l(:label_document_new)}: #{document.title}"
94 94 body :document => document,
95 95 :document_url => url_for(:controller => 'documents', :action => 'show', :id => document)
96 96 end
97 97
98 98 # Builds a tmail object used to email recipients of a project when an attachements are added.
99 99 #
100 100 # Example:
101 101 # attachments_added(attachments) => tmail object
102 102 # Mailer.deliver_attachments_added(attachments) => sends an email to the project's recipients
103 103 def attachments_added(attachments)
104 104 container = attachments.first.container
105 105 added_to = ''
106 106 added_to_url = ''
107 107 case container.class.name
108 108 when 'Project'
109 109 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container)
110 110 added_to = "#{l(:label_project)}: #{container}"
111 111 when 'Version'
112 112 added_to_url = url_for(:controller => 'projects', :action => 'list_files', :id => container.project_id)
113 113 added_to = "#{l(:label_version)}: #{container.name}"
114 114 when 'Document'
115 115 added_to_url = url_for(:controller => 'documents', :action => 'show', :id => container.id)
116 116 added_to = "#{l(:label_document)}: #{container.title}"
117 117 end
118 118 redmine_headers 'Project' => container.project.identifier
119 119 recipients container.project.recipients
120 120 subject "[#{container.project.name}] #{l(:label_attachment_new)}"
121 121 body :attachments => attachments,
122 122 :added_to => added_to,
123 123 :added_to_url => added_to_url
124 124 end
125 125
126 126 # Builds a tmail object used to email recipients of a news' project when a news item is added.
127 127 #
128 128 # Example:
129 129 # news_added(news) => tmail object
130 130 # Mailer.deliver_news_added(news) => sends an email to the news' project recipients
131 131 def news_added(news)
132 132 redmine_headers 'Project' => news.project.identifier
133 133 message_id news
134 134 recipients news.project.recipients
135 135 subject "[#{news.project.name}] #{l(:label_news)}: #{news.title}"
136 136 body :news => news,
137 137 :news_url => url_for(:controller => 'news', :action => 'show', :id => news)
138 138 end
139 139
140 140 # Builds a tmail object used to email the specified recipients of the specified message that was posted.
141 141 #
142 142 # Example:
143 143 # message_posted(message, recipients) => tmail object
144 144 # Mailer.deliver_message_posted(message, recipients) => sends an email to the recipients
145 145 def message_posted(message, recipients)
146 146 redmine_headers 'Project' => message.project.identifier,
147 147 'Topic-Id' => (message.parent_id || message.id)
148 148 message_id message
149 149 references message.parent unless message.parent.nil?
150 150 recipients(recipients)
151 151 subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
152 152 body :message => message,
153 153 :message_url => url_for(:controller => 'messages', :action => 'show', :board_id => message.board_id, :id => message.root)
154 154 end
155 155
156 156 # Builds a tmail object used to email the recipients of a project of the specified wiki content was added.
157 157 #
158 158 # Example:
159 159 # wiki_content_added(wiki_content) => tmail object
160 160 # Mailer.deliver_wiki_content_added(wiki_content) => sends an email to the project's recipients
161 161 def wiki_content_added(wiki_content)
162 162 redmine_headers 'Project' => wiki_content.project.identifier,
163 163 'Wiki-Page-Id' => wiki_content.page.id
164 164 message_id wiki_content
165 165 recipients wiki_content.project.recipients
166 cc(wiki_content.page.wiki.watcher_recipients - recipients)
166 167 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_added, :page => wiki_content.page.pretty_title)}"
167 168 body :wiki_content => wiki_content,
168 169 :wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title)
169 170 end
170 171
171 172 # Builds a tmail object used to email the recipients of a project of the specified wiki content was updated.
172 173 #
173 174 # Example:
174 175 # wiki_content_updated(wiki_content) => tmail object
175 176 # Mailer.deliver_wiki_content_updated(wiki_content) => sends an email to the project's recipients
176 177 def wiki_content_updated(wiki_content)
177 178 redmine_headers 'Project' => wiki_content.project.identifier,
178 179 'Wiki-Page-Id' => wiki_content.page.id
179 180 message_id wiki_content
180 181 recipients wiki_content.project.recipients
182 cc(wiki_content.page.wiki.watcher_recipients + wiki_content.page.watcher_recipients - recipients)
181 183 subject "[#{wiki_content.project.name}] #{l(:mail_subject_wiki_content_updated, :page => wiki_content.page.pretty_title)}"
182 184 body :wiki_content => wiki_content,
183 185 :wiki_content_url => url_for(:controller => 'wiki', :action => 'index', :id => wiki_content.project, :page => wiki_content.page.title),
184 186 :wiki_diff_url => url_for(:controller => 'wiki', :action => 'diff', :id => wiki_content.project, :page => wiki_content.page.title, :version => wiki_content.version)
185 187 end
186 188
187 189 # Builds a tmail object used to email the specified user their account information.
188 190 #
189 191 # Example:
190 192 # account_information(user, password) => tmail object
191 193 # Mailer.deliver_account_information(user, password) => sends account information to the user
192 194 def account_information(user, password)
193 195 set_language_if_valid user.language
194 196 recipients user.mail
195 197 subject l(:mail_subject_register, Setting.app_title)
196 198 body :user => user,
197 199 :password => password,
198 200 :login_url => url_for(:controller => 'account', :action => 'login')
199 201 end
200 202
201 203 # Builds a tmail object used to email all active administrators of an account activation request.
202 204 #
203 205 # Example:
204 206 # account_activation_request(user) => tmail object
205 207 # Mailer.deliver_account_activation_request(user)=> sends an email to all active administrators
206 208 def account_activation_request(user)
207 209 # Send the email to all active administrators
208 210 recipients User.active.find(:all, :conditions => {:admin => true}).collect { |u| u.mail }.compact
209 211 subject l(:mail_subject_account_activation_request, Setting.app_title)
210 212 body :user => user,
211 213 :url => url_for(:controller => 'users', :action => 'index', :status => User::STATUS_REGISTERED, :sort_key => 'created_on', :sort_order => 'desc')
212 214 end
213 215
214 216 # Builds a tmail object used to email the specified user that their account was activated by an administrator.
215 217 #
216 218 # Example:
217 219 # account_activated(user) => tmail object
218 220 # Mailer.deliver_account_activated(user) => sends an email to the registered user
219 221 def account_activated(user)
220 222 set_language_if_valid user.language
221 223 recipients user.mail
222 224 subject l(:mail_subject_register, Setting.app_title)
223 225 body :user => user,
224 226 :login_url => url_for(:controller => 'account', :action => 'login')
225 227 end
226 228
227 229 def lost_password(token)
228 230 set_language_if_valid(token.user.language)
229 231 recipients token.user.mail
230 232 subject l(:mail_subject_lost_password, Setting.app_title)
231 233 body :token => token,
232 234 :url => url_for(:controller => 'account', :action => 'lost_password', :token => token.value)
233 235 end
234 236
235 237 def register(token)
236 238 set_language_if_valid(token.user.language)
237 239 recipients token.user.mail
238 240 subject l(:mail_subject_register, Setting.app_title)
239 241 body :token => token,
240 242 :url => url_for(:controller => 'account', :action => 'activate', :token => token.value)
241 243 end
242 244
243 245 def test(user)
244 246 set_language_if_valid(user.language)
245 247 recipients user.mail
246 248 subject 'Redmine test'
247 249 body :url => url_for(:controller => 'welcome')
248 250 end
249 251
250 252 # Overrides default deliver! method to prevent from sending an email
251 253 # with no recipient, cc or bcc
252 254 def deliver!(mail = @mail)
253 255 return false if (recipients.nil? || recipients.empty?) &&
254 256 (cc.nil? || cc.empty?) &&
255 257 (bcc.nil? || bcc.empty?)
256 258
257 259 # Set Message-Id and References
258 260 if @message_id_object
259 261 mail.message_id = self.class.message_id_for(@message_id_object)
260 262 end
261 263 if @references_objects
262 264 mail.references = @references_objects.collect {|o| self.class.message_id_for(o)}
263 265 end
264 266 super(mail)
265 267 end
266 268
267 269 # Sends reminders to issue assignees
268 270 # Available options:
269 271 # * :days => how many days in the future to remind about (defaults to 7)
270 272 # * :tracker => id of tracker for filtering issues (defaults to all trackers)
271 273 # * :project => id or identifier of project to process (defaults to all projects)
272 274 def self.reminders(options={})
273 275 days = options[:days] || 7
274 276 project = options[:project] ? Project.find(options[:project]) : nil
275 277 tracker = options[:tracker] ? Tracker.find(options[:tracker]) : nil
276 278
277 279 s = ARCondition.new ["#{IssueStatus.table_name}.is_closed = ? AND #{Issue.table_name}.due_date <= ?", false, days.day.from_now.to_date]
278 280 s << "#{Issue.table_name}.assigned_to_id IS NOT NULL"
279 281 s << "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}"
280 282 s << "#{Issue.table_name}.project_id = #{project.id}" if project
281 283 s << "#{Issue.table_name}.tracker_id = #{tracker.id}" if tracker
282 284
283 285 issues_by_assignee = Issue.find(:all, :include => [:status, :assigned_to, :project, :tracker],
284 286 :conditions => s.conditions
285 287 ).group_by(&:assigned_to)
286 288 issues_by_assignee.each do |assignee, issues|
287 289 deliver_reminder(assignee, issues, days) unless assignee.nil?
288 290 end
289 291 end
290 292
291 293 private
292 294 def initialize_defaults(method_name)
293 295 super
294 296 set_language_if_valid Setting.default_language
295 297 from Setting.mail_from
296 298
297 299 # Common headers
298 300 headers 'X-Mailer' => 'Redmine',
299 301 'X-Redmine-Host' => Setting.host_name,
300 302 'X-Redmine-Site' => Setting.app_title,
301 303 'Precedence' => 'bulk',
302 304 'Auto-Submitted' => 'auto-generated'
303 305 end
304 306
305 307 # Appends a Redmine header field (name is prepended with 'X-Redmine-')
306 308 def redmine_headers(h)
307 309 h.each { |k,v| headers["X-Redmine-#{k}"] = v }
308 310 end
309 311
310 312 # Overrides the create_mail method
311 313 def create_mail
312 314 # Removes the current user from the recipients and cc
313 315 # if he doesn't want to receive notifications about what he does
314 316 @author ||= User.current
315 317 if @author.pref[:no_self_notified]
316 318 recipients.delete(@author.mail) if recipients
317 319 cc.delete(@author.mail) if cc
318 320 end
319 321 # Blind carbon copy recipients
320 322 if Setting.bcc_recipients?
321 323 bcc([recipients, cc].flatten.compact.uniq)
322 324 recipients []
323 325 cc []
324 326 end
325 327 super
326 328 end
327 329
328 330 # Renders a message with the corresponding layout
329 331 def render_message(method_name, body)
330 332 layout = method_name.to_s.match(%r{text\.html\.(rhtml|rxml)}) ? 'layout.text.html.rhtml' : 'layout.text.plain.rhtml'
331 333 body[:content_for_layout] = render(:file => method_name, :body => body)
332 334 ActionView::Base.new(template_root, body, self).render(:file => "mailer/#{layout}", :use_full_path => true)
333 335 end
334 336
335 337 # for the case of plain text only
336 338 def body(*params)
337 339 value = super(*params)
338 340 if Setting.plain_text_mail?
339 341 templates = Dir.glob("#{template_path}/#{@template}.text.plain.{rhtml,erb}")
340 342 unless String === @body or templates.empty?
341 343 template = File.basename(templates.first)
342 344 @body[:content_for_layout] = render(:file => template, :body => @body)
343 345 @body = ActionView::Base.new(template_root, @body, self).render(:file => "mailer/layout.text.plain.rhtml", :use_full_path => true)
344 346 return @body
345 347 end
346 348 end
347 349 return value
348 350 end
349 351
350 352 # Makes partial rendering work with Rails 1.2 (retro-compatibility)
351 353 def self.controller_path
352 354 ''
353 355 end unless respond_to?('controller_path')
354 356
355 357 # Returns a predictable Message-Id for the given object
356 358 def self.message_id_for(object)
357 359 # id + timestamp should reduce the odds of a collision
358 360 # as far as we don't send multiple emails for the same object
359 361 timestamp = object.send(object.respond_to?(:created_on) ? :created_on : :updated_on)
360 362 hash = "redmine.#{object.class.name.demodulize.underscore}-#{object.id}.#{timestamp.strftime("%Y%m%d%H%M%S")}"
361 363 host = Setting.mail_from.to_s.gsub(%r{^.*@}, '')
362 364 host = "#{::Socket.gethostname}.redmine" if host.empty?
363 365 "<#{hash}@#{host}>"
364 366 end
365 367
366 368 private
367 369
368 370 def message_id(object)
369 371 @message_id_object = object
370 372 end
371 373
372 374 def references(object)
373 375 @references_objects ||= []
374 376 @references_objects << object
375 377 end
376 378 end
377 379
378 380 # Patch TMail so that message_id is not overwritten
379 381 module TMail
380 382 class Mail
381 383 def add_message_id( fqdn = nil )
382 384 self.message_id ||= ::TMail::new_message_id(fqdn)
383 385 end
384 386 end
385 387 end
@@ -1,73 +1,75
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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 class Wiki < ActiveRecord::Base
19 19 belongs_to :project
20 20 has_many :pages, :class_name => 'WikiPage', :dependent => :destroy, :order => 'title'
21 21 has_many :redirects, :class_name => 'WikiRedirect', :dependent => :delete_all
22 22
23 acts_as_watchable
24
23 25 validates_presence_of :start_page
24 26 validates_format_of :start_page, :with => /^[^,\.\/\?\;\|\:]*$/
25 27
26 28 # find the page with the given title
27 29 # if page doesn't exist, return a new page
28 30 def find_or_new_page(title)
29 31 title = start_page if title.blank?
30 32 find_page(title) || WikiPage.new(:wiki => self, :title => Wiki.titleize(title))
31 33 end
32 34
33 35 # find the page with the given title
34 36 def find_page(title, options = {})
35 37 title = start_page if title.blank?
36 38 title = Wiki.titleize(title)
37 39 page = pages.find_by_title(title)
38 40 if !page && !(options[:with_redirect] == false)
39 41 # search for a redirect
40 42 redirect = redirects.find_by_title(title)
41 43 page = find_page(redirect.redirects_to, :with_redirect => false) if redirect
42 44 end
43 45 page
44 46 end
45 47
46 48 # Finds a page by title
47 49 # The given string can be of one of the forms: "title" or "project:title"
48 50 # Examples:
49 51 # Wiki.find_page("bar", project => foo)
50 52 # Wiki.find_page("foo:bar")
51 53 def self.find_page(title, options = {})
52 54 project = options[:project]
53 55 if title.to_s =~ %r{^([^\:]+)\:(.*)$}
54 56 project_identifier, title = $1, $2
55 57 project = Project.find_by_identifier(project_identifier) || Project.find_by_name(project_identifier)
56 58 end
57 59 if project && project.wiki
58 60 page = project.wiki.find_page(title)
59 61 if page && page.content
60 62 page
61 63 end
62 64 end
63 65 end
64 66
65 67 # turn a string into a valid page title
66 68 def self.titleize(title)
67 69 # replace spaces with _ and remove unwanted caracters
68 70 title = title.gsub(/\s+/, '_').delete(',./?;|:') if title
69 71 # upcase the first letter
70 72 title = (title.slice(0..0).upcase + (title.slice(1..-1) || '')) if title
71 73 title
72 74 end
73 75 end
@@ -1,188 +1,189
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
1 # Redmine - project management software
2 # Copyright (C) 2006-2009 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 acts_as_watchable
27 28 acts_as_event :title => Proc.new {|o| "#{l(:label_wiki)}: #{o.title}"},
28 29 :description => :text,
29 30 :datetime => :created_on,
30 31 :url => Proc.new {|o| {:controller => 'wiki', :id => o.wiki.project_id, :page => o.title}}
31 32
32 33 acts_as_searchable :columns => ['title', 'text'],
33 34 :include => [{:wiki => :project}, :content],
34 35 :project_key => "#{Wiki.table_name}.project_id"
35 36
36 37 attr_accessor :redirect_existing_links
37 38
38 39 validates_presence_of :title
39 40 validates_format_of :title, :with => /^[^,\.\/\?\;\|\s]*$/
40 41 validates_uniqueness_of :title, :scope => :wiki_id, :case_sensitive => false
41 42 validates_associated :content
42 43
43 44 def title=(value)
44 45 value = Wiki.titleize(value)
45 46 @previous_title = read_attribute(:title) if @previous_title.blank?
46 47 write_attribute(:title, value)
47 48 end
48 49
49 50 def before_save
50 51 self.title = Wiki.titleize(title)
51 52 # Manage redirects if the title has changed
52 53 if !@previous_title.blank? && (@previous_title != title) && !new_record?
53 54 # Update redirects that point to the old title
54 55 wiki.redirects.find_all_by_redirects_to(@previous_title).each do |r|
55 56 r.redirects_to = title
56 57 r.title == r.redirects_to ? r.destroy : r.save
57 58 end
58 59 # Remove redirects for the new title
59 60 wiki.redirects.find_all_by_title(title).each(&:destroy)
60 61 # Create a redirect to the new title
61 62 wiki.redirects << WikiRedirect.new(:title => @previous_title, :redirects_to => title) unless redirect_existing_links == "0"
62 63 @previous_title = nil
63 64 end
64 65 end
65 66
66 67 def before_destroy
67 68 # Remove redirects to this page
68 69 wiki.redirects.find_all_by_redirects_to(title).each(&:destroy)
69 70 end
70 71
71 72 def pretty_title
72 73 WikiPage.pretty_title(title)
73 74 end
74 75
75 76 def content_for_version(version=nil)
76 77 result = content.versions.find_by_version(version.to_i) if version
77 78 result ||= content
78 79 result
79 80 end
80 81
81 82 def diff(version_to=nil, version_from=nil)
82 83 version_to = version_to ? version_to.to_i : self.content.version
83 84 version_from = version_from ? version_from.to_i : version_to - 1
84 85 version_to, version_from = version_from, version_to unless version_from < version_to
85 86
86 87 content_to = content.versions.find_by_version(version_to)
87 88 content_from = content.versions.find_by_version(version_from)
88 89
89 90 (content_to && content_from) ? WikiDiff.new(content_to, content_from) : nil
90 91 end
91 92
92 93 def annotate(version=nil)
93 94 version = version ? version.to_i : self.content.version
94 95 c = content.versions.find_by_version(version)
95 96 c ? WikiAnnotate.new(c) : nil
96 97 end
97 98
98 99 def self.pretty_title(str)
99 100 (str && str.is_a?(String)) ? str.tr('_', ' ') : str
100 101 end
101 102
102 103 def project
103 104 wiki.project
104 105 end
105 106
106 107 def text
107 108 content.text if content
108 109 end
109 110
110 111 # Returns true if usr is allowed to edit the page, otherwise false
111 112 def editable_by?(usr)
112 113 !protected? || usr.allowed_to?(:protect_wiki_pages, wiki.project)
113 114 end
114 115
115 116 def attachments_deletable?(usr=User.current)
116 117 editable_by?(usr) && super(usr)
117 118 end
118 119
119 120 def parent_title
120 121 @parent_title || (self.parent && self.parent.pretty_title)
121 122 end
122 123
123 124 def parent_title=(t)
124 125 @parent_title = t
125 126 parent_page = t.blank? ? nil : self.wiki.find_page(t)
126 127 self.parent = parent_page
127 128 end
128 129
129 130 protected
130 131
131 132 def validate
132 133 errors.add(:parent_title, :invalid) if !@parent_title.blank? && parent.nil?
133 134 errors.add(:parent_title, :circular_dependency) if parent && (parent == self || parent.ancestors.include?(self))
134 135 errors.add(:parent_title, :not_same_project) if parent && (parent.wiki_id != wiki_id)
135 136 end
136 137 end
137 138
138 139 class WikiDiff
139 140 attr_reader :diff, :words, :content_to, :content_from
140 141
141 142 def initialize(content_to, content_from)
142 143 @content_to = content_to
143 144 @content_from = content_from
144 145 @words = content_to.text.split(/(\s+)/)
145 146 @words = @words.select {|word| word != ' '}
146 147 words_from = content_from.text.split(/(\s+)/)
147 148 words_from = words_from.select {|word| word != ' '}
148 149 @diff = words_from.diff @words
149 150 end
150 151 end
151 152
152 153 class WikiAnnotate
153 154 attr_reader :lines, :content
154 155
155 156 def initialize(content)
156 157 @content = content
157 158 current = content
158 159 current_lines = current.text.split(/\r?\n/)
159 160 @lines = current_lines.collect {|t| [nil, nil, t]}
160 161 positions = []
161 162 current_lines.size.times {|i| positions << i}
162 163 while (current.previous)
163 164 d = current.previous.text.split(/\r?\n/).diff(current.text.split(/\r?\n/)).diffs.flatten
164 165 d.each_slice(3) do |s|
165 166 sign, line = s[0], s[1]
166 167 if sign == '+' && positions[line] && positions[line] != -1
167 168 if @lines[positions[line]][0].nil?
168 169 @lines[positions[line]][0] = current.version
169 170 @lines[positions[line]][1] = current.author
170 171 end
171 172 end
172 173 end
173 174 d.each_slice(3) do |s|
174 175 sign, line = s[0], s[1]
175 176 if sign == '-'
176 177 positions.insert(line, -1)
177 178 else
178 179 positions[line] = nil
179 180 end
180 181 end
181 182 positions.compact!
182 183 # Stop if every line is annotated
183 184 break unless @lines.detect { |line| line[0].nil? }
184 185 current = current.previous
185 186 end
186 187 @lines.each { |line| line[0] ||= current.version }
187 188 end
188 189 end
@@ -1,60 +1,61
1 1 <div class="contextual">
2 2 <% if @editable %>
3 3 <%= link_to_if_authorized(l(:button_edit), {:action => 'edit', :page => @page.title}, :class => 'icon icon-edit', :accesskey => accesskey(:edit)) if @content.version == @page.content.version %>
4 <%= watcher_tag(@page, User.current) %>
4 5 <%= link_to_if_authorized(l(:button_lock), {:action => 'protect', :page => @page.title, :protected => 1}, :method => :post, :class => 'icon icon-lock') if !@page.protected? %>
5 6 <%= link_to_if_authorized(l(:button_unlock), {:action => 'protect', :page => @page.title, :protected => 0}, :method => :post, :class => 'icon icon-unlock') if @page.protected? %>
6 7 <%= link_to_if_authorized(l(:button_rename), {:action => 'rename', :page => @page.title}, :class => 'icon icon-move') if @content.version == @page.content.version %>
7 8 <%= link_to_if_authorized(l(:button_delete), {:action => 'destroy', :page => @page.title}, :method => :post, :confirm => l(:text_are_you_sure), :class => 'icon icon-del') %>
8 9 <%= link_to_if_authorized(l(:button_rollback), {:action => 'edit', :page => @page.title, :version => @content.version }, :class => 'icon icon-cancel') if @content.version < @page.content.version %>
9 10 <% end %>
10 11 <%= link_to_if_authorized(l(:label_history), {:action => 'history', :page => @page.title}, :class => 'icon icon-history') %>
11 12 </div>
12 13
13 14 <%= breadcrumb(@page.ancestors.reverse.collect {|parent| link_to h(parent.pretty_title), {:page => parent.title}}) %>
14 15
15 16 <% if @content.version != @page.content.version %>
16 17 <p>
17 18 <%= link_to(('&#171; ' + l(:label_previous)), :action => 'index', :page => @page.title, :version => (@content.version - 1)) + " - " if @content.version > 1 %>
18 19 <%= "#{l(:label_version)} #{@content.version}/#{@page.content.version}" %>
19 20 <%= '(' + link_to('diff', :controller => 'wiki', :action => 'diff', :page => @page.title, :version => @content.version) + ')' if @content.version > 1 %> -
20 21 <%= link_to((l(:label_next) + ' &#187;'), :action => 'index', :page => @page.title, :version => (@content.version + 1)) + " - " if @content.version < @page.content.version %>
21 22 <%= link_to(l(:label_current_version), :action => 'index', :page => @page.title) %>
22 23 <br />
23 24 <em><%= @content.author ? @content.author.name : "anonyme" %>, <%= format_time(@content.updated_on) %> </em><br />
24 25 <%=h @content.comments %>
25 26 </p>
26 27 <hr />
27 28 <% end %>
28 29
29 30 <%= render(:partial => "wiki/content", :locals => {:content => @content}) %>
30 31
31 32 <%= link_to_attachments @page %>
32 33
33 34 <% if @editable && authorize_for('wiki', 'add_attachment') %>
34 35 <div id="wiki_add_attachment">
35 36 <p><%= link_to l(:label_attachment_new), {}, :onclick => "Element.show('add_attachment_form'); Element.hide(this); Element.scrollTo('add_attachment_form'); return false;",
36 37 :id => 'attach_files_link' %></p>
37 38 <% form_tag({ :controller => 'wiki', :action => 'add_attachment', :page => @page.title }, :multipart => true, :id => "add_attachment_form", :style => "display:none;") do %>
38 39 <div class="box">
39 40 <p><%= render :partial => 'attachments/form' %></p>
40 41 </div>
41 42 <%= submit_tag l(:button_add) %>
42 43 <%= link_to l(:button_cancel), {}, :onclick => "Element.hide('add_attachment_form'); Element.show('attach_files_link'); return false;" %>
43 44 <% end %>
44 45 </div>
45 46 <% end %>
46 47
47 48 <% other_formats_links do |f| %>
48 49 <%= f.link_to 'HTML', :url => {:page => @page.title, :version => @content.version} %>
49 50 <%= f.link_to 'TXT', :url => {:page => @page.title, :version => @content.version} %>
50 51 <% end %>
51 52
52 53 <% content_for :header_tags do %>
53 54 <%= stylesheet_link_tag 'scm' %>
54 55 <% end %>
55 56
56 57 <% content_for :sidebar do %>
57 58 <%= render :partial => 'sidebar' %>
58 59 <% end %>
59 60
60 61 <% html_title @page.pretty_title %>
@@ -1,29 +1,33
1 <div class="contextual">
2 <%= watcher_tag(@wiki, User.current) %>
3 </div>
4
1 5 <h2><%= l(:label_index_by_date) %></h2>
2 6
3 7 <% if @pages.empty? %>
4 8 <p class="nodata"><%= l(:label_no_data) %></p>
5 9 <% end %>
6 10
7 11 <% @pages_by_date.keys.sort.reverse.each do |date| %>
8 12 <h3><%= format_date(date) %></h3>
9 13 <ul>
10 14 <% @pages_by_date[date].each do |page| %>
11 15 <li><%= link_to page.pretty_title, :action => 'index', :page => page.title %></li>
12 16 <% end %>
13 17 </ul>
14 18 <% end %>
15 19
16 20 <% content_for :sidebar do %>
17 21 <%= render :partial => 'sidebar' %>
18 22 <% end %>
19 23
20 24 <% unless @pages.empty? %>
21 25 <% other_formats_links do |f| %>
22 26 <%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :key => User.current.rss_key} %>
23 27 <%= f.link_to 'HTML', :url => {:action => 'special', :page => 'export'} %>
24 28 <% end %>
25 29 <% end %>
26 30
27 31 <% content_for :header_tags do %>
28 32 <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key) %>
29 33 <% end %>
@@ -1,22 +1,26
1 <div class="contextual">
2 <%= watcher_tag(@wiki, User.current) %>
3 </div>
4
1 5 <h2><%= l(:label_index_by_title) %></h2>
2 6
3 7 <% if @pages.empty? %>
4 8 <p class="nodata"><%= l(:label_no_data) %></p>
5 9 <% end %>
6 10
7 11 <%= render_page_hierarchy(@pages_by_parent_id) %>
8 12
9 13 <% content_for :sidebar do %>
10 14 <%= render :partial => 'sidebar' %>
11 15 <% end %>
12 16
13 17 <% unless @pages.empty? %>
14 18 <% other_formats_links do |f| %>
15 19 <%= f.link_to 'Atom', :url => {:controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :key => User.current.rss_key} %>
16 20 <%= f.link_to 'HTML', :url => {:action => 'special', :page => 'export'} %>
17 21 <% end %>
18 22 <% end %>
19 23
20 24 <% content_for :header_tags do %>
21 25 <%= auto_discovery_link_tag(:atom, :controller => 'projects', :action => 'activity', :id => @project, :show_wiki_pages => 1, :format => 'atom', :key => User.current.rss_key) %>
22 26 <% end %>
General Comments 0
You need to be logged in to leave comments. Login now