Project

General

Profile

Patch #44274 ยป 0001-journal-decoupling.patch

Florian Walchshofer, 2026-07-19 19:28

View differences:

app/controllers/journals_controller.rb
46 46
  end
47 47

  
48 48
  def diff
49
    @issue = @journal.issue
49
    @issue = @journal.journalized
50 50
    if params[:detail_id].present?
51 51
      @detail = @journal.details.find_by_id(params[:detail_id])
52 52
    else
......
101 101

  
102 102
  def find_journal
103 103
    @journal = Journal.visible.find(params[:id])
104
    @project = @journal.journalized.project
104
    @project = @journal.project
105 105
  rescue ActiveRecord::RecordNotFound
106 106
    render_404
107 107
  end
app/helpers/journals_helper.rb
30 30
  def render_journal_actions(issue, journal, options={})
31 31
    links = []
32 32
    dropbown_links = []
33
    indice = journal.indice || journal.issue.visible_journals_with_index.find{|j| j.id == journal.id}&.indice
33
    indice = journal.indice || journal.journalized.visible_journals_with_index.find{|j| j.id == journal.id}&.indice
34 34

  
35 35
    dropbown_links << copy_object_url_link(issue_url(issue, anchor: "note-#{indice}", only_path: false))
36 36
    if journal.attachments.size > 1
app/models/issue.rb
38 38
  belongs_to :priority, :class_name => 'IssuePriority'
39 39
  belongs_to :category, :class_name => 'IssueCategory'
40 40

  
41
  has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
42 41
  has_many :time_entries, :dependent => :destroy
43 42
  has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
44 43

  
......
48 47
  acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
49 48
  acts_as_customizable
50 49
  acts_as_watchable
50
  acts_as_journalized :activity_type => 'issues'
51 51
  acts_as_searchable :columns => ['subject', "#{table_name}.description"],
52 52
                     :preload => [:project, :status, :tracker],
53 53
                     :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
......
1304 1304
  def self.load_visible_last_updated_by(issues, user=User.current)
1305 1305
    if issues.any?
1306 1306
      issue_ids = issues.map(&:id)
1307
      journal_ids = Journal.joins(issue: :project).
1308
        where(:journalized_type => 'Issue', :journalized_id => issue_ids).
1309
        where(Journal.visible_notes_condition(user, :skip_pre_condition => true)).
1307
      journals = journal_visibility_scope(Journal.where(journalized_type: 'Issue', journalized_id: issue_ids))
1308
      journal_ids = journal_notes_visibility_scope(journals, user, skip_pre_condition: true).
1310 1309
        group(:journalized_id).
1311 1310
        maximum(:id).
1312 1311
        values
......
1323 1322
  def self.load_visible_last_notes(issues, user=User.current)
1324 1323
    if issues.any?
1325 1324
      issue_ids = issues.map(&:id)
1326
      journal_ids = Journal.joins(issue: :project).
1327
        where(:journalized_type => 'Issue', :journalized_id => issue_ids).
1328
        where(Journal.visible_notes_condition(user, :skip_pre_condition => true)).
1325
      journals = journal_visibility_scope(Journal.where(journalized_type: 'Issue', journalized_id: issue_ids))
1326
      journal_ids = journal_notes_visibility_scope(journals, user, skip_pre_condition: true).
1329 1327
        where.not(notes: '').
1330 1328
        group(:journalized_id).
1331 1329
        maximum(:id).
......
1721 1719
    end
1722 1720
  end
1723 1721

  
1722
  def self.journal_activity_preload
1723
    [{ journalized: :project }, { journalized: :tracker }, :user]
1724
  end
1725

  
1726
  def self.journal_activity_scope(scope)
1727
    scope.joins("LEFT OUTER JOIN #{JournalDetail.table_name} ON #{JournalDetail.table_name}.journal_id = #{Journal.table_name}.id").
1728
          where("#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> ''").distinct
1729
  end
1730

  
1731
  def journal_event_title(journal)
1732
    status = (s = journal_new_status(journal)) ? " (#{s})" : nil
1733
    "#{tracker} ##{id}#{status}: #{subject}"
1734
  end
1735

  
1736
  def journal_event_type(journal)
1737
    status = journal_new_status(journal)
1738
    if status
1739
      status.is_closed? ? 'issue-closed' : 'issue-edit'
1740
    else
1741
      'issue-note'
1742
    end
1743
  end
1744

  
1745
  def journal_event_url(journal)
1746
    {controller: 'issues', action: 'show', id: id, anchor: "change-#{journal.id}"}
1747
  end
1748

  
1749
  def self.journal_visibility_scope(scope)
1750
    scope.joins("INNER JOIN #{Issue.table_name} ON #{Issue.table_name}.id = #{Journal.table_name}.journalized_id").
1751
          joins("INNER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{Issue.table_name}.project_id")
1752
  end
1753

  
1754
  def self.journal_notes_visibility_scope(scope, user, options = {})
1755
    scope.where(Journal.visible_notes_condition(user, options))
1756
  end
1757

  
1758
  def journal_editable_by?(journal, usr)
1759
    usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (journal.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
1760
  end
1761

  
1762
  def journal_relation_visible?(detail, user)
1763
    Issue.find_by(id: detail.value || detail.old_value)&.visible?(user)
1764
  end
1765

  
1766
  def journal_notify(journal)
1767
    return unless journal.notify?
1768

  
1769
    if Setting.notified_events.include?('issue_updated') ||
1770
       (Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
1771
       (Setting.notified_events.include?('issue_status_updated') && journal_new_status(journal).present?) ||
1772
       (Setting.notified_events.include?('issue_assigned_to_updated') && journal.detail_for_attribute('assigned_to_id').present?) ||
1773
       (Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?) ||
1774
       (Setting.notified_events.include?('issue_fixed_version_updated') && journal.detail_for_attribute('fixed_version_id').present?) ||
1775
       (Setting.notified_events.include?('issue_attachment_added') &&
1776
         journal.details.any? {|d| d.property == 'attachment' && d.value })
1777

  
1778
      Mailer.deliver_issue_edit(journal)
1779
    end
1780
  end
1781

  
1782
  def journal_process_watchers(journal)
1783
    usr = journal.user
1784
    if usr.is_a?(User) &&
1785
       usr.pref.auto_watch_on?('issue_contributed_to') &&
1786
       journal.valid_watcher?(usr)
1787
      set_watcher(usr, true)
1788
    end
1789

  
1790
    assignee = assigned_to
1791
    if assignee.is_a?(User) &&
1792
       assignee.pref.auto_watch_on?('issue_assigned_to_me') &&
1793
       journal.valid_watcher?(assignee)
1794
      set_watcher(assignee, true)
1795
    end
1796
  end
1797

  
1798
  def self.journal_visible_scope(user = User.current, options = {})
1799
    joins(:project).where(visible_condition(user, options))
1800
  end
1801

  
1802
  def journal_user_can_view_private_notes?(user)
1803
    user.allowed_to?(:view_private_notes, project)
1804
  end
1805

  
1806
  def journal_private_notes_allowed?(user)
1807
    user.allowed_to?(:set_notes_private, project)
1808
  end
1809

  
1810
  def journal_valid_watcher?(user)
1811
    user.active? &&
1812
      user.allowed_to?(:add_issue_watchers, project) &&
1813
      valid_watcher?(user) &&
1814
      !watched_by?(user)
1815
  end
1816

  
1817
  # Returns the new status if the journal contains a status change, otherwise nil
1818
  def journal_new_status(journal)
1819
    s = journal.new_value_for('status_id')
1820
    s ? IssueStatus.find_by_id(s.to_i) : nil
1821
  end
1822

  
1724 1823
  private
1725 1824

  
1726 1825
  def user_tracker_permission?(user, permission)
app/models/issue_query.rb
477 477
  # Returns the journals
478 478
  # Valid options are :order, :offset, :limit
479 479
  def journals(options={})
480
    Journal.visible.
481
      joins(:issue => [:project, :status]).
480
    Issue.journal_visibility_scope(
481
      Journal.visible.where(journalized_type: 'Issue')).
482 482
      where(statement).
483 483
      order(options[:order]).
484 484
      limit(options[:limit]).
485 485
      offset(options[:offset]).
486
      preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
486
      preload(:details, :user, {:journalized => [:project, :author, :tracker, :status]}).
487 487
      to_a
488 488
  rescue ::ActiveRecord::StatementInvalid => e
489 489
    raise StatementInvalid.new(e.message)
app/models/journal.rb
21 21
  include Redmine::SafeAttributes
22 22
  include Redmine::Reaction::Reactable
23 23

  
24
  belongs_to :journalized, :polymorphic => true
25
  # added as a quick fix to allow eager loading of the polymorphic association
26
  # since always associated to an issue, for now
27
  belongs_to :issue, :foreign_key => :journalized_id
28

  
24
  belongs_to :journalized, :polymorphic => true, :inverse_of => :journals
29 25
  belongs_to :user
30 26
  belongs_to :updated_by, :class_name => 'User'
27

  
31 28
  has_many :details, :class_name => "JournalDetail", :dependent => :delete_all, :inverse_of => :journal
32 29
  attr_accessor :indice
33 30

  
31
  cattr_accessor :journalized_types
32

  
33
  def self.register_journalized_type(klass)
34
    self.journalized_types ||= []
35
    self.journalized_types << klass unless journalized_types.include?(klass)
36
  end
37

  
38
  def self.activity_scope_for(klass)
39
    scope = where(journalized_type: klass.name)
40
    scope = scope.preload(*klass.journal_activity_preload)
41
    klass.journal_activity_scope(scope)
42
  end
43

  
44
  def self.register_activity_provider(klass)
45
    return if klass.journal_activity_type.blank?
46

  
47
    acts_as_activity_provider(
48
      type: klass.journal_activity_type,
49
      author_key: :user_id,
50
      scope: proc { activity_scope_for(klass) }
51
    )
52
  end
53

  
34 54
  acts_as_event(
35
    :title =>
36
       Proc.new do |o|
37
         status = ((s = o.new_status) ? " (#{s})" : nil)
38
         "#{o.issue.tracker} ##{o.issue.id}#{status}: #{o.issue.subject}"
39
       end,
40
    :description => :notes,
41
    :author => :user,
42
    :group => :issue,
43
    :type =>
44
      Proc.new do |o|
45
        (s = o.new_status) ? (s.is_closed? ? 'issue-closed' : 'issue-edit') : 'issue-note'
46
      end,
47
    :url =>
48
      Proc.new do |o|
49
        {:controller => 'issues', :action => 'show', :id => o.issue.id, :anchor => "change-#{o.id}"}
50
      end
51
  )
52
  acts_as_activity_provider(
53
    :type => 'issues',
54
    :author_key => :user_id,
55
    :scope =>
56
      proc do
57
        preload({:issue => :project}, {:issue => :tracker}, :user).
58
          joins("LEFT OUTER JOIN #{JournalDetail.table_name} ON #{JournalDetail.table_name}.journal_id = #{Journal.table_name}.id").
59
            where("#{Journal.table_name}.journalized_type = 'Issue' AND" +
60
                  " (#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> '')").distinct
61
      end
55
    title: proc {|o|  o.journalized.journal_event_title(o) },
56
    description: :notes,
57
    author: :user,
58
    group: :journalized,
59
    type: proc {|o| o.journalized.journal_event_type(o) },
60
    url: proc {|o| o.journalized.journal_event_url(o) }
62 61
  )
63 62
  acts_as_mentionable :attributes => ['notes']
64 63
  before_create :split_private_notes
......
68 67
  scope :visible, (lambda do |*args|
69 68
    user = args.shift || User.current
70 69
    options = args.shift || {}
71

  
72
    joins(:issue => :project).
73
      where(Issue.visible_condition(user, options)).
74
      where(Journal.visible_notes_condition(user, :skip_pre_condition => true))
70
    visible_scope = none
71

  
72
    journalized_types.each do |klass|
73
      relation = where(journalized_type: klass.name)
74
      relation = klass.journal_visibility_scope(relation)
75
      relation = relation.where(journalized_id: klass.journal_visible_scope(user, options).select(:id))
76
      relation = klass.journal_notes_visibility_scope(relation, user, options)
77
      visible_scope = visible_scope.or(relation)
78
    end
79
    visible_scope
75 80
  end)
76 81

  
77 82
  safe_attributes(
......
79 84
    :if => lambda {|journal, user| journal.new_record? || journal.editable_by?(user)})
80 85
  safe_attributes(
81 86
    'private_notes',
82
    :if => lambda {|journal, user| user.allowed_to?(:set_notes_private, journal.project)})
87
    :if => lambda {|journal, user| journal.journalized.journal_private_notes_allowed?(user)})
83 88
  safe_attributes 'updated_by'
84 89

  
85 90
  # Returns a SQL condition to filter out journals with notes that are not visible to user
......
109 114
    notes.blank? && details.empty?
110 115
  end
111 116

  
112
  def journalized
113
    if journalized_type == 'Issue' && association(:issue).loaded?
114
      # Avoid extra query by using preloaded association
115
      issue
116
    else
117
      super
118
    end
119
  end
120

  
121 117
  # Returns journal details that are visible to user
122 118
  def visible_details(user=User.current)
123 119
    details.select do |detail|
124 120
      if detail.property == 'cf'
125 121
        detail.custom_field && detail.custom_field.visible_by?(project, user)
126 122
      elsif detail.property == 'relation'
127
        Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user)
123
        journalized.journal_relation_visible?(detail, user)
128 124
      else
129 125
        true
130 126
      end
......
137 133
    details.detect {|detail| detail.prop_key == attribute}
138 134
  end
139 135

  
140
  # Returns the new status if the journal contains a status change, otherwise nil
141 136
  def new_status
142
    s = new_value_for('status_id')
143
    s ? IssueStatus.find_by_id(s.to_i) : nil
137
    journalized.journal_new_status(self)
144 138
  end
145 139

  
146 140
  def new_value_for(prop)
......
148 142
  end
149 143

  
150 144
  def editable_by?(usr)
151
    usr && usr.logged? && (usr.allowed_to?(:edit_issue_notes, project) || (self.user == usr && usr.allowed_to?(:edit_own_issue_notes, project)))
145
    journalized.journal_editable_by?(self, usr)
152 146
  end
153 147

  
154 148
  def project
155
    journalized.respond_to?(:project) ? journalized.project : nil
149
    journalized.journal_project
156 150
  end
157 151

  
158 152
  def attachments
......
163 157
  end
164 158

  
165 159
  def visible?(*)
166
    journalized.visible?(*)
160
    journalized.journal_visible?(*)
167 161
  end
168 162

  
169 163
  def attachments_visible?
......
188 182
  end
189 183

  
190 184
  def notified_users
191
    notified = journalized.notified_users
185
    notified = journalized.journal_notified_users
192 186
    if private_notes?
193
      notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
187
      notified = select_journal_visible_user(notified)
194 188
    end
195 189
    notified
196 190
  end
......
200 194
  end
201 195

  
202 196
  def notified_watchers
203
    notified = journalized.notified_watchers
197
    notified = journalized.journal_notified_watchers
204 198
    select_journal_visible_user(notified)
205 199
  end
206 200

  
......
264 258
      )
265 259
  end
266 260

  
261
  def valid_watcher?(user)
262
    journalized.journal_valid_watcher?(user)
263
  end
264

  
267 265
  private
268 266

  
269 267
  # Generates journal details for attribute and custom field changes
......
354 352
  end
355 353

  
356 354
  def add_watcher
357
    if user.is_a?(User) &&
358
       user.pref.auto_watch_on?('issue_contributed_to') &&
359
       valid_watcher?(user)
360
      journalized.set_watcher(user, true)
361
    end
362

  
363
    assignee = journalized.assigned_to
364
    if assignee.is_a?(User) &&
365
       assignee.pref.auto_watch_on?('issue_assigned_to_me') &&
366
       valid_watcher?(assignee)
367
      journalized.set_watcher(assignee, true)
368
    end
369
  end
370

  
371
  def valid_watcher?(user)
372
    user.active? &&
373
      user.allowed_to?(:add_issue_watchers, journalized.project) &&
374
      journalized.valid_watcher?(user) &&
375
      !journalized.watched_by?(user)
355
    journalized.journal_process_watchers(self)
376 356
  end
377 357

  
378 358
  def send_notification
379
    if notify? &&
380
        (
381
          Setting.notified_events.include?('issue_updated') ||
382
          (Setting.notified_events.include?('issue_note_added') && notes.present?) ||
383
          (Setting.notified_events.include?('issue_status_updated') && new_status.present?) ||
384
          (Setting.notified_events.include?('issue_assigned_to_updated') && detail_for_attribute('assigned_to_id').present?) ||
385
          (Setting.notified_events.include?('issue_priority_updated') && new_value_for('priority_id').present?) ||
386
          (Setting.notified_events.include?('issue_fixed_version_updated') && detail_for_attribute('fixed_version_id').present?) ||
387
          (Setting.notified_events.include?('issue_attachment_added') && details.any? {|d| d.property == 'attachment' && d.value })
388
        )
389
      Mailer.deliver_issue_edit(self)
390
    end
359
    journalized.journal_notify(self)
391 360
  end
392 361

  
393 362
  def select_journal_visible_user(notified)
394 363
    if private_notes?
395
      notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
364
      notified = notified.select {|user| journalized.journal_user_can_view_private_notes?(user)}
396 365
    end
397 366
    notified
398 367
  end
app/views/journals/_notes_form.html.erb
18 18
    <p><%= submit_tag l(:button_save) %>
19 19
    <%= link_to l(:button_cancel), '#', :onclick => "$('#journal-#{@journal.id}-form').remove(); $('#journal-#{@journal.id}-notes').show(); return false;" %></p>
20 20
<% end %>
21
<%= wikitoolbar_for "journal_#{@journal.id}_notes", preview_issue_path(:project_id => @project, :issue_id => @journal.issue) %>
21
<%= wikitoolbar_for "journal_#{@journal.id}_notes", preview_issue_path(:project_id => @project, :issue_id => @journal.journalized) %>
app/views/journals/index.builder
10 10
  xml.updated((@journals.first ? @journals.first.event_datetime : Time.now).xmlschema)
11 11
  xml.author  {xml.name "#{Setting.app_title}"}
12 12
  @journals.each do |change|
13
    issue = change.issue
13
    journalized = change.journalized
14 14
    xml.entry do
15
      xml.title   "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}"
16
      xml.link    "rel" => "alternate", "href" => issue_url(issue)
17
      xml.id      issue_url(issue, :journal_id => change)
15
      xml.title   journalized.journal_event_title(change)
16
      xml.link    "rel" => "alternate", "href" => issue_url(journalized)
17
      xml.id      issue_url(journalized, :journal_id => change)
18 18
      xml.updated change.created_on.xmlschema
19 19
      xml.author do
20 20
        xml.name change.user.name
app/views/journals/update.js.erb
2 2
  $("#change-<%= @journal.id %>").remove();
3 3
<% else %>
4 4
  $("#change-<%= @journal.id %>").attr('class', '<%= @journal.css_classes %>');
5
  $("#change-<%= @journal.id %> .journal-actions").html('<%= escape_javascript(render_journal_actions(@journal.issue, @journal, :reply_links => authorize_for('issues', 'edit'))) %>');
5
  $("#change-<%= @journal.id %> .journal-actions").html('<%= escape_javascript(render_journal_actions(@journal.journalized, @journal, :reply_links => authorize_for('issues', 'edit'))) %>');
6 6
  $("#journal-<%= @journal.id %>-private_notes").replaceWith('<%= escape_javascript(render_private_notes_indicator(@journal)) %>');
7
  $("#journal-<%= @journal.id %>-notes").replaceWith('<%= escape_javascript(render_notes(@journal.issue, @journal, :reply_links => authorize_for('issues', 'edit'))) %>');
7
  $("#journal-<%= @journal.id %>-notes").replaceWith('<%= escape_javascript(render_notes(@journal.journalized, @journal, :reply_links => authorize_for('issues', 'edit'))) %>');
8 8
  $("#journal-<%= @journal.id %>-notes").show();
9 9
  $("#journal-<%= @journal.id %>-form").remove();
10 10
  var journal_header = $("#change-<%= @journal.id %>>div.note>h4.journal-header>.journal-info");
lib/plugins/acts_as_journalized/init.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
require_relative 'lib/acts_as_journalized'
21
Rails.application.reloader.to_prepare do
22
  ApplicationRecord.send(:include, Redmine::Acts::Journalized)
23
end
lib/plugins/acts_as_journalized/lib/acts_as_journalized.rb
1
# frozen_string_literal: true
2

  
3
# Redmine - project management software
4
# Copyright (C) 2006-  Jean-Philippe Lang
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of the GNU General Public License
8
# as published by the Free Software Foundation; either version 2
9
# of the License, or (at your option) any later version.
10
#
11
# This program is distributed in the hope that it will be useful,
12
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
# GNU General Public License for more details.
15
#
16
# You should have received a copy of the GNU General Public License
17
# along with this program; if not, write to the Free Software
18
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
19

  
20
module Redmine
21
  module Acts
22
    module Journalized
23
      def self.included(base)
24
        base.extend ClassMethods
25
      end
26

  
27
      module ClassMethods
28
        def acts_as_journalized(options = {})
29
          class_attribute :journal_activity_type
30

  
31
          has_many :journals, as: :journalized, dependent: :destroy, inverse_of: :journalized
32

  
33
          Journal.register_journalized_type(self)
34
          self.journal_activity_type = options[:activity_type]
35
          Journal.register_activity_provider(self) if journal_activity_type.present?
36

  
37
          include InstanceMethods
38
        end
39

  
40
        # Visibility
41
        # Returns journalized records visible for the given user.
42
        # Default implementation simply returns all records.
43
        def journal_visible_scope(user = User.current, options = {})
44
          all
45
        end
46

  
47
        def journal_notes_visibility_scope(scope, user, options = {})
48
          scope
49
        end
50

  
51
        def journal_visibility_scope(scope)
52
          scope
53
        end
54

  
55
        # scope contains journals of the current journalized class only
56
        def journal_activity_scope(scope)
57
          scope
58
        end
59

  
60
        def journal_activity_preload
61
          []
62
        end
63
      end
64

  
65
      module InstanceMethods
66
        # Event API
67
        def journal_event_title(journal)
68
          "#{self.class.model_name.human} ##{id}"
69
        end
70

  
71
        def journal_event_type(journal)
72
          'journal-update'
73
        end
74

  
75
        def journal_event_url(journal)
76
          {}
77
        end
78

  
79
        # Notification API
80
        def journal_notify(journal)
81
          # no-op
82
        end
83

  
84
        # Watcher API
85
        def journal_process_watchers(journal)
86
          # no-op
87
        end
88

  
89
        # Journal detail tracking API
90
        def journalized_attribute_names
91
          []
92
        end
93

  
94
        # Recipient API
95
        def journal_notified_users
96
          respond_to?(:notified_users) ? notified_users : []
97
        end
98

  
99
        def journal_notified_watchers
100
          respond_to?(:notified_watchers) ? notified_watchers : []
101
        end
102

  
103
        # Visibility API
104
        def journal_visible?(user = User.current)
105
          respond_to?(:visible?) ? visible?(user) : true
106
        end
107

  
108
        def journal_project
109
          respond_to?(:project) ? project : nil
110
        end
111

  
112
        def journal_editable_by?(journal, user)
113
          false
114
        end
115

  
116
        def journal_relation_visible?(detail, user)
117
          true
118
        end
119

  
120
        def journal_user_can_view_private_notes?(user)
121
          true
122
        end
123

  
124
        def journal_private_notes_allowed?(user)
125
          false
126
        end
127

  
128
        def journal_valid_watcher?(user)
129
          false
130
        end
131

  
132
        def journal_new_status(journal)
133
          nil
134
        end
135
      end
136
    end
137
  end
138
end
test/unit/changeset_test.rb
313 313
      end
314 314
      assert issue.reload.closed?
315 315
      journal = Journal.order(id: :desc).first
316
      assert_equal issue, journal.issue
316
      assert_equal issue, journal.journalized
317 317
      assert_include "Applied in changeset ecookbook:r12345.", journal.notes
318 318
    end
319 319
  end
test/unit/journal_test.rb
26 26
  end
27 27

  
28 28
  def test_journalized_is_an_issue
29
    issue = @journal.issue
29
    issue = @journal.journalized
30 30
    assert_kind_of Issue, issue
31 31
    assert_equal 1, issue.id
32 32
  end
......
181 181
    # Anonymous user should see issues of public projects only
182 182
    journals = Journal.visible(User.anonymous).to_a
183 183
    assert journals.any?
184
    assert_nil journals.detect {|journal| !journal.issue.project.is_public?}
184
    assert_nil journals.detect {|journal| !journal.project.is_public?}
185 185
    # Anonymous user should not see issues without permission
186 186
    Role.anonymous.remove_permission!(:view_issues)
187 187
    journals = Journal.visible(User.anonymous).to_a
......
194 194
    # Non member user should see issues of public projects only
195 195
    journals = Journal.visible(user).to_a
196 196
    assert journals.any?
197
    assert_nil journals.detect {|journal| !journal.issue.project.is_public?}
197
    assert_nil journals.detect {|journal| !journal.project.is_public?}
198 198
    # Non member user should not see issues without permission
199 199
    Role.non_member.remove_permission!(:view_issues)
200 200
    user.reload
......
205 205
    user.reload
206 206
    journals = Journal.visible(user).to_a
207 207
    assert journals.any?
208
    assert_nil journals.detect {|journal| journal.issue.project_id != 1}
208
    assert_nil journals.detect {|journal| journal.journalized.project_id != 1}
209 209
  end
210 210

  
211 211
  def test_visible_scope_for_admin
......
215 215
    journals = Journal.visible(user).to_a
216 216
    assert journals.any?
217 217
    # Admin should see issues on private projects that admin does not belong to
218
    assert journals.detect {|journal| !journal.issue.project.is_public?}
218
    assert journals.detect {|journal| !journal.project.is_public?}
219 219
  end
220 220

  
221 221
  def test_preload_journals_details_custom_fields_should_set_custom_field_instance_variable
......
268 268
    visible_issue = Issue.generate!
269 269
    hidden_issue = Issue.generate!(:is_private => true)
270 270

  
271
    journal = Journal.new
271
    journal = Journal.new(journalized: issue)
272 272
    journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'relates', :value => visible_issue.id)
273 273
    journal.details << JournalDetail.new(:property => 'relation', :prop_key => 'relates', :value => hidden_issue.id)
274 274

  
test/unit/mail_handler_test.rb
944 944
    assert_equal Issue.find(2), journal.journalized
945 945
    assert_match /This is reply/, journal.notes
946 946
    assert_equal false, journal.private_notes
947
    assert_equal 'Feature request', journal.issue.tracker.name
947
    assert_equal 'Feature request', journal.journalized.tracker.name
948 948
  end
949 949

  
950 950
  def test_update_issue_should_accept_issue_id_after_space_inside_brackets
......
977 977
                                               'start_date', 'due_date',
978 978
                                               'float field'])
979 979
    assert journal.is_a?(Journal)
980
    issue = Issue.find(journal.issue.id)
980
    issue = Issue.find(journal.journalized.id)
981 981
    assert_equal User.find_by_login('jsmith'), journal.user
982 982
    assert_equal Issue.find(2), journal.journalized
983 983
    assert_match /This is reply/, journal.notes
984
    assert_equal 'Feature request', journal.issue.tracker.name
984
    assert_equal 'Feature request', journal.journalized.tracker.name
985 985
    assert_equal IssueStatus.find_by_name("Resolved"), issue.status
986 986
    assert_equal '2010-01-01', issue.start_date.to_s
987 987
    assert_equal '2010-12-31', issue.due_date.to_s
......
1042 1042
      )
1043 1043
    assert journal.is_a?(Journal)
1044 1044
    assert_match /This is reply/, journal.notes
1045
    assert_equal 'Feature request', journal.issue.tracker.name
1046
    assert_equal 'Normal', journal.issue.priority.name
1045
    assert_equal 'Feature request', journal.journalized.tracker.name
1046
    assert_equal 'Normal', journal.journalized.priority.name
1047 1047
  end
1048 1048

  
1049 1049
  def test_update_issue_should_add_cc_as_watchers
test/unit/mailer_test.rb
362 362

  
363 363
  def test_issue_edit_message_id
364 364
    journal = Journal.find(3)
365
    journal.issue = Issue.find(2)
365
    journal.journalized = Issue.find(2)
366 366

  
367 367
    Mailer.deliver_issue_edit(journal)
368 368
    mail = last_email
    (1-1/1)