Commit dabe5caa authored by Eric Davis's avatar Eric Davis

Merge branch 'ticket/unstable/123-journalized' into unstable

parents bf90848a f79e209d
......@@ -17,8 +17,7 @@ class IssueMovesController < ApplicationController
moved_issues = []
@issues.each do |issue|
issue.reload
issue.init_journal(User.current)
issue.current_journal.notes = @notes if @notes.present?
issue.init_journal(User.current, @notes || "")
call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => extract_changed_attributes_for_move(params)})
moved_issues << r
......
......@@ -31,6 +31,9 @@ class IssuesController < ApplicationController
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :journals
include JournalsHelper
helper :projects
include ProjectsHelper
include CustomFieldsHelper
include IssueRelationsHelper
......@@ -92,8 +95,7 @@ class IssuesController < ApplicationController
end
def show
@journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
@journals.each_with_index {|j,i| j.indice = i+1}
@journals = @issue.journals.find(:all, :include => [:user], :order => "#{Journal.table_name}.created_at ASC")
@journals.reverse! if User.current.wants_comments_in_reverse_order?
@changesets = @issue.changesets.visible.all
@changesets.reverse! if User.current.wants_comments_in_reverse_order?
......@@ -144,6 +146,7 @@ class IssuesController < ApplicationController
end
def edit
return render_reply(@journal) if @journal
update_issue_from_params
@journal = @issue.current_journal
......@@ -159,7 +162,7 @@ class IssuesController < ApplicationController
JournalObserver.instance.send_notification = params[:send_notification] == '0' ? false : true
if @issue.save_issue_with_child_records(params, @time_entry)
render_attachment_warning_if_needed(@issue)
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal == @journal
respond_to do |format|
format.html { redirect_back_or_default({:action => 'show', :id => @issue}) }
......@@ -167,7 +170,7 @@ class IssuesController < ApplicationController
end
else
render_attachment_warning_if_needed(@issue)
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal.new_record?
flash[:notice] = l(:notice_successful_update) unless @issue.current_journal == @journal
@journal = @issue.current_journal
respond_to do |format|
......@@ -268,6 +271,7 @@ private
@notes = params[:notes] || (params[:issue].present? ? params[:issue][:notes] : nil)
@issue.init_journal(User.current, @notes)
@issue.safe_attributes = params[:issue]
@journal = @issue.current_journal
end
# TODO: Refactor, lots of extra code in here
......
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class JournalsController < ApplicationController
before_filter :find_journal, :only => [:edit, :diff]
before_filter :find_issue, :only => [:new]
before_filter :find_optional_project, :only => [:index]
before_filter :authorize, :only => [:new, :edit, :diff]
accept_key_auth :index
menu_item :issues
include QueriesHelper
include SortHelper
def index
retrieve_query
sort_init 'id', 'desc'
sort_update(@query.sortable_columns)
if @query.valid?
@journals = @query.journals(:order => "#{Journal.table_name}.created_on DESC",
:limit => 25)
end
@title = (@project ? @project.name : Setting.app_title) + ": " + (@query.new_record? ? l(:label_changes_details) : @query.name)
render :layout => false, :content_type => 'application/atom+xml'
rescue ActiveRecord::RecordNotFound
render_404
end
def diff
@issue = @journal.issue
if params[:detail_id].present?
@detail = @journal.details.find_by_id(params[:detail_id])
else
@detail = @journal.details.detect {|d| d.prop_key == 'description'}
end
(render_404; return false) unless @issue && @detail
@diff = Redmine::Helpers::Diff.new(@detail.value, @detail.old_value)
end
def new
journal = Journal.find(params[:journal_id]) if params[:journal_id]
if journal
user = journal.user
text = journal.notes
else
user = @issue.author
text = @issue.description
end
# Replaces pre blocks with [...]
text = text.to_s.strip.gsub(%r{<pre>((.|\s)*?)</pre>}m, '[...]')
content = "#{ll(Setting.default_language, :text_user_wrote, user)}\n> "
content << text.gsub(/(\r?\n|\r\n?)/, "\n> ") + "\n\n"
render(:update) { |page|
page.<< "$('notes').value = \"#{escape_javascript content}\";"
page.show 'update'
page << "Form.Element.focus('notes');"
page << "Element.scrollTo('update');"
page << "$('notes').scrollTop = $('notes').scrollHeight - $('notes').clientHeight;"
}
end
def edit
(render_403; return false) unless @journal.editable_by?(User.current)
if request.post?
@journal.update_attributes(:notes => params[:notes]) if params[:notes]
@journal.destroy if @journal.details.empty? && @journal.notes.blank?
call_hook(:controller_journals_edit_post, { :journal => @journal, :params => params})
respond_to do |format|
format.html { redirect_to :controller => 'issues', :action => 'show', :id => @journal.journalized_id }
format.js { render :action => 'update' }
end
else
respond_to do |format|
format.html {
# TODO: implement non-JS journal update
render :nothing => true
}
format.js
end
end
end
private
def find_journal
@journal = Journal.find(params[:id])
@project = @journal.journalized.project
rescue ActiveRecord::RecordNotFound
render_404
end
# TODO: duplicated in IssuesController
def find_issue
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
end
......@@ -97,7 +97,7 @@ class WikiController < ApplicationController
@content.comments = nil
# To prevent StaleObjectError exception when reverting to a previous version
@content.version = @page.content.version
@content.lock_version = @page.content.lock_version
end
verify :method => :put, :only => :update, :render => {:nothing => true, :status => :method_not_allowed }
......@@ -119,6 +119,7 @@ class WikiController < ApplicationController
redirect_to :action => 'show', :project_id => @project, :id => @page.title
return
end
params[:content].delete(:version) # The version count is automatically increased
@content.attributes = params[:content]
@content.author = User.current
# if page is new @page.save will also save content, but not if page isn't a new record
......@@ -160,7 +161,7 @@ class WikiController < ApplicationController
@version_pages = Paginator.new self, @version_count, per_page_option, params['p']
# don't load text
@versions = @page.content.versions.find :all,
:select => "id, author_id, comments, updated_on, version",
:select => "id, user_id, notes, created_at, version",
:order => 'version DESC',
:limit => @version_pages.items_per_page + 1,
:offset => @version_pages.current.offset
......
......@@ -19,28 +19,43 @@ require "digest/md5"
class Attachment < ActiveRecord::Base
belongs_to :container, :polymorphic => true
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
# FIXME: Remove these once the Versions, Documents and Projects themselves can provide file events
belongs_to :version, :foreign_key => "container_id"
belongs_to :document, :foreign_key => "container_id"
belongs_to :author, :class_name => "User", :foreign_key => "author_id"
validates_presence_of :container, :filename, :author
validates_length_of :filename, :maximum => 255
validates_length_of :disk_filename, :maximum => 255
acts_as_event :title => :filename,
:url => Proc.new {|o| {:controller => 'attachments', :action => 'download', :id => o.id, :filename => o.filename}}
acts_as_journalized :event_title => :filename,
:event_url => (Proc.new do |o|
{ :controller => 'attachments', :action => 'download',
:id => o.journaled_id, :filename => o.filename }
end),
:activity_type => 'files',
:activity_permission => :view_files,
:activity_find_options => { :include => { :version => :project } }
acts_as_activity_provider :type => 'files',
:permission => :view_files,
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Version.table_name} ON #{Attachment.table_name}.container_type='Version' AND #{Version.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Version.table_name}.project_id = #{Project.table_name}.id OR ( #{Attachment.table_name}.container_type='Project' AND #{Attachment.table_name}.container_id = #{Project.table_name}.id )"}
acts_as_activity_provider :type => 'documents',
:permission => :view_documents,
:author_key => :author_id,
:find_options => {:select => "#{Attachment.table_name}.*",
:joins => "LEFT JOIN #{Document.table_name} ON #{Attachment.table_name}.container_type='Document' AND #{Document.table_name}.id = #{Attachment.table_name}.container_id " +
"LEFT JOIN #{Project.table_name} ON #{Document.table_name}.project_id = #{Project.table_name}.id"}
acts_as_activity :type => 'documents', :permission => :view_documents,
:find_options => { :include => { :document => :project } }
# This method is called on save by the AttachmentJournal in order to
# decide which kind of activity we are dealing with. When that activity
# is retrieved later, we don't need to check the container_type in
# SQL anymore as that will be just the one we have specified here.
def activity_type
case container_type
when "Document"
"documents"
when "Version"
"files"
else
super
end
end
cattr_accessor :storage_path
@@storage_path = Redmine::Configuration['attachments_storage_path'] || "#{RAILS_ROOT}/files"
......
......@@ -23,19 +23,17 @@ class Changeset < ActiveRecord::Base
has_many :changes, :dependent => :delete_all
has_and_belongs_to_many :issues
acts_as_event :title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
:description => :long_comments,
:datetime => :committed_on,
:url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.identifier}}
acts_as_journalized :event_title => Proc.new {|o| "#{l(:label_revision)} #{o.format_identifier}" + (o.short_comments.blank? ? '' : (': ' + o.short_comments))},
:event_description => :long_comments,
:event_datetime => :committed_on,
:event_url => Proc.new {|o| {:controller => 'repositories', :action => 'revision', :id => o.repository.project, :rev => o.revision}},
:activity_timestamp => "#{table_name}.committed_on",
:activity_find_options => {:include => [:user, {:repository => :project}]}
acts_as_searchable :columns => 'comments',
:include => {:repository => :project},
:project_key => "#{Repository.table_name}.project_id",
:date_column => 'committed_on'
acts_as_activity_provider :timestamp => "#{table_name}.committed_on",
:author_key => :user_id,
:find_options => {:include => [:user, {:repository => :project}]}
validates_presence_of :repository_id, :revision, :committed_on, :commit_date
validates_uniqueness_of :revision, :scope => :repository_id
......@@ -193,7 +191,7 @@ class Changeset < ActiveRecord::Base
# don't change the status is the issue is closed
return if issue.status && issue.status.is_closed?
journal = issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
issue.init_journal(user || User.anonymous, ll(Setting.default_language, :text_status_changed_by_changeset, text_tag))
issue.status = status
unless Setting.commit_fix_done_ratio.blank?
issue.done_ratio = Setting.commit_fix_done_ratio.to_i
......
......@@ -20,11 +20,13 @@ class Document < ActiveRecord::Base
belongs_to :category, :class_name => "DocumentCategory", :foreign_key => "category_id"
acts_as_attachable :delete_permission => :manage_documents
acts_as_journalized :event_title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
:event_url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.journaled_id}},
:event_author => (Proc.new do |o|
o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC").try(:author)
end)
acts_as_searchable :columns => ['title', "#{table_name}.description"], :include => :project
acts_as_event :title => Proc.new {|o| "#{l(:label_document)}: #{o.title}"},
:author => Proc.new {|o| (a = o.attachments.find(:first, :order => "#{Attachment.table_name}.created_on ASC")) ? a.author : nil },
:url => Proc.new {|o| {:controller => 'documents', :action => 'show', :id => o.id}}
acts_as_activity_provider :find_options => {:include => :project}
validates_presence_of :project, :title, :category
validates_length_of :title, :maximum => 60
......
......@@ -27,7 +27,6 @@ class Issue < ActiveRecord::Base
belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
has_many :journals, :as => :journalized, :dependent => :destroy
has_many :time_entries, :dependent => :delete_all
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
......@@ -38,21 +37,25 @@ class Issue < ActiveRecord::Base
acts_as_attachable :after_remove => :attachment_removed
acts_as_customizable
acts_as_watchable
acts_as_journalized :event_title => Proc.new {|o| "#{o.tracker.name} ##{o.journaled_id} (#{o.status}): #{o.subject}"},
:event_type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '')}
register_on_journal_formatter(:id, 'parent_id')
register_on_journal_formatter(:named_association, 'project_id', 'status_id', 'tracker_id', 'assigned_to_id',
'priority_id', 'category_id', 'fixed_version_id')
register_on_journal_formatter(:fraction, 'estimated_hours')
register_on_journal_formatter(:decimal, 'done_ratio')
register_on_journal_formatter(:datetime, 'due_date', 'start_date')
register_on_journal_formatter(:plaintext, 'subject', 'description')
acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
:include => [:project, :journals],
# sort by id so that limited eager loading doesn't break with postgresql
:order_column => "#{table_name}.id"
acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
:type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
:author_key => :author_id
DONE_RATIO_OPTIONS = %w(issue_field issue_status)
attr_reader :current_journal
validates_presence_of :subject, :priority, :project, :tracker, :author, :status
validates_length_of :subject, :maximum => 255
......@@ -83,7 +86,7 @@ class Issue < ActiveRecord::Base
before_create :default_assign
before_save :close_duplicates, :update_done_ratio_from_issue_status
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes
after_destroy :update_parent_attributes
# Returns a SQL conditions string used to find all issues visible by the specified user
......@@ -346,15 +349,11 @@ class Issue < ActiveRecord::Base
end
end
def init_journal(user, notes = "")
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
@issue_before_change = self.clone
@issue_before_change.status = self.status
@custom_values_before_change = {}
self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
# Make sure updated_on is updated when adding a note.
updated_on_will_change!
@current_journal
# Callback on attachment deletion
def attachment_removed(obj)
init_journal(User.current)
create_journal
last_journal.update_attribute(:changes, {"attachments_" + obj.id.to_s => [obj.filename, nil]}.to_yaml)
end
# Return true if the issue is closed, otherwise false
......@@ -556,13 +555,12 @@ class Issue < ActiveRecord::Base
if valid?
attachments = Attachment.attach_files(self, params[:attachments])
attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
# TODO: Rename hook
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => current_journal})
begin
if save
# TODO: Rename hook
Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => current_journal})
else
raise ActiveRecord::Rollback
end
......@@ -777,22 +775,12 @@ class Issue < ActiveRecord::Base
).each do |issue|
next if issue.project.nil? || issue.fixed_version.nil?
unless issue.project.shared_versions.include?(issue.fixed_version)
issue.init_journal(User.current)
issue.fixed_version = nil
issue.save
end
end
end
# Callback on attachment deletion
def attachment_removed(obj)
journal = init_journal(User.current)
journal.details << JournalDetail.new(:property => 'attachment',
:prop_key => obj.id,
:old_value => obj.filename)
journal.save
end
# Default assignment based on category
def default_assign
if assigned_to.nil? && category && category.assigned_to
......@@ -817,40 +805,14 @@ class Issue < ActiveRecord::Base
duplicate.reload
# Don't re-close it if it's already closed
next if duplicate.closed?
# Same user and notes
if @current_journal
duplicate.init_journal(@current_journal.user, @current_journal.notes)
end
# Implicitely creates a new journal
duplicate.update_attribute :status, self.status
# Same user and notes
duplicate.journals.last.user = current_journal.user
duplicate.journals.last.notes = current_journal.notes
end
end
end
# Saves the changes in a Journal
# Called after_save
def create_journal
if @current_journal
# attributes changes
(Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
@current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c,
:old_value => @issue_before_change.send(c),
:value => send(c)) unless send(c)==@issue_before_change.send(c)
}
# custom fields changes
custom_values.each {|c|
next if (@custom_values_before_change[c.custom_field_id]==c.value ||
(@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
@current_journal.details << JournalDetail.new(:property => 'cf',
:prop_key => c.custom_field_id,
:old_value => @custom_values_before_change[c.custom_field_id],
:value => c.value)
}
@current_journal.save
# reset current journal
init_journal @current_journal.user, @current_journal.notes
end
end
# Query generator for selecting groups of issue counts for a project
# based on specific criteria
......@@ -880,4 +842,13 @@ class Issue < ActiveRecord::Base
end
IssueJournal.class_eval do
# Shortcut
def new_status
if details.keys.include? 'status_id'
(newval = details['status_id'].last) ? IssueStatus.find_by_id(newval.to_i) : nil
end
end
end
end
# Redmine - project management software
# Copyright (C) 2006-2011 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
class Journal < ActiveRecord::Base
belongs_to :journalized, :polymorphic => true
# added as a quick fix to allow eager loading of the polymorphic association
# since always associated to an issue, for now
belongs_to :issue, :foreign_key => :journalized_id
belongs_to :user
has_many :details, :class_name => "JournalDetail", :dependent => :delete_all
attr_accessor :indice
acts_as_event :title => Proc.new {|o| status = ((s = o.new_status) ? " (#{s})" : nil); "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}" },
:description => :notes,
:author => :user,
:type => Proc.new {|o| (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note' },
:url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}}
acts_as_activity_provider :type => 'issues',
:permission => :view_issues,
:author_key => :user_id,
:find_options => {:include => [{:issue => :project}, :details, :user],
:conditions => "#{Journal.table_name}.journalized_type = 'Issue' AND" +
" (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')"}
named_scope :visible, lambda {|*args| {
:include => {:issue => :project},
:conditions => Issue.visible_condition(args.first || User.current)
}}
def save(*args)
# Do not save an empty journal
(details.empty? && notes.blank?) ? false : super
end
# Returns the new status if the journal contains a status change, otherwise nil
def new_status
c = details.detect {|detail| detail.prop_key == 'status_id'}
(c && c.value) ? IssueStatus.find_by_id(c.value.to_i) : nil
end
def new_value_for(prop)
c = details.detect {|detail| detail.prop_key == prop}
c ? c.value : nil
end
def editable_by?(usr)
usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
end
def project
journalized.respond_to?(:project) ? journalized.project : nil
end
def attachments
journalized.respond_to?(:attachments) ? journalized.attachments : nil
end
# Returns a string of css classes
def css_classes
s = 'journal'
s << ' has-notes' unless notes.blank?
s << ' has-details' unless details.blank?
s
end
end
......@@ -159,21 +159,20 @@ class MailHandler < ActionMailer::Base
# ignore CLI-supplied defaults for new issues
@@handler_options[:issue].clear
journal = issue.init_journal(user)
issue.safe_attributes = issue_attributes_from_keywords(issue)
issue.safe_attributes = {'custom_field_values' => custom_field_values_from_keywords(issue)}
journal.notes = cleaned_up_text_body
issue.init_journal(user, cleaned_up_text_body)
add_attachments(issue)
issue.save!
logger.info "MailHandler: issue ##{issue.id} updated by #{user}" if logger && logger.info
journal
issue.last_journal
end
# Reply will be added to the issue
def receive_journal_reply(journal_id)
journal = Journal.find_by_id(journal_id)
if journal && journal.journalized_type == 'Issue'
receive_issue_reply(journal.journalized_id)
if journal and journal.journaled.is_a? Issue
receive_issue_reply(journal.journaled_id)
end
end
......
......@@ -21,6 +21,7 @@ class Mailer < ActionMailer::Base
layout 'mailer'
helper :application
helper :issues
helper :journals
helper :custom_fields
include ActionController::UrlWriter
......@@ -58,7 +59,7 @@ class Mailer < ActionMailer::Base
# issue_edit(journal) => tmail object
# Mailer.deliver_issue_edit(journal) => sends an email to issue recipients
def issue_edit(journal)
issue = journal.journalized.reload
issue = journal.journaled.reload
redmine_headers 'Project' => issue.project.identifier,
'Issue-Id' => issue.id,
'Issue-Author' => issue.author.login,
......@@ -71,7 +72,7 @@ class Mailer < ActionMailer::Base
# Watchers in cc
cc(issue.watcher_recipients - @recipients)
s = "[#{issue.project.name} - #{issue.tracker.name} ##{issue.id}] "
s << "(#{issue.status.name}) " if journal.new_value_for('status_id')
s << "(#{issue.status.name}) " if journal.details['status_id']
s << issue.subject
subject s
body :issue => issue,
......@@ -188,7 +189,7 @@ class Mailer < ActionMailer::Base
cc((message.root.watcher_recipients + message.board.watcher_recipients).uniq - @recipients)
subject "[#{message.board.project.name} - #{message.board.name} - msg#{message.root.id}] #{message.subject}"
body :message => message,
:message_url => url_for(message.event_url)
:message_url => url_for(message.last_journal.event_url)
render_multipart('message_posted', body)
end
......
......@@ -22,18 +22,24 @@ class Message < ActiveRecord::Base
acts_as_attachable
belongs_to :last_reply, :class_name => 'Message', :foreign_key => 'last_reply_id'
acts_as_journalized :event_title => Proc.new {|o| "#{o.board.name}: #{o.subject}"},
:event_description => :content,
:event_type => Proc.new {|o| o.parent_id.nil? ? 'message' : 'reply'},
:event_url => (Proc.new do |o|