Skip to content

Commit 8784355

Browse files
authored
Merge pull request opf#18409 from opf/implementation/60988-define-mention-criterion-for-work-package-comments-with-restricted-visibility
Define user @mention criterion for work package comments with restricted visibility
2 parents 0aa8917 + 5e62896 commit 8784355

File tree

20 files changed

+687
-62
lines changed

20 files changed

+687
-62
lines changed

app/components/work_packages/activities_tab/index_component.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ def add_comment_wrapper_data_attributes
8080
action: index_stimulus_controller(":onSubmit-end@window->#{restricted_comment_stimulus_controller}#onSubmitEnd"),
8181
restricted_comment_stimulus_controller("-highlight-class") => "work-packages-activities-tab-journals-new-component--journal-notes-body__restricted-comment", # rubocop:disable Layout/LineLength
8282
restricted_comment_stimulus_controller("-hidden-class") => "d-none",
83-
restricted_comment_stimulus_controller("-#{index_stimulus_controller}-outlet") => "##{wrapper_key}"
83+
restricted_comment_stimulus_controller("-#{index_stimulus_controller}-outlet") => "##{wrapper_key}",
84+
restricted_comment_stimulus_controller("-is-restricted-value") => false # Initial value
8485
}
8586
end
8687

app/components/work_packages/activities_tab/journals/item_component/edit.html.erb

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<%=
2-
component_wrapper(class: "work-packages-activities-tab-journals-item-component-edit") do
2+
component_wrapper(class: "work-packages-activities-tab-journals-item-component-edit", data: wrapper_data_attributes) do
33
render(Primer::Box.new(my: 3)) do
44
primer_form_with(
55
id: "work-package-journal-form-element", # required for specs

app/components/work_packages/activities_tab/journals/item_component/edit.rb

+7
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class ItemComponent::Edit < ApplicationComponent
3333
include ApplicationHelper
3434
include OpPrimer::ComponentHelpers
3535
include OpTurbo::Streamable
36+
include WorkPackages::ActivitiesTab::StimulusControllers
3637

3738
def initialize(journal:, filter:)
3839
super
@@ -49,6 +50,12 @@ def initialize(journal:, filter:)
4950
def wrapper_uniq_by
5051
journal.id
5152
end
53+
54+
def wrapper_data_attributes
55+
{
56+
restricted_comment_stimulus_controller("-is-restricted-value") => journal.restricted?
57+
}
58+
end
5259
end
5360
end
5461
end

app/controllers/work_packages/activities_tab_controller.rb

+18-5
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,13 @@ def update
139139
respond_with_turbo_streams
140140
end
141141

142+
def sanitize_restricted_mentions
143+
render plain: sanitized_journal_notes
144+
rescue StandardError => e
145+
handle_internal_server_error(e)
146+
respond_with_turbo_streams
147+
end
148+
142149
def toggle_reaction # rubocop:disable Metrics/AbcSize
143150
emoji_reaction_service =
144151
if @journal.emoji_reactions.exists?(user: User.current, reaction: params[:reaction])
@@ -221,6 +228,10 @@ def journal_sorting
221228
User.current.preference&.comments_sorting || OpenProject::Configuration.default_comment_sort_order
222229
end
223230

231+
def sanitized_journal_notes
232+
WorkPackages::ActivitiesTab::RestrictedMentionsSanitizer.sanitize(@work_package, journal_params[:notes])
233+
end
234+
224235
def journal_params
225236
params.expect(journal: %i[notes restricted])
226237
end
@@ -295,22 +306,24 @@ def update_index_component
295306
end
296307

297308
def create_journal_service_call
309+
restricted = to_boolean(journal_params[:restricted], false)
310+
notes = restricted ? sanitized_journal_notes : journal_params[:notes]
311+
298312
AddWorkPackageNoteService
299313
.new(user: User.current,
300314
work_package: @work_package)
301-
.call(journal_params[:notes],
315+
.call(notes,
302316
send_notifications: to_boolean(params[:notify], true),
303-
restricted: to_boolean(journal_params[:restricted], false))
317+
restricted:)
304318
end
305319

306320
def to_boolean(value, default)
307321
ActiveRecord::Type::Boolean.new.cast(value.presence || default)
308322
end
309323

310324
def update_journal_service_call
311-
Journals::UpdateService.new(model: @journal, user: User.current).call(
312-
notes: journal_params[:notes]
313-
)
325+
notes = @journal.restricted? ? sanitized_journal_notes : journal_params[:notes]
326+
Journals::UpdateService.new(model: @journal, user: User.current).call(notes:)
314327
end
315328

316329
def generate_time_based_update_streams(last_update_timestamp)

app/forms/work_packages/activities_tab/journals/restricted_note_form.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ class RestrictedNoteForm < ApplicationForm
3636
checked: false,
3737
data: {
3838
"work-packages--activities-tab--restricted-comment-target": "restrictedCheckbox",
39-
action: "input->work-packages--activities-tab--restricted-comment#toggleBackgroundColor"
39+
action: "input->work-packages--activities-tab--restricted-comment#toggleRestriction"
4040
}
4141
)
4242
end

app/models/queries/principals.rb

+1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ module Queries::Principals
3232
filter Filters::TypeFilter
3333
filter Filters::MemberFilter
3434
filter Filters::MentionableOnWorkPackageFilter
35+
filter Filters::RestrictedMentionableOnWorkPackageFilter
3536
filter Filters::StatusFilter
3637
filter Filters::NameFilter
3738
filter Filters::AnyNameAttributeFilter

app/models/queries/principals/filters/mentionable_on_work_package_filter.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def key
4747
end
4848

4949
def human_name
50-
"mentionable" # intenral use
50+
"mentionable" # Only for Internal use, not visible in the UI
5151
end
5252

5353
def apply_to(query_scope)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# frozen_string_literal: true
2+
3+
# -- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
# ++
30+
31+
class Queries::Principals::Filters::RestrictedMentionableOnWorkPackageFilter <
32+
Queries::Principals::Filters::PrincipalFilter
33+
validate :values_are_a_single_work_package_id
34+
35+
def allowed_values
36+
raise NotImplementedError, "There would be too many candidates"
37+
end
38+
39+
def allowed_values_subset
40+
@allowed_values_subset ||= ::WorkPackage.visible
41+
end
42+
43+
def type
44+
:list_optional
45+
end
46+
47+
def key
48+
:restricted_mentionable_on_work_package
49+
end
50+
51+
def human_name
52+
"restricted mentionable" # Only for Internal use, not visible in the UI
53+
end
54+
55+
def apply_to(query_scope)
56+
case operator
57+
when "="
58+
query_scope.where(id: project_members.select(:user_id))
59+
when "!"
60+
query_scope.where.not(id: project_members.select(:user_id))
61+
end
62+
end
63+
64+
def permission
65+
:view_comments_with_restricted_visibility
66+
end
67+
68+
private
69+
70+
def type_strategy
71+
@type_strategy ||= Queries::Filters::Strategies::HugeList.new(self)
72+
end
73+
74+
def values_are_a_single_work_package_id
75+
errors.add(:values, :single_value_requirement) if values.size > 1
76+
end
77+
78+
def project_members
79+
Member.of_project(work_package.project)
80+
.joins(roles: :role_permissions)
81+
.where(role_permissions: { permission: })
82+
end
83+
84+
def work_package
85+
WorkPackage.find(values.first)
86+
end
87+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
class WorkPackages::ActivitiesTab::RestrictedMentionsSanitizer
32+
def self.sanitize(work_package, notes)
33+
new(work_package, notes).call
34+
end
35+
36+
def initialize(work_package, notes)
37+
@work_package = work_package
38+
@notes = notes
39+
end
40+
41+
def call
42+
return "" if notes.blank?
43+
44+
convert_unmentionable_principals_to_plain_text
45+
CGI.unescapeHTML(parser.to_html)
46+
end
47+
48+
private
49+
50+
attr_reader :work_package, :notes
51+
52+
def convert_unmentionable_principals_to_plain_text
53+
mentionable_principals_ids = mentionable_principals.pluck(:id)
54+
55+
parser.css("mention").each do |mention|
56+
unless mentionable_principals_ids.include?(mention["data-id"].to_i)
57+
mention.replace(mention.content)
58+
end
59+
end
60+
end
61+
62+
def parser
63+
@parser ||= Nokogiri::HTML.fragment(notes)
64+
end
65+
66+
def mentionable_principals
67+
@mentionable_principals ||= Queries::Principals::PrincipalQuery.new(user: User.current)
68+
.where(:restricted_mentionable_on_work_package, "=", [work_package.id])
69+
.where(:status, "!", [Principal.statuses[:locked]])
70+
.where(:type, "=", %w[User Group])
71+
.results
72+
end
73+
end

config/initializers/permissions.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@
289289
# FIXME: Although the endpoint is removed, the code checking whether a user
290290
# is eligible to add work packages through the API still seems to rely on this.
291291
journals: [:new],
292-
"work_packages/activities_tab": %i[create toggle_reaction]
292+
"work_packages/activities_tab": %i[create toggle_reaction sanitize_restricted_mentions]
293293
},
294294
permissible_on: %i[work_package project],
295295
dependencies: :view_work_packages

config/locales/en.yml

+4
Original file line numberDiff line numberDiff line change
@@ -1386,6 +1386,10 @@ en:
13861386
values:
13871387
inclusion: "filter has invalid values."
13881388
format: "%{message}"
1389+
queries/principals/filters/restricted_mentionable_on_work_package_filter:
1390+
attributes:
1391+
values:
1392+
single_value_requirement: "must be a single work package"
13891393
relation:
13901394
typed_dag:
13911395
circular_dependency: "The relationship creates a circle of relationships."

config/routes.rb

+2
Original file line numberDiff line numberDiff line change
@@ -646,10 +646,12 @@
646646
get :cancel_edit
647647
put :toggle_reaction
648648
end
649+
649650
collection do
650651
get :update_streams
651652
get :update_filter # filter not persisted
652653
put :update_sorting # sorting is persisted
654+
post :sanitize_restricted_mentions
653655
end
654656
end
655657

frontend/src/app/core/path-helper/apiv3-paths.ts

+20-4
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ export class ApiV3Paths {
4747
filters.add('member', '=', [(workPackage.project as HalResource).id as string]);
4848
} else {
4949
// that are mentionable on the work package
50-
filters.add('mentionable_on_work_package', '=', [workPackage.id.toString()]);
50+
filters.add(
51+
(this.isRestrictedMentionable() ? 'restricted_mentionable_on_work_package' : 'mentionable_on_work_package'),
52+
'=',
53+
[workPackage.id.toString()],
54+
);
5155
}
5256
// That are users:
5357
filters.add('type', '=', ['User', 'Group']);
@@ -57,8 +61,20 @@ export class ApiV3Paths {
5761
filters.add('name', '~', [term]);
5862
}
5963

60-
return `${this.apiV3Base
61-
}/principals?${
62-
filters.toParams({ sortBy: '[["name","asc"]]', offset: '1', pageSize: '10' })}`;
64+
return `${this.apiV3Base}/principals?${filters.toParams({ sortBy: '[["name","asc"]]', offset: '1', pageSize: '10' })}`;
65+
}
66+
67+
/**
68+
* Check if either adding or editing a comment is restricted, and thus
69+
* the mentionable principals are to be restricted
70+
*
71+
* @returns {boolean}
72+
*/
73+
private isRestrictedMentionable():boolean {
74+
const isRestrictedAttributeValue = 'data-work-packages--activities-tab--restricted-comment-is-restricted-value';
75+
const addingCommentIsRestricted = document.getElementById('work-packages-activities-tab-add-comment-component')?.getAttribute(isRestrictedAttributeValue) === 'true';
76+
const editingCommentIsRestricted = document.querySelector('.work-packages-activities-tab-journals-item-component-edit')?.getAttribute(isRestrictedAttributeValue) === 'true';
77+
78+
return addingCommentIsRestricted || editingCommentIsRestricted;
6379
}
6480
}

frontend/src/stimulus/controllers/dynamic/work-packages/activities-tab/quote-comment.controller.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export default class QuoteCommentController extends Controller {
8383
private setCommentRestriction(isRestricted:boolean) {
8484
if (isRestricted && !this.workPackagesActivitiesTabRestrictedCommentOutlet.restrictedCheckboxTarget.checked) {
8585
this.workPackagesActivitiesTabRestrictedCommentOutlet.restrictedCheckboxTarget.checked = isRestricted;
86-
this.workPackagesActivitiesTabRestrictedCommentOutlet.toggleBackgroundColor();
86+
this.workPackagesActivitiesTabRestrictedCommentOutlet.toggleRestriction();
8787
}
8888
}
8989

0 commit comments

Comments
 (0)