Merge remote-tracking branch 'origin/dev' into feature/38520-Sidebar-in-Notification-Center-with-project-filter
commit
fc31d31f6d
@ -0,0 +1,88 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
class WorkPackageMailer < ApplicationMailer |
||||
helper :mail_notification |
||||
|
||||
def mentioned(recipient, journal) |
||||
@user = recipient |
||||
@work_package = journal.journable |
||||
@journal = journal |
||||
|
||||
author = journal.user |
||||
|
||||
User.execute_as author do |
||||
set_work_package_headers(@work_package) |
||||
|
||||
message_id journal, recipient |
||||
|
||||
with_locale_for(recipient) do |
||||
mail to: recipient.mail, |
||||
subject: I18n.t(:'mail.mention.subject', |
||||
user_name: author.name, |
||||
id: @work_package.id, |
||||
subject: @work_package.subject) |
||||
end |
||||
end |
||||
end |
||||
|
||||
def watcher_changed(work_package, user, watcher_changer, action) |
||||
User.execute_as user do |
||||
@work_package = work_package |
||||
@watcher_changer = watcher_changer |
||||
@action = action |
||||
|
||||
set_work_package_headers(work_package) |
||||
message_id work_package, user |
||||
|
||||
with_locale_for(user) do |
||||
mail to: user.mail, subject: subject_for_work_package(work_package) |
||||
end |
||||
end |
||||
end |
||||
|
||||
private |
||||
|
||||
def subject_for_work_package(work_package) |
||||
"#{work_package.project.name} - #{work_package.status.name} #{work_package.type.name} " + |
||||
"##{work_package.id}: #{work_package.subject}" |
||||
end |
||||
|
||||
def set_work_package_headers(work_package) |
||||
open_project_headers 'Project' => work_package.project.identifier, |
||||
'WorkPackage-Id' => work_package.id, |
||||
'WorkPackage-Author' => work_package.author.login, |
||||
'Type' => 'WorkPackage' |
||||
|
||||
if work_package.assigned_to |
||||
open_project_headers 'WorkPackage-Assignee' => work_package.assigned_to.login |
||||
end |
||||
end |
||||
end |
@ -1,11 +1,51 @@ |
||||
class NotificationSetting < ApplicationRecord |
||||
enum channel: { in_app: 0, mail: 1, mail_digest: 2 } |
||||
WATCHED = :watched |
||||
INVOLVED = :involved |
||||
MENTIONED = :mentioned |
||||
WORK_PACKAGE_CREATED = :work_package_created |
||||
WORK_PACKAGE_COMMENTED = :work_package_commented |
||||
WORK_PACKAGE_PROCESSED = :work_package_processed |
||||
WORK_PACKAGE_PRIORITIZED = :work_package_prioritized |
||||
WORK_PACKAGE_SCHEDULED = :work_package_scheduled |
||||
NEWS_ADDED = :news_added |
||||
NEWS_COMMENTED = :news_commented |
||||
DOCUMENT_ADDED = :document_added |
||||
FORUM_MESSAGES = :forum_messages |
||||
WIKI_PAGE_ADDED = :wiki_page_added |
||||
WIKI_PAGE_UPDATED = :wiki_page_updated |
||||
MEMBERSHIP_ADDED = :membership_added |
||||
MEMBERSHIP_UPDATED = :membership_updated |
||||
|
||||
def self.all_settings |
||||
[ |
||||
WATCHED, |
||||
INVOLVED, |
||||
MENTIONED, |
||||
WORK_PACKAGE_CREATED, |
||||
WORK_PACKAGE_COMMENTED, |
||||
WORK_PACKAGE_PROCESSED, |
||||
WORK_PACKAGE_PRIORITIZED, |
||||
WORK_PACKAGE_SCHEDULED, |
||||
*email_settings |
||||
] |
||||
end |
||||
|
||||
def self.email_settings |
||||
[ |
||||
NEWS_ADDED, |
||||
NEWS_COMMENTED, |
||||
DOCUMENT_ADDED, |
||||
FORUM_MESSAGES, |
||||
WIKI_PAGE_ADDED, |
||||
WIKI_PAGE_UPDATED, |
||||
MEMBERSHIP_ADDED, |
||||
MEMBERSHIP_UPDATED |
||||
] |
||||
end |
||||
|
||||
belongs_to :project |
||||
belongs_to :user |
||||
|
||||
include Scopes::Scoped |
||||
scopes :applicable |
||||
|
||||
validates :channel, uniqueness: { scope: %i[project user] } |
||||
end |
||||
|
@ -0,0 +1,144 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
module Users::Scopes |
||||
module HavingReminderMailToSend |
||||
extend ActiveSupport::Concern |
||||
|
||||
class_methods do |
||||
# Returns all users for which a reminder mails should be sent now. A user will be included if: |
||||
# * That user has an unread notification |
||||
# * The user hasn't been informed about the unread notification before |
||||
# * The user has configured reminder mails to be within the time frame between the provided time and now. |
||||
# This assumes that users only have full hours specified for the times they desire |
||||
# to receive a reminder mail at. |
||||
# @param [DateTime] earliest_time The earliest time to consider as a matching slot. All quarter hours from that time |
||||
# to now are included. |
||||
# Only the time part is used which is moved forward to the next quarter hour (e.g. 2021-05-03 10:34:12+02:00 -> 08:45:00). |
||||
# This is done because time zones always have a mod(15) == 0 minutes offset. |
||||
# Needs to be before the current time. |
||||
def having_reminder_mail_to_send(earliest_time) |
||||
local_times = local_times_from(earliest_time) |
||||
|
||||
return none if local_times.empty? |
||||
|
||||
# Left outer join as not all user instances have preferences associated |
||||
# but we still want to select them. |
||||
recipient_candidates = User |
||||
.active |
||||
.left_joins(:preference) |
||||
.joins(local_time_join(local_times)) |
||||
|
||||
subscriber_ids = Notification |
||||
.unsent_reminders_before(recipient: recipient_candidates, time: Time.current) |
||||
.group(:recipient_id) |
||||
.select(:recipient_id) |
||||
|
||||
where(id: subscriber_ids) |
||||
end |
||||
|
||||
def local_time_join(local_times) |
||||
# Joins the times local to the user preferences and then checks whether: |
||||
# * reminders are enabled |
||||
# * any of the configured reminder time is the local time |
||||
# If no time zone is present, utc is assumed. |
||||
# If no reminder settings are present, sending a reminder at 08:00 local time is assumed. |
||||
times_sql = arel_table |
||||
.grouping(Arel::Nodes::ValuesList.new(local_times)) |
||||
.as('t(time, zone, workday)') |
||||
<<~SQL.squish |
||||
JOIN (SELECT * FROM #{times_sql.to_sql}) AS local_times |
||||
ON COALESCE(user_preferences.settings->>'time_zone', 'UTC') = local_times.zone |
||||
AND ( |
||||
user_preferences.settings->'workdays' @> to_jsonb(local_times.workday) |
||||
OR ( |
||||
user_preferences.settings->'workdays' IS NULL |
||||
AND local_times.workday BETWEEN 1 AND 5 |
||||
) |
||||
) |
||||
AND ( |
||||
( |
||||
user_preferences.settings->'daily_reminders'->'times' IS NULL |
||||
AND local_times.time = '08:00:00+00:00' |
||||
) |
||||
OR |
||||
( |
||||
(user_preferences.settings->'daily_reminders'->'enabled')::boolean |
||||
AND user_preferences.settings->'daily_reminders'->'times' ? local_times.time |
||||
) |
||||
) |
||||
SQL |
||||
end |
||||
|
||||
def local_times_from(earliest_time) |
||||
times = quarters_between_earliest_and_now(earliest_time) |
||||
|
||||
times_for_zones(times) |
||||
end |
||||
|
||||
def times_for_zones(times) |
||||
ActiveSupport::TimeZone |
||||
.all |
||||
.map do |z| |
||||
times.map do |time| |
||||
local_time = time.in_time_zone(z) |
||||
|
||||
# Get the iso weekday of the current time to check |
||||
# which users have it enabled as a workday |
||||
workday = local_time.to_date.cwday |
||||
|
||||
# Since only full hours can be configured, we can disregard any local time that is not |
||||
# a full hour. |
||||
next if local_time.min != 0 |
||||
|
||||
[local_time.strftime('%H:00:00+00:00'), z.name.gsub("'", "''"), workday] |
||||
end |
||||
end |
||||
.flatten(1) |
||||
.compact |
||||
end |
||||
|
||||
def quarters_between_earliest_and_now(earliest_time) |
||||
latest_time = Time.current |
||||
raise ArgumentError if latest_time < earliest_time || (latest_time - earliest_time) > 1.day |
||||
|
||||
quarters = ((latest_time - earliest_time) / 60 / 15).floor |
||||
|
||||
(1..quarters).each_with_object([next_quarter_hour(earliest_time)]) do |_, times| |
||||
times << (times.last + 15.minutes) |
||||
end |
||||
end |
||||
|
||||
def next_quarter_hour(time) |
||||
(time + (time.min % 15 == 0 ? 0.minutes : (15 - (time.min % 15)).minutes)) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,43 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Attachments |
||||
class BaseService < ::BaseServices::Create |
||||
## |
||||
# Create an attachment service bypassing the user-provided whitelist |
||||
# for internal purposes such as exporting data. |
||||
# |
||||
# @param user The user to call the service with |
||||
# @param whitelist A custom whitelist to validate with, or empty to disable validation |
||||
# |
||||
# Warning: When passing an empty whitelist, this results in no validations on the content type taking place. |
||||
def self.bypass_whitelist(user:, whitelist: []) |
||||
new(user: user, contract_options: { whitelist: whitelist.map(&:to_s) }) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,31 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
module Journals |
||||
class SetAttributesService < ::BaseServices::SetAttributes; end |
||||
end |
@ -0,0 +1,31 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
module Journals |
||||
class UpdateService < ::BaseServices::Update; end |
||||
end |
@ -0,0 +1,40 @@ |
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
module Notifications::MailService::WorkPackageStrategy |
||||
class << self |
||||
def send_mail(notification) |
||||
return unless notification.reason_mentioned? |
||||
return unless notification.recipient.pref.immediate_reminders[:mentioned] |
||||
|
||||
WorkPackageMailer |
||||
.mentioned(notification.recipient, notification.journal) |
||||
.deliver_later |
||||
end |
||||
end |
||||
end |
@ -1,30 +1,57 @@ |
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="border-bottom: 1px solid #cccccc; margin-bottom: 32px;"> |
||||
<table <%= placeholder_table_styles(width:'100%',style: "width:100%;min-width:100%") %>> |
||||
<tr> |
||||
<td width="100%" style="padding-top: 8px; padding-left: 12px;"> |
||||
<table> |
||||
<td> |
||||
<table <%= placeholder_table_styles %>> |
||||
<tr> |
||||
<td style="font-size: 24px; color: #333333; padding-bottom: 5px;"> |
||||
<%= I18n.t(:'mail.salutation', user: @user.firstname) %> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
<td> |
||||
<table <%= placeholder_table_styles(width:'100%',style: "width:100%") %>> |
||||
<tr> |
||||
<td> |
||||
<span style="font-size: 24px; color: #333333;"> |
||||
<%= I18n.t(:'mail.salutation', user: @user.firstname) %> |
||||
</span> |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<%= placeholder_cell('8px', vertical: false) %> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
<span style="font-size:16px; color: #1A67A3; font-weight: bold;"> |
||||
<%= summary %> |
||||
</span> |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<%= placeholder_cell('24px', vertical: false) %> |
||||
</tr> |
||||
<tr> |
||||
<td> |
||||
<a href="<%= notifications_center_url %>" |
||||
target="_blank" |
||||
style="background: #D1E5F5; padding: 8px 12px; color: #1A67A3; border: 1px solid #1A67A3; border-radius: 16px; text-decoration: none;font-size: 14px;"> |
||||
<%= I18n.t(:'mail.notification.center') %> |
||||
</a> |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<%= placeholder_cell('40px', vertical: false) %> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td style="font-size:16px; color: #1A67A3; font-weight: bold; padding-bottom: 10px;"> |
||||
<%= summary %> |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<td style="padding: 10px 0 32px 0;"> |
||||
<a href="<%= notifications_center_url %>" |
||||
target="_blank" |
||||
style="background: #D1E5F5; padding: 8px 12px; color: #1A67A3; border: 1px solid #1A67A3; border-radius: 16px; text-decoration: none;"> |
||||
<%= I18n.t(:'mail.notification.center') %> |
||||
</a> |
||||
<%= placeholder_cell('16px', vertical: true) %> |
||||
<td style="vertical-align: top;"> |
||||
<table <%= placeholder_table_styles %>> |
||||
<tr> |
||||
<td style="width: 96px; height: 96px;"> |
||||
<%= logo_tag({ alt: "#{Setting.app_title} #{I18n.t(:'mail.logo_alt_text')}", style: "height: 96px;max-width: 240px;"}) %> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
<td style="width: 96px; height: 96px; vertical-align: top;"> |
||||
<%= logo_tag({ alt: "#{Setting.app_title} #{I18n.t(:'mail.logo_alt_text')}", style: "height: 96px;"}) %> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</table> |
@ -1,57 +1,107 @@ |
||||
<a style="border: 1px solid #E0E0E0; |
||||
margin-bottom: 16px; |
||||
padding: 12px 12px 16px 12px; |
||||
border-radius: 10px; |
||||
text-decoration: none; |
||||
display: block;" |
||||
<a style="text-decoration: none;display: block;" |
||||
href="<%= notifications_path(work_package.id) %>" |
||||
target="_blank"> |
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="margin-bottom: 9px; font-size: 14px;"> |
||||
<table <%= placeholder_table_styles(width:'100%',style: "width:100%;border-width:1px;border-color:#E0E0E0;border-style:solid;border-radius:10px") %>> |
||||
<tr> |
||||
<td > |
||||
<div style="color: #333333; |
||||
background-color: #FFFFFF; |
||||
<%= status_colors(work_package.status) %> |
||||
white-space: nowrap; |
||||
padding: 2px 12px; |
||||
height: 16px;"> |
||||
<%= work_package.status %> |
||||
</div> |
||||
</td> |
||||
<td width="100%" style="padding-left: 8px; |
||||
color: #878787;"> |
||||
|
||||
#<%= work_package.id %> - <%= work_package.project %> |
||||
<% unique_reasons = unique_reasons_of_notifications(notifications_by_work_package) %> |
||||
<%= ' - ' unless unique_reasons.length === 1 && unique_reasons.first.nil? %> |
||||
<% unique_reasons.each_with_index do |reason, index| %> |
||||
<%= I18n.t( |
||||
:"mail.digests.work_packages.reason.#{reason || :unknown}", |
||||
default: '') %><%= ', ' unless unique_reasons.size-1 == index %> |
||||
<% end %> |
||||
</td> |
||||
<td style="text-align: right;"> |
||||
<span style="background-color: #00A3FF; |
||||
color: white; |
||||
border-radius: 8px; |
||||
padding: 0px 8px; |
||||
font-size: 14px;"> |
||||
<%= notifications_by_work_package.length %> |
||||
</span> |
||||
</td> |
||||
<%= placeholder_cell('12px', vertical: false) %> |
||||
</tr> |
||||
</table> |
||||
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="margin-bottom: 9px; font-size: 16px; font-weight: bold;"> |
||||
<tr> |
||||
<td style="color: <%= type_color(work_package.type, '#333333') %>; |
||||
white-space: nowrap;"> |
||||
<%= work_package.type.to_s.upcase %> |
||||
</td> |
||||
<td width="100%" style="padding-left: 5px; color: #333333;"> |
||||
<%= work_package.subject %> |
||||
<td> |
||||
<table <%= placeholder_table_styles(width:'100%',style: 'width:100%;font-size:14px;') %>> |
||||
<tr> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
<td> |
||||
<table <%= placeholder_table_styles %>> |
||||
<tr> |
||||
<td> |
||||
<table <%= placeholder_table_styles %>> |
||||
<tr> |
||||
<td style="color: #333333; |
||||
background-color: #FFFFFF; |
||||
<%= status_colors(work_package.status) %> |
||||
white-space: nowrap; |
||||
padding: 2px 12px; |
||||
height: 18px;"> |
||||
<%= work_package.status %> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
<%= placeholder_cell('8px', vertical: true) %> |
||||
<td width="100%" style="color: #878787;"> |
||||
#<%= work_package.id %> - <%= work_package.project %> |
||||
<%= ' - ' unless unique_reasons.length === 1 && unique_reasons.first.nil? %> |
||||
<% unique_reasons.each_with_index do |reason, index| %> |
||||
<%= I18n.t( |
||||
:"mail.work_packages.reason.#{reason || :unknown}", |
||||
default: '') %><%= ', ' unless unique_reasons.size-1 == index %> |
||||
<% end %> |
||||
</td> |
||||
<td> |
||||
<table <%= placeholder_table_styles %>> |
||||
<tr> |
||||
<% if show_count %> |
||||
<td style="background-color: #00A3FF; |
||||
color: white; |
||||
border-radius: 10px; |
||||
padding: 2px 8px; |
||||
font-size: 14px; |
||||
height: 18px; |
||||
line-height: 18px;"> |
||||
<%= count %> |
||||
</td> |
||||
<% end %> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
</tr> |
||||
<tr> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
<%= placeholder_cell('16px', vertical: false) %> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
</tr> |
||||
<tr> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
<td> |
||||
<table <%= placeholder_table_styles(style: 'font-size:16px;font-weight:bold') %>> |
||||
<tr> |
||||
<td style="color: <%= type_color(work_package.type, '#333333') %>;white-space: nowrap;"> |
||||
<%= work_package.type.to_s.upcase %> |
||||
</td> |
||||
<%= placeholder_cell('4px', vertical: true) %> |
||||
<td width="100%" style="color: #333333;"> |
||||
<%= work_package.subject %> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
</tr> |
||||
<tr> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
<%= placeholder_cell('12px', vertical: false) %> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
</tr> |
||||
<tr> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
<td><%= yield %></td> |
||||
<%= placeholder_cell('12px', vertical: true) %> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
</tr> |
||||
<tr> |
||||
<%= placeholder_cell('12px', vertical: false) %> |
||||
</tr> |
||||
</table> |
||||
|
||||
<%= yield %> |
||||
<table> |
||||
<tr> |
||||
<%= placeholder_cell('20px', vertical: false) %> |
||||
</tr> |
||||
</table> |
||||
</a> |
@ -0,0 +1,55 @@ |
||||
<table <%= placeholder_table_styles %>> |
||||
<tr> |
||||
<%= placeholder_cell('20px', vertical: false) %> |
||||
</tr> |
||||
</table> |
||||
|
||||
<table <%= placeholder_table_styles(width:'100%',style: 'width:100%;max-width:700px;margin-left:auto;margin-right:auto') %>> |
||||
<tr> |
||||
<td> |
||||
<%= render partial: 'mailer/notification_mailer_header', |
||||
locals: { |
||||
summary: "#{I18n.t(:'mail.work_packages.mentioned_by', user: @journal.user)}" |
||||
} %> |
||||
|
||||
<%= render layout: 'mailer/notification_row', |
||||
locals: { |
||||
work_package: @work_package, |
||||
unique_reasons: [:mentioned], |
||||
show_count: false |
||||
} do %> |
||||
<table <%= placeholder_table_styles(width:'100%',style: 'width:100%;') %>> |
||||
<tr> |
||||
<td style="color: #878787; line-height: 24px; font-size: 14px; white-space: normal; overflow: hidden; max-width: 100%; width: 100%;"> |
||||
<%= format_text @journal.notes, only_path: false %> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
<% end %> |
||||
|
||||
<table <%= placeholder_table_styles(style: 'font-size:14px;') %>> |
||||
<tr> |
||||
<td> |
||||
<table <%= placeholder_table_styles(width:'100%',style: 'width:100%;') %>> |
||||
<tr> |
||||
<td> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
<%= placeholder_cell('10px', vertical: true) %> |
||||
<td> |
||||
<%= render partial: 'mailer/notification_settings_button' %> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
|
||||
<table> |
||||
<tr> |
||||
<%= placeholder_cell('40px', vertical: false) %> |
||||
</tr> |
||||
</table> |
||||
</td> |
||||
</tr> |
||||
</table> |
||||
|
@ -0,0 +1,13 @@ |
||||
<%= I18n.t(:'mail.salutation', user: @user.firstname) %> |
||||
<%= "#{I18n.t(:'mail.work_packages.mentioned_by', user: @journal.user)}" %> |
||||
<%= "-" * 100 %> |
||||
|
||||
|
||||
<%= "=" * (('# ' + @work_package.id.to_s + @work_package.subject).length + 4) %> |
||||
= #<%= @work_package.id %> <%= @work_package.subject %> = |
||||
<%= "=" * (('# ' + @work_package.id.to_s + @work_package.subject).length + 4) %> |
||||
|
||||
<%= I18n.t(:label_comment_added) %>: |
||||
<%= strip_tags @journal.notes %> |
||||
|
||||
<%= "-" * 100 %> |
@ -0,0 +1,46 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See docs/COPYRIGHT.rdoc for more details. |
||||
#++ |
||||
|
||||
module Notifications |
||||
class ScheduleReminderMailsJob < Cron::CronJob |
||||
# runs every quarter of an hour, so 00:00, 00:15... |
||||
self.cron_expression = '*/15 * * * *' |
||||
|
||||
def perform |
||||
User.having_reminder_mail_to_send(run_at).pluck(:id).each do |user_id| |
||||
Mails::ReminderJob.perform_later(user_id) |
||||
end |
||||
end |
||||
|
||||
def run_at |
||||
self.class.delayed_job.run_at |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,55 @@ |
||||
#-- encoding: UTF-8 |
||||
|
||||
#-- copyright |
||||
# OpenProject is an open source project management software. |
||||
# Copyright (C) 2012-2021 the OpenProject GmbH |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License version 3. |
||||
# |
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: |
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang |
||||
# Copyright (C) 2010-2013 the ChiliProject Team |
||||
# |
||||
# This program is free software; you can redistribute it and/or |
||||
# modify it under the terms of the GNU General Public License |
||||
# as published by the Free Software Foundation; either version 2 |
||||
# of the License, or (at your option) any later version. |
||||
# |
||||
# This program is distributed in the hope that it will be useful, |
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
# GNU General Public License for more details. |
||||
# |
||||
# You should have received a copy of the GNU General Public License |
||||
# along with this program; if not, write to the Free Software |
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
||||
# |
||||
# See COPYRIGHT and LICENSE files for more details. |
||||
#++ |
||||
|
||||
# Because we mark the notifications as read even though they in fact aren't, we do it in a transaction |
||||
# so that the change is rolled back in case of an error. |
||||
module Notifications |
||||
module WithMarkedNotifications |
||||
extend ActiveSupport::Concern |
||||
|
||||
included do |
||||
private |
||||
|
||||
def with_marked_notifications(notification_ids) |
||||
Notification.transaction do |
||||
mark_notifications_sent(notification_ids) |
||||
|
||||
yield |
||||
end |
||||
end |
||||
|
||||
def mark_notifications_sent(notification_ids) |
||||
Notification |
||||
.where(id: Array(notification_ids)) |
||||
.update_all(notification_marked_attribute => true, updated_at: Time.current) |
||||
end |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,9 @@ |
||||
config = Rails.env.production? && Rails.application.config.database_configuration[Rails.env] |
||||
pool_size = config && [OpenProject::Configuration.web_max_threads + 1, config['pool'].to_i].max |
||||
|
||||
# make sure we have enough connections in the pool for each thread and then some |
||||
if pool_size && pool_size > ActiveRecord::Base.connection_pool.size |
||||
Rails.logger.debug { "Increasing database pool size to #{pool_size} to match max threads" } |
||||
|
||||
ActiveRecord::Base.establish_connection config.merge(pool: pool_size) |
||||
end |
@ -0,0 +1,43 @@ |
||||
# Use rack-timeout if we run in clustered mode with at least 2 workers |
||||
# so that workers, should a timeout occur, can be restarted without interruption. |
||||
if OpenProject::Configuration.web_workers >= 2 |
||||
timeout = Integer(ENV['RACK_TIMEOUT_SERVICE_TIMEOUT'].presence || OpenProject::Configuration.web_timeout) |
||||
wait_timeout = Integer(ENV['RACK_TIMEOUT_WAIT_TIMEOUT'].presence || OpenProject::Configuration.web_wait_timeout) |
||||
|
||||
Rails.logger.debug { "Enabling Rack::Timeout (service=#{timeout}s wait=#{wait_timeout}s)" } |
||||
|
||||
Rails.application.config.middleware.insert_before( |
||||
::Rack::Runtime, |
||||
::Rack::Timeout, |
||||
service_timeout: timeout, # time after which a request being served times out |
||||
wait_timeout: wait_timeout, # time after which a request waiting to be served times out |
||||
term_on_timeout: 1 # shut down worker (gracefully) right away on timeout to be restarted |
||||
) |
||||
|
||||
# remove default logger (logging uninteresting extra info with each not timed out request) |
||||
Rack::Timeout.unregister_state_change_observer(:logger) |
||||
|
||||
Rack::Timeout.register_state_change_observer(:wait_timeout_logger) do |env| |
||||
details = env[Rack::Timeout::ENV_INFO_KEY] |
||||
|
||||
if details.state == :timed_out && details.wait.present? |
||||
::OpenProject.logger.error "Request timed out waiting to be served!" |
||||
end |
||||
end |
||||
|
||||
# The timeout itself is already reported so no need to |
||||
# report the generic internal server error too as it doesn't |
||||
# add any more information. Even worse, it's not immediately |
||||
# clear that the two reports are related. |
||||
module SuppressInternalErrorReportOnTimeout |
||||
def op_handle_error(message_or_exception, context = {}) |
||||
return if request && request.env[Rack::Timeout::ENV_INFO_KEY].try(:state) == :timed_out |
||||
|
||||
super |
||||
end |
||||
end |
||||
|
||||
OpenProjectErrorHelper.prepend SuppressInternalErrorReportOnTimeout |
||||
else |
||||
Rails.logger.debug { "Not enabling Rack::Timeout since we are not running in cluster mode with at least 2 workers" } |
||||
end |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue