Patch #44274 » 0002-journal-decoupling-revised.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 |
| ... | ... | |
| 100 | 100 |
private |
| 101 | 101 | |
| 102 | 102 |
def find_journal |
| 103 |
@journal = Journal.visible.find(params[:id]) |
|
| 104 |
@project = @journal.journalized.project |
|
| 103 |
# Only issue journals are handled by this controller for now |
|
| 104 |
@journal = Journal.where(:journalized_type => 'Issue').visible.find(params[:id]) |
|
| 105 |
@project = @journal.project |
|
| 105 | 106 |
rescue ActiveRecord::RecordNotFound |
| 106 | 107 |
render_404 |
| 107 | 108 |
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 |
:permissions => {
|
|
| 52 |
:edit_notes => :edit_issue_notes, |
|
| 53 |
:edit_own_notes => :edit_own_issue_notes, |
|
| 54 |
:add_watchers => :add_issue_watchers |
|
| 55 |
} |
|
| 51 | 56 |
acts_as_searchable :columns => ['subject', "#{table_name}.description"],
|
| 52 | 57 |
:preload => [:project, :status, :tracker], |
| 53 | 58 |
:scope => lambda {|options| options[:open_issues] ? self.open : self.all}
|
| ... | ... | |
| 1304 | 1309 |
def self.load_visible_last_updated_by(issues, user=User.current) |
| 1305 | 1310 |
if issues.any? |
| 1306 | 1311 |
issue_ids = issues.map(&:id) |
| 1307 |
journal_ids = Journal.joins(issue: :project).
|
|
| 1308 |
where(:journalized_type => 'Issue', :journalized_id => issue_ids).
|
|
| 1312 |
journal_ids = Journal.joins_journalized(Issue).
|
|
| 1313 |
where(:journalized_id => issue_ids). |
|
| 1309 | 1314 |
where(Journal.visible_notes_condition(user, :skip_pre_condition => true)). |
| 1310 | 1315 |
group(:journalized_id). |
| 1311 | 1316 |
maximum(:id). |
| ... | ... | |
| 1323 | 1328 |
def self.load_visible_last_notes(issues, user=User.current) |
| 1324 | 1329 |
if issues.any? |
| 1325 | 1330 |
issue_ids = issues.map(&:id) |
| 1326 |
journal_ids = Journal.joins(issue: :project).
|
|
| 1327 |
where(:journalized_type => 'Issue', :journalized_id => issue_ids).
|
|
| 1331 |
journal_ids = Journal.joins_journalized(Issue).
|
|
| 1332 |
where(:journalized_id => issue_ids). |
|
| 1328 | 1333 |
where(Journal.visible_notes_condition(user, :skip_pre_condition => true)). |
| 1329 | 1334 |
where.not(notes: ''). |
| 1330 | 1335 |
group(:journalized_id). |
| ... | ... | |
| 1721 | 1726 |
end |
| 1722 | 1727 |
end |
| 1723 | 1728 | |
| 1729 |
def self.journal_visible_join_condition(user=User.current, options={})
|
|
| 1730 |
"(#{visible_condition(user, options)}) AND #{Journal.visible_notes_condition(user, :skip_pre_condition => true)}"
|
|
| 1731 |
end |
|
| 1732 | ||
| 1733 |
def self.journal_activity_preload |
|
| 1734 |
[{:journalized => [:project, :tracker]}, :user]
|
|
| 1735 |
end |
|
| 1736 | ||
| 1737 |
def self.journal_activity_scope(scope) |
|
| 1738 |
scope.joins("LEFT OUTER JOIN #{JournalDetail.table_name} ON #{JournalDetail.table_name}.journal_id = #{Journal.table_name}.id").
|
|
| 1739 |
where("#{JournalDetail.table_name}.prop_key = 'status_id' OR #{Journal.table_name}.notes <> ''").distinct
|
|
| 1740 |
end |
|
| 1741 | ||
| 1742 |
def journal_event_title(journal) |
|
| 1743 |
status = (s = journal_new_status(journal)) ? " (#{s})" : nil
|
|
| 1744 |
"#{tracker} ##{id}#{status}: #{subject}"
|
|
| 1745 |
end |
|
| 1746 | ||
| 1747 |
def journal_event_type(journal) |
|
| 1748 |
if (status = journal_new_status(journal)) |
|
| 1749 |
status.is_closed? ? 'issue-closed' : 'issue-edit' |
|
| 1750 |
else |
|
| 1751 |
'issue-note' |
|
| 1752 |
end |
|
| 1753 |
end |
|
| 1754 | ||
| 1755 |
def journal_relation_visible?(detail, user) |
|
| 1756 |
Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user) |
|
| 1757 |
end |
|
| 1758 | ||
| 1759 |
def journal_new_status(journal) |
|
| 1760 |
s = journal.new_value_for('status_id')
|
|
| 1761 |
s ? IssueStatus.find_by_id(s.to_i) : nil |
|
| 1762 |
end |
|
| 1763 | ||
| 1764 |
def journal_notify(journal) |
|
| 1765 |
if Setting.notified_events.include?('issue_updated') ||
|
|
| 1766 |
(Setting.notified_events.include?('issue_note_added') && journal.notes.present?) ||
|
|
| 1767 |
(Setting.notified_events.include?('issue_status_updated') && journal_new_status(journal).present?) ||
|
|
| 1768 |
(Setting.notified_events.include?('issue_assigned_to_updated') && journal.detail_for_attribute('assigned_to_id').present?) ||
|
|
| 1769 |
(Setting.notified_events.include?('issue_priority_updated') && journal.new_value_for('priority_id').present?) ||
|
|
| 1770 |
(Setting.notified_events.include?('issue_fixed_version_updated') && journal.detail_for_attribute('fixed_version_id').present?) ||
|
|
| 1771 |
(Setting.notified_events.include?('issue_attachment_added') && journal.details.any? {|d| d.property == 'attachment' && d.value })
|
|
| 1772 |
Mailer.deliver_issue_edit(journal) |
|
| 1773 |
end |
|
| 1774 |
end |
|
| 1775 | ||
| 1776 |
def journal_process_watchers(journal) |
|
| 1777 |
user = journal.user |
|
| 1778 |
if user.is_a?(User) && |
|
| 1779 |
user.pref.auto_watch_on?('issue_contributed_to') &&
|
|
| 1780 |
journal_valid_watcher?(user) |
|
| 1781 |
set_watcher(user, true) |
|
| 1782 |
end |
|
| 1783 | ||
| 1784 |
if assigned_to.is_a?(User) && |
|
| 1785 |
assigned_to.pref.auto_watch_on?('issue_assigned_to_me') &&
|
|
| 1786 |
journal_valid_watcher?(assigned_to) |
|
| 1787 |
set_watcher(assigned_to, true) |
|
| 1788 |
end |
|
| 1789 |
end |
|
| 1790 | ||
| 1724 | 1791 |
private |
| 1725 | 1792 | |
| 1726 | 1793 |
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 |
Journal.joins_journalized(Issue). |
|
| 481 |
visible. |
|
| 482 |
joins("INNER JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{Issue.table_name}.status_id").
|
|
| 482 | 483 |
where(statement). |
| 483 | 484 |
order(options[:order]). |
| 484 | 485 |
limit(options[:limit]). |
| 485 | 486 |
offset(options[:offset]). |
| 486 |
preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
|
|
| 487 |
preload(:details, :user, {:journalized => [:project, :author, :tracker, :status]}).
|
|
| 487 | 488 |
to_a |
| 488 | 489 |
rescue ::ActiveRecord::StatementInvalid => e |
| 489 | 490 |
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, lambda {order(:id)}, :class_name => "JournalDetail", :dependent => :delete_all, :inverse_of => :journal
|
| 32 | 29 |
attr_accessor :indice |
| 33 | 30 | |
| 31 |
# Returns journals of klass joined with the table of klass (and its project) |
|
| 32 |
def self.joins_journalized(klass) |
|
| 33 |
where(:journalized_type => klass.polymorphic_name).joins(*klass.journal_joins) |
|
| 34 |
end |
|
| 35 | ||
| 36 |
def self.activity_scope_for(klass) |
|
| 37 |
klass.journal_activity_scope(joins_journalized(klass).preload(*klass.journal_activity_preload)) |
|
| 38 |
end |
|
| 39 | ||
| 40 |
def self.register_activity_provider(klass) |
|
| 41 |
# Resolve the class at runtime so that the scope survives code reloading |
|
| 42 |
class_name = klass.name |
|
| 43 |
acts_as_activity_provider( |
|
| 44 |
:type => klass.journalized_options[:activity_type], |
|
| 45 |
:author_key => :user_id, |
|
| 46 |
:scope => proc { activity_scope_for(class_name.constantize) }
|
|
| 47 |
) |
|
| 48 |
end |
|
| 49 | ||
| 34 | 50 |
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 |
|
| 51 |
title: proc {|o| o.journalized.journal_event_title(o) },
|
|
| 52 |
description: :notes, |
|
| 53 |
author: :user, |
|
| 54 |
group: :journalized, |
|
| 55 |
type: proc {|o| o.journalized.journal_event_type(o) },
|
|
| 56 |
url: proc {|o| o.journalized.journal_event_url(o) }
|
|
| 62 | 57 |
) |
| 63 | 58 |
acts_as_mentionable :attributes => ['notes'] |
| 64 | 59 |
before_create :split_private_notes |
| ... | ... | |
| 69 | 64 |
user = args.shift || User.current |
| 70 | 65 |
options = args.shift || {}
|
| 71 | 66 | |
| 72 |
joins(:issue => :project). |
|
| 73 |
where(Issue.visible_condition(user, options)). |
|
| 74 |
where(Journal.visible_notes_condition(user, :skip_pre_condition => true)) |
|
| 67 |
classes = Redmine::Acts::Journalized.journalized_classes |
|
| 68 |
# Only consider the types the current scope is restricted to (eg. activity) |
|
| 69 |
if (types = Array(where_values_hash['journalized_type'])).any? |
|
| 70 |
classes = classes.select {|klass| types.include?(klass.polymorphic_name)}
|
|
| 71 |
end |
|
| 72 | ||
| 73 |
if classes.size == 1 && (condition = classes.first.journal_visible_join_condition(user, options)) |
|
| 74 |
# Joining the journalized table lets the database use the same query |
|
| 75 |
# plans as before journals were decoupled from issues |
|
| 76 |
joins_journalized(classes.first).where(condition) |
|
| 77 |
elsif classes.any? |
|
| 78 |
where(classes.map {|klass| "(#{klass.journal_visible_condition(user, options)})"}.join(' OR '))
|
|
| 79 |
else |
|
| 80 |
none |
|
| 81 |
end |
|
| 75 | 82 |
end) |
| 76 | 83 | |
| 77 | 84 |
safe_attributes( |
| ... | ... | |
| 79 | 86 |
:if => lambda {|journal, user| journal.new_record? || journal.editable_by?(user)})
|
| 80 | 87 |
safe_attributes( |
| 81 | 88 |
'private_notes', |
| 82 |
:if => lambda {|journal, user| user.allowed_to?(:set_notes_private, journal.project)})
|
|
| 89 |
:if => lambda {|journal, user| journal.journalized&.journal_private_notes_allowed?(user)})
|
|
| 83 | 90 |
safe_attributes 'updated_by' |
| 84 | 91 | |
| 85 | 92 |
# Returns a SQL condition to filter out journals with notes that are not visible to user |
| ... | ... | |
| 109 | 116 |
notes.blank? && details.empty? |
| 110 | 117 |
end |
| 111 | 118 | |
| 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 |
# @deprecated Use #journalized instead |
|
| 120 |
def issue |
|
| 121 |
Rails.application.deprecators[:redmine].warn "Journal#issue is deprecated and will be removed. Use Journal#journalized instead." |
|
| 122 |
journalized if journalized_type == 'Issue' |
|
| 119 | 123 |
end |
| 120 | 124 | |
| 121 | 125 |
# Returns journal details that are visible to user |
| ... | ... | |
| 124 | 128 |
if detail.property == 'cf' |
| 125 | 129 |
detail.custom_field && detail.custom_field.visible_by?(project, user) |
| 126 | 130 |
elsif detail.property == 'relation' |
| 127 |
Issue.find_by_id(detail.value || detail.old_value).try(:visible?, user)
|
|
| 131 |
journalized&.journal_relation_visible?(detail, user)
|
|
| 128 | 132 |
else |
| 129 | 133 |
true |
| 130 | 134 |
end |
| ... | ... | |
| 139 | 143 | |
| 140 | 144 |
# Returns the new status if the journal contains a status change, otherwise nil |
| 141 | 145 |
def new_status |
| 142 |
s = new_value_for('status_id')
|
|
| 143 |
s ? IssueStatus.find_by_id(s.to_i) : nil |
|
| 146 |
journalized&.journal_new_status(self) |
|
| 144 | 147 |
end |
| 145 | 148 | |
| 146 | 149 |
def new_value_for(prop) |
| ... | ... | |
| 148 | 151 |
end |
| 149 | 152 | |
| 150 | 153 |
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)))
|
|
| 154 |
journalized.present? && journalized.journal_editable_by?(self, usr)
|
|
| 152 | 155 |
end |
| 153 | 156 | |
| 154 | 157 |
def project |
| 155 |
journalized.respond_to?(:project) ? journalized.project : nil
|
|
| 158 |
journalized&.journal_project
|
|
| 156 | 159 |
end |
| 157 | 160 | |
| 158 | 161 |
def attachments |
| ... | ... | |
| 163 | 166 |
end |
| 164 | 167 | |
| 165 | 168 |
def visible?(*) |
| 166 |
journalized.visible?(*) |
|
| 169 |
journalized.present? && journalized.journal_visible?(*)
|
|
| 167 | 170 |
end |
| 168 | 171 | |
| 169 | 172 |
def attachments_visible? |
| ... | ... | |
| 188 | 191 |
end |
| 189 | 192 | |
| 190 | 193 |
def notified_users |
| 191 |
notified = journalized.notified_users |
|
| 192 |
if private_notes? |
|
| 193 |
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
|
| 194 |
end |
|
| 195 |
notified |
|
| 194 |
notified = journalized.journal_notified_users |
|
| 195 |
select_journal_visible_user(notified) |
|
| 196 | 196 |
end |
| 197 | 197 | |
| 198 | 198 |
def recipients |
| ... | ... | |
| 200 | 200 |
end |
| 201 | 201 | |
| 202 | 202 |
def notified_watchers |
| 203 |
notified = journalized.notified_watchers |
|
| 203 |
notified = journalized.journal_notified_watchers
|
|
| 204 | 204 |
select_journal_visible_user(notified) |
| 205 | 205 |
end |
| 206 | 206 | |
| ... | ... | |
| 235 | 235 |
@attributes_before_change = journalized.journalized_attribute_names.index_with do |attribute| |
| 236 | 236 |
journalized.send(attribute) |
| 237 | 237 |
end |
| 238 |
@custom_values_before_change = journalized.custom_field_values.to_h do |c| |
|
| 239 |
[c.custom_field_id, c.value] |
|
| 238 |
if journalized.respond_to?(:custom_field_values) |
|
| 239 |
@custom_values_before_change = journalized.custom_field_values.to_h do |c| |
|
| 240 |
[c.custom_field_id, c.value] |
|
| 241 |
end |
|
| 240 | 242 |
end |
| 241 | 243 |
end |
| 242 | 244 |
self |
| ... | ... | |
| 354 | 356 |
end |
| 355 | 357 | |
| 356 | 358 |
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) |
|
| 359 |
journalized.journal_process_watchers(self) |
|
| 376 | 360 |
end |
| 377 | 361 | |
| 378 | 362 |
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 |
|
| 363 |
journalized.journal_notify(self) if notify? |
|
| 391 | 364 |
end |
| 392 | 365 | |
| 393 | 366 |
def select_journal_visible_user(notified) |
| 394 | 367 |
if private_notes? |
| 395 |
notified = notified.select {|user| user.allowed_to?(:view_private_notes, journalized.project)}
|
|
| 368 |
notified = notified.select {|user| journalized.journal_user_can_view_private_notes?(user)}
|
|
| 396 | 369 |
end |
| 397 | 370 |
notified |
| 398 | 371 |
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 |
issue = change.journalized
|
|
| 14 | 14 |
xml.entry do |
| 15 | 15 |
xml.title "#{issue.project.name} - #{issue.tracker.name} ##{issue.id}: #{issue.subject}"
|
| 16 | 16 |
xml.link "rel" => "alternate", "href" => issue_url(issue) |
| 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 |
# Makes a model journalized, i.e. its changes and notes are recorded |
|
| 23 |
# as Journal records. |
|
| 24 |
# |
|
| 25 |
# acts_as_journalized( |
|
| 26 |
# :activity_type => 'issues', |
|
| 27 |
# :attributes => %w(subject description), |
|
| 28 |
# :permissions => {
|
|
| 29 |
# :edit_notes => :edit_issue_notes, |
|
| 30 |
# :edit_own_notes => :edit_own_issue_notes, |
|
| 31 |
# :add_watchers => :add_issue_watchers |
|
| 32 |
# } |
|
| 33 |
# ) |
|
| 34 |
# |
|
| 35 |
# Options: |
|
| 36 |
# * :activity_type - registers the journals of the model as activity |
|
| 37 |
# provider of the given event type (optional) |
|
| 38 |
# * :attributes - names of the attributes to journalize, unless the model |
|
| 39 |
# defines #journalized_attribute_names |
|
| 40 |
# * :permissions - permissions used by the default implementations, see |
|
| 41 |
# DEFAULT_PERMISSIONS. A nil permission is never granted. |
|
| 42 |
# |
|
| 43 |
# The defaults assume that the model has a visible scope and a visible? |
|
| 44 |
# method, and optionally a project association. Every journal_* method |
|
| 45 |
# can be overridden by the model for specific behavior. |
|
| 46 |
module Journalized |
|
| 47 |
DEFAULT_PERMISSIONS = {
|
|
| 48 |
:edit_notes => nil, |
|
| 49 |
:edit_own_notes => nil, |
|
| 50 |
:view_private_notes => :view_private_notes, |
|
| 51 |
:set_notes_private => :set_notes_private, |
|
| 52 |
:add_watchers => nil |
|
| 53 |
}.freeze |
|
| 54 | ||
| 55 |
@journalized_type_names = [] |
|
| 56 | ||
| 57 |
class << self |
|
| 58 |
# Registers a journalized type by its name, so that journals of this |
|
| 59 |
# type are taken into account even if the class is not loaded yet |
|
| 60 |
def register(class_name) |
|
| 61 |
name = class_name.to_s |
|
| 62 |
@journalized_type_names << name unless @journalized_type_names.include?(name) |
|
| 63 |
end |
|
| 64 | ||
| 65 |
def unregister(class_name) |
|
| 66 |
@journalized_type_names.delete(class_name.to_s) |
|
| 67 |
end |
|
| 68 | ||
| 69 |
def journalized_type_names |
|
| 70 |
@journalized_type_names.dup |
|
| 71 |
end |
|
| 72 | ||
| 73 |
# Returns the classes of the registered journalized types |
|
| 74 |
def journalized_classes |
|
| 75 |
@journalized_type_names.filter_map do |name| |
|
| 76 |
klass = name.safe_constantize |
|
| 77 |
klass if klass.respond_to?(:journalized_options) |
|
| 78 |
end |
|
| 79 |
end |
|
| 80 |
end |
|
| 81 | ||
| 82 |
def self.included(base) |
|
| 83 |
base.extend ClassMethods |
|
| 84 |
end |
|
| 85 | ||
| 86 |
module ClassMethods |
|
| 87 |
def acts_as_journalized(options = {})
|
|
| 88 |
options.assert_valid_keys(:activity_type, :attributes, :permissions) |
|
| 89 |
permissions = options[:permissions] || {}
|
|
| 90 |
permissions.assert_valid_keys(*DEFAULT_PERMISSIONS.keys) |
|
| 91 | ||
| 92 |
class_attribute :journalized_options, :instance_writer => false |
|
| 93 |
self.journalized_options = {
|
|
| 94 |
:activity_type => options[:activity_type]&.to_s, |
|
| 95 |
:attributes => Array(options[:attributes]).map(&:to_s), |
|
| 96 |
:permissions => DEFAULT_PERMISSIONS.merge(permissions) |
|
| 97 |
} |
|
| 98 | ||
| 99 |
has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized |
|
| 100 | ||
| 101 |
extend SingletonMethods |
|
| 102 |
include InstanceMethods |
|
| 103 | ||
| 104 |
Redmine::Acts::Journalized.register(polymorphic_name) |
|
| 105 |
Journal.register_activity_provider(self) if journalized_options[:activity_type] |
|
| 106 |
end |
|
| 107 |
end |
|
| 108 | ||
| 109 |
module SingletonMethods |
|
| 110 |
# Returns a SQL condition on the tables joined by journal_joins that |
|
| 111 |
# restricts journals to the ones visible to user, including private |
|
| 112 |
# notes, or nil if the model does not provide one. |
|
| 113 |
# |
|
| 114 |
# Journal.visible uses it when only the journals of this type are |
|
| 115 |
# queried. Joins usually perform better than the EXISTS subquery of |
|
| 116 |
# journal_visible_condition, which is used otherwise. |
|
| 117 |
def journal_visible_join_condition(user=User.current, options={})
|
|
| 118 |
nil |
|
| 119 |
end |
|
| 120 | ||
| 121 |
# Returns a SQL condition that restricts journals to the journals |
|
| 122 |
# of this type that are visible to user, including private notes |
|
| 123 |
def journal_visible_condition(user=User.current, options={})
|
|
| 124 |
return '1=0' unless respond_to?(:visible) |
|
| 125 | ||
| 126 |
journals = Journal.table_name |
|
| 127 |
records = visible(user, options). |
|
| 128 |
where("#{table_name}.#{primary_key} = #{journals}.journalized_id").
|
|
| 129 |
where(journal_private_notes_condition(user, options)) |
|
| 130 |
"#{journals}.journalized_type = #{connection.quote(polymorphic_name)}" \
|
|
| 131 |
" AND EXISTS (#{records.select('1').to_sql})"
|
|
| 132 |
end |
|
| 133 | ||
| 134 |
# Returns a SQL condition to filter out journals with private notes |
|
| 135 |
# that are not visible to user |
|
| 136 |
def journal_private_notes_condition(user=User.current, options={})
|
|
| 137 |
journals = Journal.table_name |
|
| 138 |
permission = journalized_options[:permissions][:view_private_notes] |
|
| 139 |
allowed = |
|
| 140 |
if user.admin? |
|
| 141 |
'1=1' |
|
| 142 |
elsif permission && (reflection = journal_project_reflection) |
|
| 143 |
project_condition = |
|
| 144 |
Project.allowed_to_condition(user, permission, options.merge(:skip_pre_condition => true)) |
|
| 145 |
"#{table_name}.#{reflection.foreign_key} IN" \
|
|
| 146 |
" (SELECT #{Project.table_name}.id FROM #{Project.table_name} WHERE #{project_condition})"
|
|
| 147 |
else |
|
| 148 |
'1=0' |
|
| 149 |
end |
|
| 150 |
sanitize_sql_for_conditions( |
|
| 151 |
["(#{journals}.private_notes = ? OR #{journals}.user_id = ? OR (#{allowed}))", false, user.id]
|
|
| 152 |
) |
|
| 153 |
end |
|
| 154 | ||
| 155 |
# Returns the SQL joins from the journals table to the table of this |
|
| 156 |
# type and, if any, to the projects table. The journals must also be |
|
| 157 |
# restricted to this type, see Journal.joins_journalized |
|
| 158 |
def journal_joins |
|
| 159 |
journals = Journal.table_name |
|
| 160 |
joins = ["INNER JOIN #{table_name} ON #{table_name}.#{primary_key} = #{journals}.journalized_id"]
|
|
| 161 |
if (reflection = journal_project_reflection) |
|
| 162 |
joins << "INNER JOIN #{Project.table_name} ON #{Project.table_name}.id = #{table_name}.#{reflection.foreign_key}"
|
|
| 163 |
end |
|
| 164 |
joins |
|
| 165 |
end |
|
| 166 | ||
| 167 |
# Returns the scope of the activity events. The given scope contains |
|
| 168 |
# the journals of this type only, joined with journal_joins |
|
| 169 |
def journal_activity_scope(scope) |
|
| 170 |
scope |
|
| 171 |
end |
|
| 172 | ||
| 173 |
def journal_activity_preload |
|
| 174 |
journal_project_reflection ? [{:journalized => :project}, :user] : [:user]
|
|
| 175 |
end |
|
| 176 | ||
| 177 |
def journal_project_reflection |
|
| 178 |
reflection = reflect_on_association(:project) |
|
| 179 |
reflection if reflection&.belongs_to? |
|
| 180 |
end |
|
| 181 |
end |
|
| 182 | ||
| 183 |
module InstanceMethods |
|
| 184 |
# Event API |
|
| 185 |
def journal_event_title(journal) |
|
| 186 |
"#{self.class.model_name.human} ##{id}"
|
|
| 187 |
end |
|
| 188 | ||
| 189 |
def journal_event_type(journal) |
|
| 190 |
'journal-update' |
|
| 191 |
end |
|
| 192 | ||
| 193 |
def journal_event_url(journal) |
|
| 194 |
{:controller => self.class.model_name.plural, :action => 'show', :id => id, :anchor => "change-#{journal.id}"}
|
|
| 195 |
end |
|
| 196 | ||
| 197 |
# Called after a journal is created, if notifications are enabled |
|
| 198 |
def journal_notify(journal) |
|
| 199 |
end |
|
| 200 | ||
| 201 |
# Called before a journal is created |
|
| 202 |
def journal_process_watchers(journal) |
|
| 203 |
end |
|
| 204 | ||
| 205 |
# Attributes whose changes are journalized |
|
| 206 |
def journalized_attribute_names |
|
| 207 |
journalized_options[:attributes] |
|
| 208 |
end |
|
| 209 | ||
| 210 |
def journal_notified_users |
|
| 211 |
respond_to?(:notified_users) ? notified_users : [] |
|
| 212 |
end |
|
| 213 | ||
| 214 |
def journal_notified_watchers |
|
| 215 |
respond_to?(:notified_watchers) ? notified_watchers : [] |
|
| 216 |
end |
|
| 217 | ||
| 218 |
def journal_visible?(user=User.current) |
|
| 219 |
respond_to?(:visible?) && visible?(user) |
|
| 220 |
end |
|
| 221 | ||
| 222 |
def journal_project |
|
| 223 |
respond_to?(:project) ? project : nil |
|
| 224 |
end |
|
| 225 | ||
| 226 |
def journal_editable_by?(journal, user) |
|
| 227 |
return false unless user&.logged? |
|
| 228 | ||
| 229 |
journal_allowed_to?(user, :edit_notes) || |
|
| 230 |
(journal.user == user && journal_allowed_to?(user, :edit_own_notes)) |
|
| 231 |
end |
|
| 232 | ||
| 233 |
def journal_user_can_view_private_notes?(user) |
|
| 234 |
journal_allowed_to?(user, :view_private_notes) |
|
| 235 |
end |
|
| 236 | ||
| 237 |
def journal_private_notes_allowed?(user) |
|
| 238 |
journal_allowed_to?(user, :set_notes_private) |
|
| 239 |
end |
|
| 240 | ||
| 241 |
def journal_valid_watcher?(user) |
|
| 242 |
user.active? && |
|
| 243 |
journal_allowed_to?(user, :add_watchers) && |
|
| 244 |
respond_to?(:valid_watcher?) && valid_watcher?(user) && |
|
| 245 |
!watched_by?(user) |
|
| 246 |
end |
|
| 247 | ||
| 248 |
# Returns true if the relation of the given journal detail is visible to user |
|
| 249 |
def journal_relation_visible?(detail, user) |
|
| 250 |
false |
|
| 251 |
end |
|
| 252 | ||
| 253 |
# Returns the new status if the journal contains a status change, otherwise nil |
|
| 254 |
def journal_new_status(journal) |
|
| 255 |
nil |
|
| 256 |
end |
|
| 257 | ||
| 258 |
private |
|
| 259 | ||
| 260 |
# Checks the permission configured for key (see DEFAULT_PERMISSIONS) |
|
| 261 |
# on the journalized project, or globally if there is no project |
|
| 262 |
def journal_allowed_to?(user, key) |
|
| 263 |
permission = journalized_options[:permissions][key] |
|
| 264 |
return false if permission.nil? || user.nil? |
|
| 265 | ||
| 266 |
if (project = journal_project) |
|
| 267 |
user.allowed_to?(permission, project) |
|
| 268 |
else |
|
| 269 |
user.allowed_to?(permission, nil, :global => true) |
|
| 270 |
end |
|
| 271 |
end |
|
| 272 |
end |
|
| 273 |
end |
|
| 274 |
end |
|
| 275 |
end |
|
| lib/redmine/preparation.rb | ||
|---|---|---|
| 25 | 25 |
ApplicationRecord.include Redmine::Acts::Webhookable |
| 26 | 26 |
ApplicationRecord.include Redmine::I18n |
| 27 | 27 | |
| 28 |
# Journalized types, registered before their classes are loaded |
|
| 29 |
Redmine::Acts::Journalized.register 'Issue' |
|
| 30 | ||
| 28 | 31 |
Scm::Base.add "Subversion" |
| 29 | 32 |
Scm::Base.add "Mercurial" |
| 30 | 33 |
Scm::Base.add "Cvs" |
| 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/lib/redmine/acts/journalized_test.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 '../../../../test_helper' |
|
| 21 | ||
| 22 |
class Redmine::Acts::JournalizedTest < ActiveSupport::TestCase |
|
| 23 |
def setup |
|
| 24 |
User.current = nil |
|
| 25 |
# Use News as a second journalized type |
|
| 26 |
News.acts_as_journalized unless News.respond_to?(:journalized_options) |
|
| 27 |
Redmine::Acts::Journalized.register('News')
|
|
| 28 |
@news = News.find(1) |
|
| 29 |
end |
|
| 30 | ||
| 31 |
def teardown |
|
| 32 |
Redmine::Acts::Journalized.unregister('News')
|
|
| 33 |
end |
|
| 34 | ||
| 35 |
def test_issue_should_be_registered |
|
| 36 |
assert_include 'Issue', Redmine::Acts::Journalized.journalized_type_names |
|
| 37 |
assert_include Issue, Redmine::Acts::Journalized.journalized_classes |
|
| 38 |
end |
|
| 39 | ||
| 40 |
def test_acts_as_journalized_should_reject_invalid_options |
|
| 41 |
assert_raise ArgumentError do |
|
| 42 |
Class.new(ApplicationRecord) { acts_as_journalized :foo => 'bar' }
|
|
| 43 |
end |
|
| 44 |
end |
|
| 45 | ||
| 46 |
def test_visible_should_include_journals_of_each_journalized_type |
|
| 47 |
journal = Journal.create!(:journalized => @news, :user => User.find(2), :notes => 'News note') |
|
| 48 |
journals = Journal.visible(User.anonymous) |
|
| 49 | ||
| 50 |
assert_include journal, journals |
|
| 51 |
assert journals.where(:journalized_type => 'Issue').exists? |
|
| 52 |
assert_equal Journal.where(:journalized_type => 'Issue').visible(User.anonymous).sort, |
|
| 53 |
journals.where(:journalized_type => 'Issue').sort |
|
| 54 |
end |
|
| 55 | ||
| 56 |
def test_visible_should_not_include_journals_of_invisible_records |
|
| 57 |
journal = Journal.create!(:journalized => @news, :user => User.find(2), :notes => 'News note') |
|
| 58 |
Role.anonymous.remove_permission!(:view_news) |
|
| 59 | ||
| 60 |
assert_not_include journal, Journal.visible(User.anonymous) |
|
| 61 |
assert_not journal.visible?(User.anonymous) |
|
| 62 |
end |
|
| 63 | ||
| 64 |
def test_visible_should_not_include_private_notes_without_permission |
|
| 65 |
journal = Journal.create!(:journalized => @news, :user => User.find(2), :notes => 'Private', :private_notes => true) |
|
| 66 | ||
| 67 |
assert_not_include journal, Journal.visible(User.anonymous) |
|
| 68 |
assert_include journal, Journal.visible(User.find(2)) |
|
| 69 |
assert_include journal, Journal.visible(User.find(1)) |
|
| 70 |
end |
|
| 71 | ||
| 72 |
def test_notified_users_should_exclude_users_not_allowed_to_view_private_notes |
|
| 73 |
journal = Journal.new(:journalized => @news, :user => User.find(2), :notes => 'Private', :private_notes => true) |
|
| 74 |
Role.find(1).remove_permission!(:view_private_notes) |
|
| 75 | ||
| 76 |
assert journal.notified_users.none? {|user| user.roles_for_project(@news.project).include?(Role.find(1)) && !user.admin?}
|
|
| 77 |
end |
|
| 78 | ||
| 79 |
def test_editable_by_should_be_false_without_configured_permission |
|
| 80 |
journal = Journal.create!(:journalized => @news, :user => User.find(2), :notes => 'News note') |
|
| 81 | ||
| 82 |
assert_not journal.editable_by?(User.find(2)) |
|
| 83 |
end |
|
| 84 | ||
| 85 |
def test_visible_should_join_journalized_table_when_restricted_to_one_type |
|
| 86 |
sql = Journal.where(:journalized_type => 'Issue').visible(User.anonymous).to_sql |
|
| 87 |
assert_include 'INNER JOIN issues', sql |
|
| 88 |
assert_not_include 'EXISTS (SELECT 1 FROM "issues"', sql |
|
| 89 |
end |
|
| 90 | ||
| 91 |
def test_visible_should_use_exists_conditions_for_several_types |
|
| 92 |
sql = Journal.visible(User.anonymous).to_sql |
|
| 93 |
assert_not_include 'INNER JOIN issues', sql |
|
| 94 |
assert_include "journals.journalized_type = 'News' AND EXISTS", sql |
|
| 95 |
end |
|
| 96 | ||
| 97 |
def test_visible_with_join_and_exists_conditions_should_return_the_same_issue_journals |
|
| 98 |
user = User.find(2) |
|
| 99 |
assert_equal Journal.where(:journalized_type => 'Issue').visible(user).ids.sort, |
|
| 100 |
Journal.where(:journalized_type => 'Issue').where(Issue.journal_visible_condition(user)).ids.sort |
|
| 101 |
end |
|
| 102 | ||
| 103 |
def test_joins_journalized_should_join_journalized_table_and_project |
|
| 104 |
journals = Journal.joins_journalized(Issue).where(:issues => {:project_id => 1})
|
|
| 105 | ||
| 106 |
assert journals.any? |
|
| 107 |
assert journals.all? {|journal| journal.journalized.is_a?(Issue) && journal.project.id == 1}
|
|
| 108 |
end |
|
| 109 |
end |
|
| 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 |
- « Previous
- 1
- 2
- 3
- 4
- Next »