Patch #44274 ยป 0001-journal-decoupling.patch
| 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 |