redmineup 1.1.17 → 1.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +1 -1
- data/app/assets/stylesheets/redmineup.css +8 -9
- data/app/assets/stylesheets/select2.css +2 -2
- data/app/views/redmineup/journals/_history.html.erb +33 -0
- data/doc/CHANGELOG +7 -0
- data/lib/redmineup/acts_as_journalized/hook.rb +16 -0
- data/lib/redmineup/acts_as_journalized/journal_event.rb +30 -0
- data/lib/redmineup/acts_as_journalized/journal_patch.rb +87 -0
- data/lib/redmineup/acts_as_journalized/up_acts_as_journalized.rb +118 -0
- data/lib/redmineup/acts_as_taggable/up_acts_as_taggable.rb +5 -0
- data/lib/redmineup/engine.rb +2 -0
- data/lib/redmineup/helpers/journals_helper.rb +53 -0
- data/lib/redmineup/helpers/tags_helper.rb +1 -1
- data/lib/redmineup/version.rb +1 -1
- data/lib/redmineup.rb +3 -0
- data/test/acts_as_journalized/up_acts_as_journalized_test.rb +106 -0
- data/test/models/journal.rb +38 -0
- data/test/models/journalized_thing.rb +5 -0
- data/test/redmine/acts_as_journalized/journalized_redmine_test.rb +264 -0
- data/test/schema.rb +17 -0
- metadata +14 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 506b754be350ea6e3591880f2d292171d54d6085f1c02bc16cfa5f42b0913bc1
|
|
4
|
+
data.tar.gz: 0400ce05a2c86b9f5704333ff05eb11faef0d6377efa81b42c33ca5b9abde33a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c1866918b5aed3b47aeb0a5b0eb74f65172ad383171bc82e434bc26346300948303d08a9b2c60932b044d94da6ba5053ee76c49e425c5cbab328b8225ca473fe
|
|
7
|
+
data.tar.gz: 1f278a0fe544255d064352e25aa1faaca6ac15b42a1167ec82053acf650b7c2d902b9466f8b98fdb5001428b3b53626fcba787db93b0cb399996b3538db703a5
|
data/Rakefile
CHANGED
|
@@ -260,9 +260,7 @@
|
|
|
260
260
|
padding: 0 3px;
|
|
261
261
|
}
|
|
262
262
|
.rup-tag-select.select2-container--default .select2-selection--multiple .select2-selection__choice {
|
|
263
|
-
|
|
264
|
-
border-radius: 3px;
|
|
265
|
-
font-size: 10px;
|
|
263
|
+
font-size: 0.65rem;
|
|
266
264
|
line-height: 1.3;
|
|
267
265
|
padding: 2px 4px;
|
|
268
266
|
}
|
|
@@ -397,27 +395,28 @@ div.tags-cloud { text-align: center; }
|
|
|
397
395
|
border-radius: 3px;
|
|
398
396
|
padding: 2px 4px;
|
|
399
397
|
display: inline-block;
|
|
400
|
-
font-size: 0.
|
|
398
|
+
font-size: 0.65rem;
|
|
401
399
|
line-height: 1.3;
|
|
402
|
-
margin: 0
|
|
403
|
-
|
|
400
|
+
margin: 0 2px 2px 0;
|
|
401
|
+
white-space: nowrap;
|
|
404
402
|
max-width: 100%;
|
|
405
403
|
box-sizing: border-box;
|
|
406
404
|
vertical-align: top;
|
|
407
405
|
}
|
|
408
406
|
|
|
409
|
-
.tag-label-color { color:
|
|
410
|
-
.tag-label-plain { background-color: #efefef;
|
|
407
|
+
.tag-label-color, .tag-label-color a, .tag-label-color a:link, .tag-label-color a:visited { color: #fff !important; }
|
|
408
|
+
.tag-label-plain { background-color: #efefef; }
|
|
409
|
+
.tag-label-plain, .tag-label-plain a, .tag-label-plain a:link, .tag-label-plain a:visited { color: #333 !important; }
|
|
411
410
|
|
|
412
411
|
.tag-label a, .tag-label-color a { color: inherit; text-decoration: none; font-weight: normal; }
|
|
413
412
|
.tag-label a:hover, .tag-label a:focus, .tag-label-color a:hover, .tag-label-color a:focus { text-decoration: none; }
|
|
414
413
|
|
|
415
414
|
.tag-label:hover, .tag-label-color:hover { filter: brightness(0.85); }
|
|
416
|
-
table.list .tag-label, table.list .tag-label-color { margin-bottom: 0; }
|
|
417
415
|
|
|
418
416
|
.tag-link a { font-weight: normal; }
|
|
419
417
|
|
|
420
418
|
.tag_list { margin-top: 5px; display: inline-block; }
|
|
419
|
+
td.tags, .tag_list { white-space: normal; }
|
|
421
420
|
|
|
422
421
|
span.tag-input { display: inline-block; }
|
|
423
422
|
|
|
@@ -217,8 +217,8 @@
|
|
|
217
217
|
border-width: 0 4px 5px 4px;
|
|
218
218
|
}
|
|
219
219
|
.select2-container--default .select2-selection--multiple {
|
|
220
|
-
background-color: white;
|
|
221
|
-
border: 1px solid #
|
|
220
|
+
background-color: var(--oc-white, #fff);
|
|
221
|
+
border: 1px solid var(--oc-gray-4, #ced4da);
|
|
222
222
|
border-radius: 3px;
|
|
223
223
|
cursor: text;
|
|
224
224
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
<% new_markup = up_journal_new_markup? %>
|
|
2
|
+
<% Journal.preload_journals_details_custom_fields(journals.to_a) %>
|
|
3
|
+
<div class="journals">
|
|
4
|
+
<% journals.to_a.each do |journal| %>
|
|
5
|
+
<div id="change-<%= journal.id %>" class="<%= journal.css_classes %>">
|
|
6
|
+
<div class="note">
|
|
7
|
+
<% if new_markup %>
|
|
8
|
+
<h4 class="journal-header">
|
|
9
|
+
<span class="journal-info">
|
|
10
|
+
<%= avatar(journal.user) %>
|
|
11
|
+
<%= authoring journal.created_on, journal.user, label: :label_updated_time_by %>
|
|
12
|
+
</span>
|
|
13
|
+
</h4>
|
|
14
|
+
<% else %>
|
|
15
|
+
<h4 class="note-header">
|
|
16
|
+
<%= avatar(journal.user) %>
|
|
17
|
+
<%= authoring journal.created_on, journal.user, label: :label_updated_time_by %>
|
|
18
|
+
</h4>
|
|
19
|
+
<% end %>
|
|
20
|
+
<%= content_tag('div', class: ('journal-content' if new_markup)) do %>
|
|
21
|
+
<% if journal.visible_details.any? %>
|
|
22
|
+
<%= content_tag('ul', class: new_markup ? 'journal-details' : 'details') do %>
|
|
23
|
+
<% journal.visible_details.each do |detail| %>
|
|
24
|
+
<li><%= up_journal_detail(detail, journal) %></li>
|
|
25
|
+
<% end %>
|
|
26
|
+
<% end %>
|
|
27
|
+
<% end %>
|
|
28
|
+
<%= content_tag('div', textilizable(journal, :notes), class: 'wiki') if show_notes && journal.notes.present? %>
|
|
29
|
+
<% end %>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
data/doc/CHANGELOG
CHANGED
|
@@ -4,6 +4,13 @@ Redmine UP gem - general functions for plugins (tags, vote, viewing, currency)
|
|
|
4
4
|
Copyright (C) 2011-2026 Kirill Bezrukov (RedmineUP)
|
|
5
5
|
https://www.redmineup.com/
|
|
6
6
|
|
|
7
|
+
== 2026-10-28 v1.1.18
|
|
8
|
+
|
|
9
|
+
* Added up_acts_as_journalized: change history for any model
|
|
10
|
+
* Added open_only option to Issue available_tags
|
|
11
|
+
* Updated tags: no commas between chips
|
|
12
|
+
* Fixed tags text color and wrapping in tables
|
|
13
|
+
|
|
7
14
|
== 2026-10-24 v1.1.17
|
|
8
15
|
|
|
9
16
|
* Fixed gem breaking Redmine 5.0-6.0 on start (Norwegian locale)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Redmineup
|
|
2
|
+
module ActsAsJournalized
|
|
3
|
+
class Hook < Redmine::Hook::Listener
|
|
4
|
+
def after_plugins_loaded(_context)
|
|
5
|
+
return unless Gem::Version.new(Redmine::VERSION.to_s) >= Gem::Version.new('5.0')
|
|
6
|
+
|
|
7
|
+
require File.expand_path('journal_patch', __dir__)
|
|
8
|
+
require File.expand_path('journal_event', __dir__)
|
|
9
|
+
::Journal.prepend(JournalPatch) unless ::Journal.include?(JournalPatch)
|
|
10
|
+
Redmineup::ActsAsJournalized.journalized_classes.each do |klass|
|
|
11
|
+
Redmineup::ActsAsJournalized.register_activity(klass) if klass.up_journalized_options[:activity]
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module Redmineup
|
|
2
|
+
class JournalEvent < ::Journal
|
|
3
|
+
def self.visible(user = User.current, options = {})
|
|
4
|
+
visible_for(user, options)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
def event_title
|
|
8
|
+
title = up_activity_options[:title]
|
|
9
|
+
title ? title.call(self) : journalized.to_s
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def event_url(options = {})
|
|
13
|
+
up_activity_options[:url].call(self).merge(options)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def event_type
|
|
17
|
+
up_activity_options[:css_type] || "#{journalized_type.underscore.dasherize}-edit"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def event_group
|
|
21
|
+
journalized
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def up_activity_options
|
|
27
|
+
journalized.up_journalized_options[:activity]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
module Redmineup
|
|
2
|
+
module ActsAsJournalized
|
|
3
|
+
module JournalPatch
|
|
4
|
+
def self.prepended(base)
|
|
5
|
+
base.extend(ClassMethods)
|
|
6
|
+
base.singleton_class.prepend(IssueOnlyVisible)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
module IssueOnlyVisible
|
|
10
|
+
def visible(*)
|
|
11
|
+
super.where(journalized_type: 'Issue')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
def up_project_ids(options)
|
|
17
|
+
project = options[:project]
|
|
18
|
+
return [project.id] unless options.fetch(:with_subprojects, Setting.display_subprojects_issues?)
|
|
19
|
+
|
|
20
|
+
project.self_and_descendants.ids
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def visible_for(user = User.current, options = {})
|
|
24
|
+
conditions = Redmineup::ActsAsJournalized.journalized_classes.map do |klass|
|
|
25
|
+
raise ArgumentError, "#{klass.name} must define scope :visible" unless klass.respond_to?(:visible)
|
|
26
|
+
|
|
27
|
+
visible = klass.visible(user).reorder(nil)
|
|
28
|
+
visible = visible.where(klass.table_name => { project_id: up_project_ids(options) }) if options[:project] && klass.column_names.include?('project_id')
|
|
29
|
+
visible = visible.except(:eager_load).left_outer_joins(*visible.eager_load_values) if visible.eager_load_values.any?
|
|
30
|
+
sanitize_sql_array(
|
|
31
|
+
["#{table_name}.journalized_type = ? AND #{table_name}.journalized_id IN (#{visible.select(klass.arel_table[:id]).to_sql})",
|
|
32
|
+
klass.base_class.name]
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
conditions.empty? ? none : where(conditions.join(' OR '))
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def up_journalized?
|
|
40
|
+
journalized.respond_to?(:up_journalized?)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def up_start_blank
|
|
44
|
+
@attributes_before_change = {}
|
|
45
|
+
@custom_values_before_change = {}
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def editable_by?(usr)
|
|
49
|
+
return super unless up_journalized?
|
|
50
|
+
|
|
51
|
+
permission = journalized.up_journalized_options[:edit_notes_permission]
|
|
52
|
+
return false unless permission && usr&.logged?
|
|
53
|
+
|
|
54
|
+
if permission.respond_to?(:call)
|
|
55
|
+
permission.call(self, usr)
|
|
56
|
+
else
|
|
57
|
+
usr.allowed_to?(permission, journalized.project)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def up_update_notes(usr, text)
|
|
62
|
+
return false unless editable_by?(usr)
|
|
63
|
+
|
|
64
|
+
self.notes = text
|
|
65
|
+
self.updated_by_id = usr.id if has_attribute?(:updated_by_id)
|
|
66
|
+
save
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def add_watcher
|
|
72
|
+
super unless up_journalized?
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def split_private_notes
|
|
76
|
+
up_journalized? || super
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def send_notification
|
|
80
|
+
return super unless up_journalized?
|
|
81
|
+
|
|
82
|
+
notifier = journalized.up_journalized_options[:notify]
|
|
83
|
+
notifier&.call(self) if notify?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
module Redmineup
|
|
2
|
+
module ActsAsJournalized
|
|
3
|
+
EXCLUDED_ATTRIBUTES = %w(id created_on updated_on created_at updated_at lock_version).freeze
|
|
4
|
+
OPTION_KEYS = %i[association associations only except journalize_create activity notify edit_notes_permission detail_formatter].freeze
|
|
5
|
+
|
|
6
|
+
def self.registry
|
|
7
|
+
@registry ||= []
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def self.journalized_classes
|
|
11
|
+
registry.filter_map(&:safe_constantize)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def self.register_activity(klass)
|
|
15
|
+
return unless defined?(::Redmineup::JournalEvent)
|
|
16
|
+
|
|
17
|
+
activity = klass.up_journalized_options[:activity]
|
|
18
|
+
type = (activity[:type] || "#{klass.name.underscore}_journals").to_s
|
|
19
|
+
class_name = klass.name
|
|
20
|
+
::Redmineup::JournalEvent.acts_as_activity_provider(
|
|
21
|
+
type: type,
|
|
22
|
+
author_key: :user_id,
|
|
23
|
+
scope: -> { ::Redmineup::ActsAsJournalized.activity_scope(class_name.constantize) }
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.activity_scope(klass)
|
|
28
|
+
keys = Array(klass.up_journalized_options[:activity][:keys]).map(&:to_s)
|
|
29
|
+
scope = ::Redmineup::JournalEvent.preload(:user, :journalized).where(journalized_type: klass.base_class.name)
|
|
30
|
+
notes = "#{::Journal.table_name}.notes <> ''"
|
|
31
|
+
return scope.where(notes) if keys.empty?
|
|
32
|
+
|
|
33
|
+
details = ::JournalDetail.table_name
|
|
34
|
+
scope.joins("LEFT OUTER JOIN #{details} ON #{details}.journal_id = #{::Journal.table_name}.id")
|
|
35
|
+
.where("#{notes} OR (#{details}.property = 'attr' AND #{details}.prop_key IN (?))", keys).distinct
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.included(base)
|
|
39
|
+
base.extend(ClassMethods)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
module ClassMethods
|
|
43
|
+
def up_acts_as_journalized(options = {})
|
|
44
|
+
options.assert_valid_keys(*OPTION_KEYS)
|
|
45
|
+
if options[:activity] && !options[:activity][:url].respond_to?(:call)
|
|
46
|
+
raise ArgumentError, 'up_acts_as_journalized: activity needs :url lambda'
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class_attribute :up_journalized_options, instance_writer: false
|
|
50
|
+
self.up_journalized_options = options
|
|
51
|
+
|
|
52
|
+
has_many options.fetch(:association, :journals), as: :journalized, class_name: 'Journal', dependent: :destroy, inverse_of: :journalized
|
|
53
|
+
(options[:associations] || {}).each do |association, attributes|
|
|
54
|
+
attributes.each do |attribute|
|
|
55
|
+
define_method("#{association}_#{attribute}") { public_send(association)&.public_send(attribute) }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
before_save :up_prepare_journal
|
|
59
|
+
after_save :up_save_journal
|
|
60
|
+
include InstanceMethods
|
|
61
|
+
include NoCustomFields unless method_defined?(:custom_field_values)
|
|
62
|
+
|
|
63
|
+
Redmineup::ActsAsJournalized.registry << name unless Redmineup::ActsAsJournalized.registry.include?(name)
|
|
64
|
+
Redmineup::ActsAsJournalized.register_activity(self) if options[:activity]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
module NoCustomFields
|
|
69
|
+
def custom_field_values
|
|
70
|
+
[]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
module InstanceMethods
|
|
75
|
+
def up_journalized?
|
|
76
|
+
true
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def init_journal(user = User.current, notes = '')
|
|
80
|
+
@current_journal ||= Journal.new(journalized: self, user: user, notes: notes).tap do
|
|
81
|
+
@tag_list = Redmineup::ActsAsTaggable::TagList.new(*@tag_list) if @tag_list
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def current_journal
|
|
86
|
+
@current_journal
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def clear_journal
|
|
90
|
+
@current_journal = nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def journalized_attribute_names
|
|
94
|
+
options = up_journalized_options
|
|
95
|
+
names = self.class.column_names - EXCLUDED_ATTRIBUTES - Array(options[:except]).map(&:to_s)
|
|
96
|
+
names = names.reject { |name| name.end_with?('_count') }
|
|
97
|
+
names &= Array(options[:only]).map(&:to_s) if options[:only]
|
|
98
|
+
names << 'tag_list' if respond_to?(:tag_list) && !Array(options[:except]).map(&:to_s).include?('tag_list')
|
|
99
|
+
names + (options[:associations] || {}).flat_map { |association, attributes| attributes.map { |attribute| "#{association}_#{attribute}" } }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def up_prepare_journal
|
|
105
|
+
current_journal.up_start_blank if current_journal && new_record? && up_journalized_options[:journalize_create]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def up_save_journal
|
|
109
|
+
return unless current_journal
|
|
110
|
+
|
|
111
|
+
current_journal.save
|
|
112
|
+
clear_journal
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
ActiveRecord::Base.include Redmineup::ActsAsJournalized
|
|
@@ -156,6 +156,11 @@ module Redmineup
|
|
|
156
156
|
projects_joined = true
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
+
if options[:open_only] && reflect_on_association(:status)&.klass == IssueStatus
|
|
160
|
+
scope = scope.joins("JOIN #{IssueStatus.table_name} ON #{IssueStatus.table_name}.id = #{table_name}.status_id").
|
|
161
|
+
where(IssueStatus.table_name => { is_closed: false })
|
|
162
|
+
end
|
|
163
|
+
|
|
159
164
|
if base_class.respond_to?(:visible_condition)
|
|
160
165
|
visible_condition = base_class.visible_condition(User.current)
|
|
161
166
|
if visible_condition.include?('project_id') && !projects_joined
|
data/lib/redmineup/engine.rb
CHANGED
|
@@ -9,6 +9,8 @@ module Redmineup
|
|
|
9
9
|
require File.expand_path('../redmineup/patches/auto_completes_controller_patch', __dir__)
|
|
10
10
|
|
|
11
11
|
require File.expand_path('../redmineup/hooks/views_layouts_hook', __dir__)
|
|
12
|
+
|
|
13
|
+
require File.expand_path('../redmineup/acts_as_journalized/hook', __dir__)
|
|
12
14
|
end
|
|
13
15
|
end
|
|
14
16
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Redmineup
|
|
2
|
+
module JournalsHelper
|
|
3
|
+
def up_journal_detail(detail, journal, options = {})
|
|
4
|
+
formatter = journal.journalized.up_journalized_options[:detail_formatter]
|
|
5
|
+
text = formatter&.call(detail, journal)
|
|
6
|
+
return text if text
|
|
7
|
+
|
|
8
|
+
label, old_value, value = up_journal_detail_values(detail, journal)
|
|
9
|
+
text =
|
|
10
|
+
if detail.old_value.blank? && detail.value.present?
|
|
11
|
+
l(:text_journal_set_to, label: label, value: value)
|
|
12
|
+
elsif detail.value.blank?
|
|
13
|
+
l(:text_journal_deleted, label: label, old: old_value)
|
|
14
|
+
else
|
|
15
|
+
l(:text_journal_changed, label: label, old: old_value, new: value)
|
|
16
|
+
end
|
|
17
|
+
options[:no_html] ? text : text.html_safe
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def up_render_journal_history(journals, show_notes: true)
|
|
21
|
+
render partial: 'redmineup/journals/history', locals: { journals: journals, show_notes: show_notes }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def up_journal_new_markup?
|
|
25
|
+
Gem::Version.new(Redmine::VERSION.to_s) >= Gem::Version.new('6.1')
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def up_journal_detail_values(detail, journal)
|
|
31
|
+
record = journal.journalized
|
|
32
|
+
if detail.property == 'cf'
|
|
33
|
+
field = CustomField.find_by(id: detail.prop_key)
|
|
34
|
+
return [h(detail.prop_key), h(detail.old_value), h(detail.value)] unless field
|
|
35
|
+
|
|
36
|
+
format = ->(v) { v.blank? ? '' : field.format.formatted_value(self, field, v, record, false) }
|
|
37
|
+
[h(field.name), h(format.call(detail.old_value)), h(format.call(detail.value))]
|
|
38
|
+
else
|
|
39
|
+
key = detail.prop_key.to_s
|
|
40
|
+
name = key.delete_suffix('_id')
|
|
41
|
+
reflection = record.class.reflect_on_association(name) if name != key
|
|
42
|
+
format = lambda do |v|
|
|
43
|
+
next '' if v.blank?
|
|
44
|
+
next v.to_s unless reflection
|
|
45
|
+
|
|
46
|
+
target = reflection.klass.find_by(id: v)
|
|
47
|
+
target && (!target.respond_to?(:visible?) || target.visible?) ? target.to_s : v.to_s
|
|
48
|
+
end
|
|
49
|
+
[h(l("field_#{key}", default: l("field_#{name}", default: name.humanize))), h(format.call(detail.old_value)), h(format.call(detail.value))]
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/redmineup/version.rb
CHANGED
data/lib/redmineup.rb
CHANGED
|
@@ -20,6 +20,7 @@ require 'redmineup/acts_as_votable/voter'
|
|
|
20
20
|
require 'redmineup/acts_as_draftable/up_acts_as_draftable'
|
|
21
21
|
require 'redmineup/acts_as_draftable/draft'
|
|
22
22
|
require 'redmineup/acts_as_priceable/up_acts_as_priceable'
|
|
23
|
+
require 'redmineup/acts_as_journalized/up_acts_as_journalized'
|
|
23
24
|
|
|
24
25
|
require 'redmineup/currency'
|
|
25
26
|
require 'redmineup/helpers/tags_helper'
|
|
@@ -42,6 +43,7 @@ require 'redmineup/liquid/drops/version_drop'
|
|
|
42
43
|
require 'redmineup/liquid/drops/custom_field_enumeration_drop'
|
|
43
44
|
|
|
44
45
|
require 'redmineup/helpers/external_assets_helper'
|
|
46
|
+
require 'redmineup/helpers/journals_helper'
|
|
45
47
|
require 'redmineup/helpers/form_tag_helper'
|
|
46
48
|
require 'redmineup/helpers/calendars_helper'
|
|
47
49
|
require 'redmineup/helpers/datetime_helper'
|
|
@@ -109,6 +111,7 @@ if defined?(ActionView::Base)
|
|
|
109
111
|
ActionView::Base.send :include, Redmineup::CalendarsHelper
|
|
110
112
|
ActionView::Base.send :include, Redmineup::ExternalAssetsHelper
|
|
111
113
|
ActionView::Base.send :include, Redmineup::FormTagHelper
|
|
114
|
+
ActionView::Base.send :include, Redmineup::JournalsHelper
|
|
112
115
|
ActionView::Base.send :include, Redmineup::TagsHelper
|
|
113
116
|
end
|
|
114
117
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require File.dirname(__FILE__) + '/../test_helper'
|
|
2
|
+
|
|
3
|
+
class UpActsAsJournalizedTest < ActiveSupport::TestCase
|
|
4
|
+
def setup
|
|
5
|
+
@user = User.first || User.create!(login: 'journalist')
|
|
6
|
+
@thing = JournalizedThing.create!(name: 'one', status: 'new')
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def test_attribute_change_is_journaled
|
|
10
|
+
@thing.init_journal(@user, 'why')
|
|
11
|
+
@thing.update!(status: 'done')
|
|
12
|
+
|
|
13
|
+
journal = @thing.journals.last
|
|
14
|
+
assert_equal 'why', journal.notes
|
|
15
|
+
assert_equal [%w(status new done)], journal.details.map { |d| [d.prop_key, d.old_value, d.value] }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_repeated_edits_keep_every_step
|
|
19
|
+
%w(a b c).each do |status|
|
|
20
|
+
@thing.init_journal(@user)
|
|
21
|
+
@thing.update!(status: status)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
assert_equal 3, @thing.journals.count
|
|
25
|
+
assert_equal [%w(new a), %w(a b), %w(b c)], @thing.journals.order(:id).map { |j| [j.details.first.old_value, j.details.first.value] }
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def test_several_changes_make_one_journal
|
|
29
|
+
@thing.init_journal(@user)
|
|
30
|
+
@thing.update!(status: 'done', due_date: Date.new(2026, 1, 1))
|
|
31
|
+
|
|
32
|
+
assert_equal 1, @thing.journals.count
|
|
33
|
+
assert_equal %w(due_date status), @thing.journals.first.details.map(&:prop_key).sort
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def test_notes_only_journal
|
|
37
|
+
@thing.init_journal(@user, 'just a note')
|
|
38
|
+
@thing.save!
|
|
39
|
+
|
|
40
|
+
assert_equal 1, @thing.journals.count
|
|
41
|
+
assert_empty @thing.journals.first.details
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def test_unchanged_save_without_notes_makes_no_journal
|
|
45
|
+
@thing.init_journal(@user)
|
|
46
|
+
@thing.save!
|
|
47
|
+
|
|
48
|
+
assert_equal 0, @thing.journals.count
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def test_no_journal_without_init_journal
|
|
52
|
+
@thing.update!(status: 'done')
|
|
53
|
+
|
|
54
|
+
assert_equal 0, @thing.journals.count
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def test_rollback_leaves_no_journal
|
|
58
|
+
JournalizedThing.transaction do
|
|
59
|
+
@thing.init_journal(@user, 'x')
|
|
60
|
+
@thing.update!(status: 'done')
|
|
61
|
+
raise ActiveRecord::Rollback
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
assert_equal 0, Journal.where(journalized_type: 'JournalizedThing').count
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_create_is_not_journaled_by_default
|
|
68
|
+
thing = JournalizedThing.new(name: 'two', status: 'new')
|
|
69
|
+
thing.init_journal(@user)
|
|
70
|
+
thing.save!
|
|
71
|
+
|
|
72
|
+
assert_equal 0, thing.journals.count
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_excluded_and_counter_attributes_are_not_tracked
|
|
76
|
+
names = @thing.journalized_attribute_names
|
|
77
|
+
|
|
78
|
+
assert_includes names, 'status'
|
|
79
|
+
assert_not_includes names, 'id'
|
|
80
|
+
assert_not_includes names, 'updated_on'
|
|
81
|
+
assert_not_includes names, 'comments_count'
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def test_destroy_deletes_journals
|
|
85
|
+
@thing.init_journal(@user, 'x')
|
|
86
|
+
@thing.update!(status: 'done')
|
|
87
|
+
|
|
88
|
+
assert_difference('Journal.count', -1) { @thing.destroy }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def test_unknown_option_is_rejected
|
|
92
|
+
assert_raises(ArgumentError) { Class.new(ActiveRecord::Base) { self.table_name = 'journalized_things'; up_acts_as_journalized foo: 1 } }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_activity_needs_url
|
|
96
|
+
assert_raises(ArgumentError) { Class.new(ActiveRecord::Base) { self.table_name = 'journalized_things'; up_acts_as_journalized activity: {} } }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def test_custom_field_values_fallback
|
|
100
|
+
assert_equal [], @thing.custom_field_values
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def test_registered
|
|
104
|
+
assert_includes Redmineup::ActsAsJournalized.registry, 'JournalizedThing'
|
|
105
|
+
end
|
|
106
|
+
end
|
data/test/models/journal.rb
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
|
+
class JournalDetail < ActiveRecord::Base
|
|
2
|
+
belongs_to :journal
|
|
3
|
+
end
|
|
4
|
+
|
|
1
5
|
class Journal < ActiveRecord::Base
|
|
2
6
|
belongs_to :journalized, :polymorphic => true
|
|
3
7
|
belongs_to :issue, :foreign_key => :journalized_id
|
|
4
8
|
|
|
5
9
|
belongs_to :user
|
|
10
|
+
has_many :details, :class_name => 'JournalDetail', :dependent => :delete_all, :inverse_of => :journal
|
|
11
|
+
|
|
12
|
+
attr_accessor :notify
|
|
13
|
+
|
|
14
|
+
def initialize(*args)
|
|
15
|
+
super
|
|
16
|
+
if journalized
|
|
17
|
+
journalized.new_record? ? self.notify = false : start
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def save(*args)
|
|
22
|
+
journalize_changes
|
|
23
|
+
(new_record? && notes.blank? && details.empty?) ? false : super()
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def start
|
|
27
|
+
@attributes_before_change = journalized.journalized_attribute_names.index_with { |name| journalized.send(name) }
|
|
28
|
+
@custom_values_before_change = {}
|
|
29
|
+
self
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def journalize_changes
|
|
33
|
+
if @attributes_before_change
|
|
34
|
+
(journalized.journalized_attribute_names + @attributes_before_change.keys).uniq.each do |name|
|
|
35
|
+
before = @attributes_before_change[name]
|
|
36
|
+
after = journalized.send(name)
|
|
37
|
+
next if before == after || (before.blank? && after.blank?)
|
|
38
|
+
|
|
39
|
+
details << JournalDetail.new(property: 'attr', prop_key: name, old_value: before, value: after)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
start
|
|
43
|
+
end
|
|
6
44
|
end
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
require File.join(Dir.pwd, 'test/test_helper')
|
|
2
|
+
|
|
3
|
+
ActiveRecord::Base.connection.create_table :up_journalized_records, force: true do |t|
|
|
4
|
+
t.references :project
|
|
5
|
+
t.string :name
|
|
6
|
+
t.date :due_date
|
|
7
|
+
t.string :status
|
|
8
|
+
t.integer :comments_count, default: 0
|
|
9
|
+
t.datetime :created_on
|
|
10
|
+
t.datetime :updated_on
|
|
11
|
+
end
|
|
12
|
+
Minitest.after_run { ActiveRecord::Base.connection.drop_table :up_journalized_records, if_exists: true }
|
|
13
|
+
|
|
14
|
+
class UpJournalizedRecordCustomField < CustomField
|
|
15
|
+
def type_name
|
|
16
|
+
:label_project_plural
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class UpJournalizedRecord < (defined?(ApplicationRecord) ? ApplicationRecord : ActiveRecord::Base)
|
|
21
|
+
NOTIFIED = []
|
|
22
|
+
|
|
23
|
+
belongs_to :project
|
|
24
|
+
acts_as_customizable
|
|
25
|
+
|
|
26
|
+
scope :visible, lambda { |user = User.current, _options = {}|
|
|
27
|
+
joins(:project).where(Project.allowed_to_condition(user, :view_issues))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
def visible?(user = User.current)
|
|
31
|
+
user.allowed_to?(:view_issues, project)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
up_acts_as_journalized(
|
|
35
|
+
notify: ->(journal) { NOTIFIED << journal.id },
|
|
36
|
+
edit_notes_permission: :edit_issues,
|
|
37
|
+
activity: { type: 'up_records', url: ->(journal) { { controller: 'welcome' } }, title: ->(journal) { "Record #{journal.journalized.name}" } }
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
Redmine::Activity.register :up_records, class_name: 'Redmineup::JournalEvent'
|
|
42
|
+
|
|
43
|
+
class UpActsAsJournalizedRedmineTest < ActiveSupport::TestCase
|
|
44
|
+
fixtures :projects, :users, :email_addresses, :roles, :members, :member_roles, :enabled_modules, :trackers,
|
|
45
|
+
:issue_statuses, :issues, :journals, :journal_details, :enumerations, :custom_fields, :custom_values,
|
|
46
|
+
:projects_trackers, :watchers, :issue_categories, :versions
|
|
47
|
+
|
|
48
|
+
def setup
|
|
49
|
+
User.current = nil
|
|
50
|
+
UpJournalizedRecord::NOTIFIED.clear
|
|
51
|
+
@project = Project.find(1)
|
|
52
|
+
@user = User.find(2)
|
|
53
|
+
@field = UpJournalizedRecordCustomField.create!(
|
|
54
|
+
name: 'Up tags', field_format: 'list', multiple: true, possible_values: %w(a b c), is_for_all: true
|
|
55
|
+
)
|
|
56
|
+
@record = UpJournalizedRecord.create!(project: @project, name: 'rec', status: 'new')
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def test_attribute_and_notes
|
|
60
|
+
@record.init_journal(@user, 'changed')
|
|
61
|
+
@record.update!(status: 'done', due_date: Date.new(2026, 1, 2))
|
|
62
|
+
|
|
63
|
+
journal = @record.journals.last
|
|
64
|
+
assert_equal 'changed', journal.notes
|
|
65
|
+
assert_equal %w(due_date status), journal.details.map(&:prop_key).sort
|
|
66
|
+
assert_equal 'attr', journal.details.first.property
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def test_multiple_value_custom_field
|
|
70
|
+
@record.reload
|
|
71
|
+
@record.init_journal(@user)
|
|
72
|
+
@record.custom_field_values = { @field.id.to_s => %w(a b) }
|
|
73
|
+
@record.save!
|
|
74
|
+
|
|
75
|
+
@record.reload
|
|
76
|
+
@record.init_journal(@user)
|
|
77
|
+
@record.custom_field_values = { @field.id.to_s => %w(b c) }
|
|
78
|
+
@record.save!
|
|
79
|
+
|
|
80
|
+
details = @record.journals.order(:id).flat_map { |j| j.details.map { |d| [d.property, d.old_value, d.value] } }
|
|
81
|
+
assert_includes details, ['cf', nil, 'a']
|
|
82
|
+
assert_includes details, ['cf', nil, 'b']
|
|
83
|
+
assert_includes details, ['cf', 'a', nil]
|
|
84
|
+
assert_includes details, ['cf', nil, 'c']
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def test_notes_only
|
|
88
|
+
@record.init_journal(@user, 'only note')
|
|
89
|
+
@record.save!
|
|
90
|
+
|
|
91
|
+
assert_equal ['only note'], @record.journals.map(&:notes)
|
|
92
|
+
assert_empty @record.journals.first.details
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def test_unchanged_save_without_notes_has_no_journal
|
|
96
|
+
@record.init_journal(@user)
|
|
97
|
+
@record.save!
|
|
98
|
+
|
|
99
|
+
assert_equal 0, @record.journals.count
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def test_rollback_leaves_no_journal
|
|
103
|
+
UpJournalizedRecord.transaction do
|
|
104
|
+
@record.init_journal(@user, 'x')
|
|
105
|
+
@record.update!(status: 'done')
|
|
106
|
+
raise ActiveRecord::Rollback
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
assert_equal 0, Journal.where(journalized_type: 'UpJournalizedRecord').count
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def test_create_with_journalize_create
|
|
113
|
+
UpJournalizedRecord.up_journalized_options = UpJournalizedRecord.up_journalized_options.merge(journalize_create: true)
|
|
114
|
+
record = UpJournalizedRecord.new(project: @project, name: 'new', status: 'open')
|
|
115
|
+
record.init_journal(@user)
|
|
116
|
+
record.save!
|
|
117
|
+
|
|
118
|
+
keys = record.journals.first.details.map(&:prop_key)
|
|
119
|
+
assert_includes keys, 'name'
|
|
120
|
+
assert_includes keys, 'status'
|
|
121
|
+
ensure
|
|
122
|
+
UpJournalizedRecord.up_journalized_options = UpJournalizedRecord.up_journalized_options.except(:journalize_create)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def test_no_watchers_and_no_issue_mail
|
|
126
|
+
ActionMailer::Base.deliveries.clear
|
|
127
|
+
assert_no_difference 'Watcher.count' do
|
|
128
|
+
@record.init_journal(@user, 'x')
|
|
129
|
+
@record.update!(status: 'done')
|
|
130
|
+
end
|
|
131
|
+
assert_empty ActionMailer::Base.deliveries
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_notify_callback_runs_after_commit
|
|
135
|
+
@record.init_journal(@user, 'x')
|
|
136
|
+
@record.update!(status: 'done')
|
|
137
|
+
|
|
138
|
+
assert_equal [@record.journals.last.id], UpJournalizedRecord::NOTIFIED
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def test_notify_callback_skipped_when_journal_notify_is_false
|
|
142
|
+
@record.init_journal(@user, 'x').notify = false
|
|
143
|
+
@record.update!(status: 'done')
|
|
144
|
+
|
|
145
|
+
assert_empty UpJournalizedRecord::NOTIFIED
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def test_issue_journals_are_unaffected
|
|
149
|
+
ActionMailer::Base.deliveries.clear
|
|
150
|
+
Setting.notified_events = %w(issue_updated)
|
|
151
|
+
issue = Issue.find(1)
|
|
152
|
+
issue.init_journal(@user, 'issue note')
|
|
153
|
+
issue.update!(subject: 'changed subject')
|
|
154
|
+
|
|
155
|
+
assert_equal 'issue note', issue.journals.last.notes
|
|
156
|
+
assert_equal 'subject', issue.journals.last.details.first.prop_key
|
|
157
|
+
assert_not_empty ActionMailer::Base.deliveries
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def test_issue_visible_scope_ignores_journals_of_other_types
|
|
161
|
+
@record.init_journal(@user, 'collision')
|
|
162
|
+
@record.save!
|
|
163
|
+
journal = @record.journals.last
|
|
164
|
+
Journal.where(id: journal.id).update_all(journalized_id: 1)
|
|
165
|
+
|
|
166
|
+
assert_not_includes Journal.visible(@user).pluck(:id), journal.id
|
|
167
|
+
assert_includes Journal.visible(@user).pluck(:id), Journal.where(journalized_type: 'Issue', journalized_id: 1).first.id
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def test_history_hides_custom_field_details_of_invisible_fields
|
|
171
|
+
@field.update!(visible: false, role_ids: [])
|
|
172
|
+
@record.reload
|
|
173
|
+
@record.init_journal(@user)
|
|
174
|
+
@record.custom_field_values = { @field.id.to_s => %w(a) }
|
|
175
|
+
@record.save!
|
|
176
|
+
|
|
177
|
+
journal = @record.journals.last
|
|
178
|
+
assert_equal 1, journal.details.size
|
|
179
|
+
assert_empty journal.visible_details(User.find(2))
|
|
180
|
+
assert_equal 1, journal.visible_details(User.find(1)).size
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def test_visible_for
|
|
184
|
+
hidden = UpJournalizedRecord.create!(project: Project.find(2), name: 'hidden')
|
|
185
|
+
[@record, hidden].each do |record|
|
|
186
|
+
record.init_journal(@user, 'n')
|
|
187
|
+
record.save!
|
|
188
|
+
end
|
|
189
|
+
Project.find(2).update!(is_public: false)
|
|
190
|
+
Member.where(project_id: 2, user_id: @user.id).delete_all
|
|
191
|
+
|
|
192
|
+
ids = Journal.visible_for(@user).pluck(:journalized_id, :journalized_type)
|
|
193
|
+
assert_includes ids, [@record.id, 'UpJournalizedRecord']
|
|
194
|
+
assert_not_includes ids, [hidden.id, 'UpJournalizedRecord']
|
|
195
|
+
assert Journal.visible_for(@user).all? { |j| j.journalized_type == 'UpJournalizedRecord' }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def test_activity_event
|
|
199
|
+
@record.init_journal(@user, 'visible note')
|
|
200
|
+
@record.save!
|
|
201
|
+
|
|
202
|
+
fetcher = Redmine::Activity::Fetcher.new(@user)
|
|
203
|
+
fetcher.scope = ['up_records']
|
|
204
|
+
event = fetcher.events(Date.today - 1, Date.today + 1).first
|
|
205
|
+
|
|
206
|
+
assert_equal "Record rec", event.event_title
|
|
207
|
+
assert_equal 'visible note', event.event_description
|
|
208
|
+
assert_equal @record, event.event_group
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def test_activity_is_limited_to_project
|
|
212
|
+
other = UpJournalizedRecord.create!(project: Project.find(2), name: 'other')
|
|
213
|
+
[@record, other].each do |record|
|
|
214
|
+
record.init_journal(@user, 'n')
|
|
215
|
+
record.save!
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
events = Redmineup::JournalEvent.find_events('up_records', User.find(1), nil, nil, project: Project.find(2))
|
|
219
|
+
assert_equal [other], events.map(&:event_group)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def test_activity_only_notes_and_listed_keys
|
|
223
|
+
@record.init_journal(@user)
|
|
224
|
+
@record.update!(status: 'quiet')
|
|
225
|
+
fetcher = Redmine::Activity::Fetcher.new(@user)
|
|
226
|
+
fetcher.scope = ['up_records']
|
|
227
|
+
assert_empty fetcher.events(Date.today - 1, Date.today + 1)
|
|
228
|
+
|
|
229
|
+
UpJournalizedRecord.up_journalized_options = UpJournalizedRecord.up_journalized_options.merge(activity: UpJournalizedRecord.up_journalized_options[:activity].merge(keys: %w(status)))
|
|
230
|
+
@record.init_journal(@user)
|
|
231
|
+
@record.update!(status: 'loud')
|
|
232
|
+
assert_equal 2, fetcher.events(Date.today - 1, Date.today + 1).size
|
|
233
|
+
ensure
|
|
234
|
+
UpJournalizedRecord.up_journalized_options = UpJournalizedRecord.up_journalized_options.merge(activity: UpJournalizedRecord.up_journalized_options[:activity].except(:keys))
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def test_editable_by_and_update_notes
|
|
238
|
+
@record.init_journal(@user, 'old')
|
|
239
|
+
@record.update!(status: 'x')
|
|
240
|
+
journal = @record.journals.last
|
|
241
|
+
|
|
242
|
+
assert journal.editable_by?(User.find(2))
|
|
243
|
+
assert_not journal.editable_by?(User.anonymous)
|
|
244
|
+
assert journal.up_update_notes(@user, 'new')
|
|
245
|
+
assert_equal 'new', journal.reload.notes
|
|
246
|
+
assert_equal @user.id, journal.updated_by_id if journal.has_attribute?(:updated_by_id)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def test_journal_detail_escapes_deleted_custom_field_values
|
|
250
|
+
detail = JournalDetail.new(property: 'cf', prop_key: '999999', old_value: '<b>old</b>', value: '<script>x</script>')
|
|
251
|
+
html = ApplicationController.new.view_context.up_journal_detail(detail, @record.journals.build)
|
|
252
|
+
|
|
253
|
+
assert_no_match(/<script>|<b>/, html)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def test_journal_detail_text
|
|
257
|
+
@record.init_journal(@user)
|
|
258
|
+
@record.update!(status: 'done')
|
|
259
|
+
detail = @record.journals.last.details.first
|
|
260
|
+
|
|
261
|
+
view = ApplicationController.new.view_context
|
|
262
|
+
assert_match(/new/, view.up_journal_detail(detail, @record.journals.last, no_html: true))
|
|
263
|
+
end
|
|
264
|
+
end
|
data/test/schema.rb
CHANGED
|
@@ -34,6 +34,23 @@ ActiveRecord::Schema.define version: 0 do
|
|
|
34
34
|
t.boolean :private_notes, default: false, null: false
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
create_table :journal_details, force: true do |t|
|
|
38
|
+
t.integer :journal_id, default: 0, null: false
|
|
39
|
+
t.string :property, limit: 30, default: '', null: false
|
|
40
|
+
t.string :prop_key, limit: 30, default: '', null: false
|
|
41
|
+
t.text :old_value
|
|
42
|
+
t.text :value
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
create_table :journalized_things, force: true do |t|
|
|
46
|
+
t.string :name
|
|
47
|
+
t.date :due_date
|
|
48
|
+
t.string :status
|
|
49
|
+
t.integer :comments_count, default: 0
|
|
50
|
+
t.datetime :created_on
|
|
51
|
+
t.datetime :updated_on
|
|
52
|
+
end
|
|
53
|
+
|
|
37
54
|
create_table :tags, force: true do |t|
|
|
38
55
|
t.string :name
|
|
39
56
|
t.string :color
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: redmineup
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.1.
|
|
4
|
+
version: 1.1.18
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- RedmineUP
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-28 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -129,6 +129,7 @@ files:
|
|
|
129
129
|
- app/controllers/redmineup_controller.rb
|
|
130
130
|
- app/views/redmineup/_additional_assets.html.erb
|
|
131
131
|
- app/views/redmineup/_money.html.erb
|
|
132
|
+
- app/views/redmineup/journals/_history.html.erb
|
|
132
133
|
- app/views/redmineup/settings.html.erb
|
|
133
134
|
- config/currency_iso.json
|
|
134
135
|
- config/locales/ar.yml
|
|
@@ -194,6 +195,10 @@ files:
|
|
|
194
195
|
- lib/redmineup.rb
|
|
195
196
|
- lib/redmineup/acts_as_draftable/draft.rb
|
|
196
197
|
- lib/redmineup/acts_as_draftable/up_acts_as_draftable.rb
|
|
198
|
+
- lib/redmineup/acts_as_journalized/hook.rb
|
|
199
|
+
- lib/redmineup/acts_as_journalized/journal_event.rb
|
|
200
|
+
- lib/redmineup/acts_as_journalized/journal_patch.rb
|
|
201
|
+
- lib/redmineup/acts_as_journalized/up_acts_as_journalized.rb
|
|
197
202
|
- lib/redmineup/acts_as_list/list.rb
|
|
198
203
|
- lib/redmineup/acts_as_priceable/up_acts_as_priceable.rb
|
|
199
204
|
- lib/redmineup/acts_as_taggable/tag.rb
|
|
@@ -217,6 +222,7 @@ files:
|
|
|
217
222
|
- lib/redmineup/helpers/datetime_helper.rb
|
|
218
223
|
- lib/redmineup/helpers/external_assets_helper.rb
|
|
219
224
|
- lib/redmineup/helpers/form_tag_helper.rb
|
|
225
|
+
- lib/redmineup/helpers/journals_helper.rb
|
|
220
226
|
- lib/redmineup/helpers/tags_helper.rb
|
|
221
227
|
- lib/redmineup/helpers/test_helper.rb
|
|
222
228
|
- lib/redmineup/helpers/vote_helper.rb
|
|
@@ -248,6 +254,7 @@ files:
|
|
|
248
254
|
- redmineup.gemspec
|
|
249
255
|
- test/acts_as_draftable/draft_test.rb
|
|
250
256
|
- test/acts_as_draftable/rup_acts_as_draftable_test.rb
|
|
257
|
+
- test/acts_as_journalized/up_acts_as_journalized_test.rb
|
|
251
258
|
- test/acts_as_taggable/rup_acts_as_taggable_test.rb
|
|
252
259
|
- test/acts_as_taggable/tag_list_test.rb
|
|
253
260
|
- test/acts_as_taggable/tag_test.rb
|
|
@@ -285,11 +292,13 @@ files:
|
|
|
285
292
|
- test/models/issue.rb
|
|
286
293
|
- test/models/issue_relation.rb
|
|
287
294
|
- test/models/journal.rb
|
|
295
|
+
- test/models/journalized_thing.rb
|
|
288
296
|
- test/models/news.rb
|
|
289
297
|
- test/models/project.rb
|
|
290
298
|
- test/models/user.rb
|
|
291
299
|
- test/models/vote_classes.rb
|
|
292
300
|
- test/money_helper_test.rb
|
|
301
|
+
- test/redmine/acts_as_journalized/journalized_redmine_test.rb
|
|
293
302
|
- test/schema.rb
|
|
294
303
|
- test/tags_helper_test.rb
|
|
295
304
|
- test/tags_helper_view_test.rb
|
|
@@ -321,6 +330,7 @@ summary: Common libraries for RedmineUP plugins for Redmine
|
|
|
321
330
|
test_files:
|
|
322
331
|
- test/acts_as_draftable/draft_test.rb
|
|
323
332
|
- test/acts_as_draftable/rup_acts_as_draftable_test.rb
|
|
333
|
+
- test/acts_as_journalized/up_acts_as_journalized_test.rb
|
|
324
334
|
- test/acts_as_taggable/rup_acts_as_taggable_test.rb
|
|
325
335
|
- test/acts_as_taggable/tag_list_test.rb
|
|
326
336
|
- test/acts_as_taggable/tag_test.rb
|
|
@@ -358,11 +368,13 @@ test_files:
|
|
|
358
368
|
- test/models/issue.rb
|
|
359
369
|
- test/models/issue_relation.rb
|
|
360
370
|
- test/models/journal.rb
|
|
371
|
+
- test/models/journalized_thing.rb
|
|
361
372
|
- test/models/news.rb
|
|
362
373
|
- test/models/project.rb
|
|
363
374
|
- test/models/user.rb
|
|
364
375
|
- test/models/vote_classes.rb
|
|
365
376
|
- test/money_helper_test.rb
|
|
377
|
+
- test/redmine/acts_as_journalized/journalized_redmine_test.rb
|
|
366
378
|
- test/schema.rb
|
|
367
379
|
- test/tags_helper_test.rb
|
|
368
380
|
- test/tags_helper_view_test.rb
|