Skip to content

Commit ab32cab

Browse files
authored
Merge pull request #23337 from opf/bug/74762-numeric-id-in-the-email-notification-after-adding-watchers
bug/74762 Numeric ID in the email notification after adding watchers
2 parents 3ea8542 + 3036e85 commit ab32cab

26 files changed

Lines changed: 1363 additions & 150 deletions
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
# Pin the external rendering channel so mailer templates never have to
32+
# remember the `render_mode:` / `only_path:` / `static_html:` combination.
33+
# Matching the `.html.erb` / `.text.erb` extension to the helper name keeps
34+
# caller intent visible.
35+
module MailFormattingHelper
36+
def format_mail_html(*, **)
37+
format_text(*, render_mode: :external_html, **)
38+
end
39+
40+
def format_mail_text(*, **)
41+
format_text(*, render_mode: :external_text, **)
42+
end
43+
end

app/mailers/application_mailer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class ApplicationMailer < ActionMailer::Base
3434
helper :application, # for format_text
3535
:work_packages, # for css classes
3636
:custom_fields, # for show_value
37+
:mail_formatting, # for format_mail_html / format_mail_text
3738
:mail_layout # for layouting
3839

3940
include OpenProject::LocaleHelper

app/models/work_package/semantic_identifier.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ def self.numeric_id?(value)
121121
end
122122
end
123123

124+
# Returns formatted value for inline UI display.
125+
# * Semantic mode: "PROJ-42" (no prefix — self-describing)
126+
# * Classic mode: "#42" (hash-prefixed)
127+
def self.format_display_id(display_id)
128+
display_id.is_a?(String) && display_id.match?(/[A-Za-z]/) ? display_id : "##{display_id}"
129+
end
130+
124131
# Returns the user-facing identifier for this work package.
125132
# In semantic mode: the project-based identifier (e.g. "PROJ-42")
126133
# In classic mode: the numeric database ID
@@ -134,8 +141,7 @@ def display_id
134141
# Semantic mode: "PROJ-42" (no prefix — self-describing)
135142
# Classic mode: "#42" (hash-prefixed)
136143
def formatted_id
137-
did = display_id
138-
did.is_a?(String) && did.match?(/[A-Za-z]/) ? did : "##{did}"
144+
WorkPackage::SemanticIdentifier.format_display_id(display_id)
139145
end
140146

141147
# Override ActiveRecord's default `to_param` so Rails URL helpers

app/services/work_packages/update_ancestors_service.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,8 @@ def derive_attributes(work_package, loader, attributes)
123123

124124
def set_journal_note(work_packages)
125125
work_packages.each do |wp|
126-
wp.journal_notes = I18n.t("work_package.updated_automatically_by_child_changes", child: "##{initiator_work_package.id}")
126+
wp.journal_notes = I18n.t("work_package.updated_automatically_by_child_changes",
127+
child: "##{initiator_work_package.id}")
127128
end
128129
end
129130

app/views/work_package_mailer/_work_package_details.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ See COPYRIGHT and LICENSE files for more details.
4747
<% end %>
4848
</ul>
4949

50-
<%= format_text(work_package, :description, only_path: false) %>
50+
<%= format_mail_html(work_package, :description) %>

app/views/work_package_mailer/mentioned.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
<table <%= placeholder_table_styles(width: "100%", style: "width:100%;") %>>
2727
<tr>
2828
<td style="<%= placeholder_text_styles %>">
29-
<%= format_text @journal.notes, only_path: false %>
29+
<%= format_mail_html @journal.notes %>
3030
</td>
3131
</tr>
3232
</table>

app/views/work_package_mailer/mentioned.text.erb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
<%= "=" * ((@work_package.formatted_id + " " + @work_package.subject).length + 4) %>
99

1010
<%= I18n.t(:label_comment_added) %>:
11-
<%= strip_tags @journal.notes %>
11+
<%= format_mail_text(
12+
@journal.notes,
13+
object: @work_package,
14+
project: @work_package.project
15+
) %>
1216

1317
<%= "-" * 100 %>

app/views/work_package_mailer/watcher_changed.html.erb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,8 @@ See COPYRIGHT and LICENSE files for more details.
3030
<hr>
3131
<%= render partial: "work_package_details", locals: { work_package: @work_package } %>
3232
<p>
33-
<%= format_text(
33+
<%= format_mail_html(
3434
t(:text_latest_note, note: last_work_package_note(@work_package)),
35-
only_path: false,
3635
object: @work_package,
3736
project: @work_package.project
3837
) %>

app/views/work_package_mailer/watcher_changed.text.erb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,8 @@ See COPYRIGHT and LICENSE files for more details.
3131

3232
----------------------------------------
3333
<%= render partial: "work_package_details", locals: { work_package: @work_package } %>
34-
<%= t(:text_latest_note, note: last_work_package_note(@work_package)) %>
34+
<%= format_mail_text(
35+
t(:text_latest_note, note: last_work_package_note(@work_package)),
36+
object: @work_package,
37+
project: @work_package.project
38+
) %>

lib/open_project/text_formatting.rb

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,36 @@ module TextFormatting
4343

4444
# @!macro format_text_params
4545
# @param [Project] project a Project context.
46-
# @param [Boolean] only_path whether to generate links with relative URLs.
46+
# @param [Symbol] render_mode the rendering channel (`:in_app_html`,
47+
# `:external_html`, `:external_text`). Resolves the `only_path`,
48+
# `static_html` and `plain_text` context flags as a set. Prefer this
49+
# over passing the primitives individually. See {RenderMode}.
50+
# @param [Boolean] only_path explicit override for the resolved `only_path`.
4751
# @param [User] current_user the current user context.
4852
# @param [:plain, :rich] format the text format.
4953
# `:plain` will return plain text.
5054
# `:rich` will render raw Markdown as HTML.
5155
# @param ** [Hash] additional context to pass to the underlying rendering
52-
# pipeline.
56+
# pipeline. Explicit `static_html:` / `plain_text:` here override the
57+
# values resolved from `render_mode:`.
5358

5459
# rubocop:disable Layout/LineLength
5560

5661
##
5762
# Formats text according to system settings and provided params.
5863
#
59-
# @overload format_text(text, object: nil, project: @project || object.try(:project), only_path: true, current_user: User.current, format: :rich, **)
64+
# @overload format_text(text, object: nil, project: @project || object.try(:project), render_mode: :in_app_html, only_path: nil, current_user: User.current, format: :rich, **)
6065
#
6166
# @param [String] text the raw text to be formatted, typically Markdown.
6267
# @param [Object] object an object context.
6368
# @macro format_text_params
6469
#
6570
# @example Setting a project context explicitly
6671
# format_text("## Hello world", project: current_project)
67-
# @example Generating links with full URLs
68-
# format_text("[Projects](/projects)", only_path: false)
72+
# @example Rendering for an external surface (mailer, RSS, export)
73+
# format_text("see #42", render_mode: :external_html)
6974
#
70-
# @overload format_text(object, attribute, project: @project || object.try(:project), only_path: true, current_user: User.current, format: :rich, **)
75+
# @overload format_text(object, attribute, project: @project || object.try(:project), render_mode: :in_app_html, only_path: nil, current_user: User.current, format: :rich, **)
7176
#
7277
# @param [Object] object an object, typically a model
7378
# (i.e. `ActiveRecord::Base` descendent).
@@ -79,7 +84,8 @@ module TextFormatting
7984
# format_text(issue, :description, options)
8085
#
8186
# @return [String] the formatted text as an HTML-safe String.
82-
def format_text(*args, object: nil, project: nil, only_path: true, current_user: User.current, format: :rich, **)
87+
def format_text(*args, object: nil, project: nil, render_mode: :in_app_html,
88+
only_path: nil, current_user: User.current, format: :rich, **kwargs)
8389
case args.size
8490
when 1
8591
attribute = nil
@@ -94,15 +100,22 @@ def format_text(*args, object: nil, project: nil, only_path: true, current_user:
94100

95101
project ||= @project || object.try(:project)
96102

103+
resolved = RenderMode.resolve(
104+
render_mode,
105+
only_path:,
106+
static_html: kwargs.delete(:static_html),
107+
plain_text: kwargs.delete(:plain_text)
108+
)
109+
97110
Renderer.format_text(
98111
text,
99-
**,
112+
**kwargs,
100113
format:,
101114
object:,
102115
request: try(:request),
103116
current_user:,
104117
attribute:,
105-
only_path:,
118+
**resolved,
106119
project:
107120
)
108121
end

0 commit comments

Comments
 (0)