diff --git a/app/contracts/reminders/base_contract.rb b/app/contracts/reminders/base_contract.rb new file mode 100644 index 000000000000..b539f96f6361 --- /dev/null +++ b/app/contracts/reminders/base_contract.rb @@ -0,0 +1,82 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Reminders + class BaseContract < ::ModelContract + MAX_NOTE_CHARS_LENGTH = 80 + + attribute :creator_id + attribute :remindable_id + attribute :remindable_type + attribute :remind_at + attribute :note + + validate :validate_creator_exists + validate :validate_acting_user + validate :validate_remindable_exists + validate :validate_manage_reminders_permissions + validate :validate_remind_at_is_in_future + validate :validate_note_length + + def self.model = Reminder + + private + + def validate_creator_exists + errors.add :creator, :not_found unless User.exists?(model.creator_id) + end + + def validate_acting_user + errors.add :creator, :invalid unless model.creator_id == user.id + end + + def validate_remindable_exists + errors.add :remindable, :not_found if model.remindable.blank? + end + + def validate_remind_at_is_in_future + if model.remind_at.present? && model.remind_at < Time.current + errors.add :remind_at, :datetime_must_be_in_future + end + end + + def validate_note_length + if model.note.present? && model.note.length > MAX_NOTE_CHARS_LENGTH + errors.add :note, :too_long, count: MAX_NOTE_CHARS_LENGTH + end + end + + def validate_manage_reminders_permissions + return if errors.added?(:remindable, :not_found) + + unless user.allowed_in_project?(:manage_own_reminders, model.remindable.project) + errors.add :base, :error_unauthorized + end + end + end +end diff --git a/app/contracts/reminders/create_contract.rb b/app/contracts/reminders/create_contract.rb new file mode 100644 index 000000000000..6bc3c01eb76a --- /dev/null +++ b/app/contracts/reminders/create_contract.rb @@ -0,0 +1,32 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Reminders + class CreateContract < BaseContract + end +end diff --git a/app/contracts/reminders/delete_contract.rb b/app/contracts/reminders/delete_contract.rb new file mode 100644 index 000000000000..06895868e6c4 --- /dev/null +++ b/app/contracts/reminders/delete_contract.rb @@ -0,0 +1,36 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Reminders + class DeleteContract < ::DeleteContract + delete_permission -> { + # The user can delete the reminder if they created it + model.creator_id == user.id + } + end +end diff --git a/app/contracts/reminders/update_contract.rb b/app/contracts/reminders/update_contract.rb new file mode 100644 index 000000000000..de462cfec17d --- /dev/null +++ b/app/contracts/reminders/update_contract.rb @@ -0,0 +1,41 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Reminders + class UpdateContract < BaseContract + validate :unchangeable_attributes + + private + + def unchangeable_attributes + if model.remindable_changed? || model.creator_id_changed? + errors.add(:base, :unchangeable) + end + end + end +end diff --git a/app/models/concerns/remindable.rb b/app/models/concerns/remindable.rb new file mode 100644 index 000000000000..d55e65bf912b --- /dev/null +++ b/app/models/concerns/remindable.rb @@ -0,0 +1,35 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Remindable + extend ActiveSupport::Concern + + included do + has_many :reminders, as: :remindable, dependent: :destroy + end +end diff --git a/app/models/notification.rb b/app/models/notification.rb index 65dff4c23ab5..e21493cc10db 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -40,17 +40,20 @@ class Notification < ApplicationRecord responsible: 9, date_alert_start_date: 10, date_alert_due_date: 11, - shared: 12 + shared: 12, + reminder: 13 }.freeze - enum reason: REASONS, - _prefix: true + enum :reason, REASONS, prefix: true belongs_to :recipient, class_name: "User" belongs_to :actor, class_name: "User" belongs_to :journal belongs_to :resource, polymorphic: true + has_one :reminder_notification, dependent: :destroy + has_one :reminder, through: :reminder_notification + include Scopes::Scoped scopes :unsent_reminders_before, :mail_reminder_unsent, diff --git a/app/models/reminder.rb b/app/models/reminder.rb new file mode 100644 index 000000000000..1228420fc19d --- /dev/null +++ b/app/models/reminder.rb @@ -0,0 +1,51 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Reminder < ApplicationRecord + belongs_to :remindable, polymorphic: true + belongs_to :creator, class_name: "User" + + has_many :reminder_notifications, dependent: :destroy + has_many :notifications, through: :reminder_notifications + + def unread_notifications? + unread_notifications.exists? + end + + def unread_notifications + notifications.where(read_ian: [false, nil]) + end + + def completed? + completed_at.present? + end + + def scheduled? + job_id.present? && !completed? + end +end diff --git a/app/models/reminder_notification.rb b/app/models/reminder_notification.rb new file mode 100644 index 000000000000..cbdd70905a9b --- /dev/null +++ b/app/models/reminder_notification.rb @@ -0,0 +1,4 @@ +class ReminderNotification < ApplicationRecord + belongs_to :reminder + belongs_to :notification +end diff --git a/app/models/work_package.rb b/app/models/work_package.rb index a1ec03892668..a16611e48b03 100644 --- a/app/models/work_package.rb +++ b/app/models/work_package.rb @@ -41,6 +41,7 @@ class WorkPackage < ApplicationRecord include WorkPackages::Relations include ::Scopes::Scoped include HasMembers + include Remindable include OpenProject::Journal::AttachmentHelper diff --git a/app/services/concerns/reminders/service_helpers.rb b/app/services/concerns/reminders/service_helpers.rb new file mode 100644 index 000000000000..b6e27e2d446a --- /dev/null +++ b/app/services/concerns/reminders/service_helpers.rb @@ -0,0 +1,57 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 Reminders + module ServiceHelpers + extend ActiveSupport::Concern + + def reschedule_reminder(reminder) + destroy_scheduled_reminder_job(reminder) + mark_unread_notifications_as_read_for(reminder) + schedule_new_reminder_job(reminder) + end + + def schedule_new_reminder_job(reminder) + job = Reminders::ScheduleReminderJob.schedule(reminder) + reminder.update_columns(job_id: job.job_id) + end + + def destroy_scheduled_reminder_job(reminder) + return unless reminder.scheduled? + return unless job = GoodJob::Job.find_by(id: reminder.job_id) + + job.destroy unless job.finished? + end + + def mark_unread_notifications_as_read_for(reminder) + return unless reminder.unread_notifications? + + reminder.unread_notifications.update_all(read_ian: true, updated_at: Time.zone.now) + end + end +end diff --git a/app/services/reminders/create_service.rb b/app/services/reminders/create_service.rb new file mode 100644 index 000000000000..340f980a289f --- /dev/null +++ b/app/services/reminders/create_service.rb @@ -0,0 +1,39 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Reminders + class CreateService < ::BaseServices::Create + include Reminders::ServiceHelpers + + def after_perform(service_call) + schedule_new_reminder_job(service_call.result) + + service_call + end + end +end diff --git a/app/services/reminders/delete_service.rb b/app/services/reminders/delete_service.rb new file mode 100644 index 000000000000..8250ffe12f8f --- /dev/null +++ b/app/services/reminders/delete_service.rb @@ -0,0 +1,39 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Reminders + class DeleteService < ::BaseServices::Delete + include Reminders::ServiceHelpers + + def destroy(reminder) + destroy_scheduled_reminder_job(reminder) + mark_unread_notifications_as_read_for(reminder) + reminder.update(completed_at: Time.zone.now) + end + end +end diff --git a/app/services/reminders/set_attributes_service.rb b/app/services/reminders/set_attributes_service.rb new file mode 100644 index 000000000000..c1cbff56aa64 --- /dev/null +++ b/app/services/reminders/set_attributes_service.rb @@ -0,0 +1,32 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Reminders + class SetAttributesService < ::BaseServices::SetAttributes + end +end diff --git a/app/services/reminders/update_service.rb b/app/services/reminders/update_service.rb new file mode 100644 index 000000000000..9a1bd002e1cd --- /dev/null +++ b/app/services/reminders/update_service.rb @@ -0,0 +1,47 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 2012-2024 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 Reminders + class UpdateService < ::BaseServices::Update + include Reminders::ServiceHelpers + + def after_perform(service_call) + reschedule_reminder(service_call.result) if remind_at_changed? + + service_call + end + + private + + def remind_at_changed? + # For some reason reminder.remind_at_changed? returns false + # so we assume a change if remind_at is present in the params (would have passed contract validation) + params[:remind_at].present? + end + end +end diff --git a/app/workers/reminders/schedule_reminder_job.rb b/app/workers/reminders/schedule_reminder_job.rb new file mode 100644 index 000000000000..522dad565bc0 --- /dev/null +++ b/app/workers/reminders/schedule_reminder_job.rb @@ -0,0 +1,65 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 Reminders + class ScheduleReminderJob < ApplicationJob + queue_with_priority :notification + + def self.schedule(reminder) + set(wait_until: reminder.remind_at).perform_later(reminder) + end + + def perform(reminder) + return if reminder.unread_notifications? + + create_notification_service = create_notification_from_reminder(reminder) + + create_notification_service.on_success do |service_result| + ReminderNotification.create!(reminder:, notification: service_result.result) + end + + create_notification_service.on_failure do |service_result| + Rails.logger.error do + "Failed to create notification for reminder #{reminder.id}: #{service_result.message}" + end + end + end + + private + + def create_notification_from_reminder(reminder) + Notifications::CreateService + .new(user: reminder.creator) + .call( + recipient_id: reminder.creator_id, + resource: reminder.remindable, + reason: :reminder + ) + end + end +end diff --git a/config/initializers/permissions.rb b/config/initializers/permissions.rb index 87f571f46a9d..cfb6f057f713 100644 --- a/config/initializers/permissions.rb +++ b/config/initializers/permissions.rb @@ -212,6 +212,11 @@ {}, permissible_on: :project_query, require: :loggedin + + map.permission :manage_own_reminders, + {}, + permissible_on: :project, + require: :member end map.project_module :work_package_tracking, order: 90 do |wpt| diff --git a/config/locales/en.yml b/config/locales/en.yml index 8535ce37aef1..4f88191d792c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1071,6 +1071,7 @@ en: not_an_integer: "is not an integer." not_an_iso_date: "is not a valid date. Required format: YYYY-MM-DD." not_same_project: "doesn't belong to the same project." + datetime_must_be_in_future: "must be in the future." odd: "must be odd." regex_match_failed: "does not match the regular expression %{expression}." regex_invalid: "could not be validated with the associated regular expression." @@ -3214,6 +3215,7 @@ en: permission_select_project_modules: "Select project modules" permission_share_work_packages: "Share work packages" permission_manage_types: "Select types" + permission_manage_own_reminders: "Create own reminders" permission_view_project: "View projects" permission_view_changesets: "View repository revisions in OpenProject" permission_view_commit_author_statistics: "View commit author statistics" diff --git a/config/locales/js-en.yml b/config/locales/js-en.yml index 761b7a5153c6..9ed3ae60a345 100644 --- a/config/locales/js-en.yml +++ b/config/locales/js-en.yml @@ -676,6 +676,7 @@ en: prioritized: "Prioritized" dateAlert: "Date alert" shared: "Shared" + reminder: "Reminder" date_alerts: milestone_date: "Milestone date" overdue: "Overdue" diff --git a/db/migrate/20241119131205_create_reminders.rb b/db/migrate/20241119131205_create_reminders.rb new file mode 100644 index 000000000000..1ad0989de80a --- /dev/null +++ b/db/migrate/20241119131205_create_reminders.rb @@ -0,0 +1,42 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 CreateReminders < ActiveRecord::Migration[7.1] + def change + create_table :reminders do |t| + t.references :remindable, polymorphic: true, null: false + t.references :creator, null: false, foreign_key: { to_table: :users } + t.datetime :remind_at, null: false + t.datetime :completed_at + t.string :job_id + t.text :note + + t.timestamps + end + end +end diff --git a/db/migrate/20241121113638_create_reminder_notifications.rb b/db/migrate/20241121113638_create_reminder_notifications.rb new file mode 100644 index 000000000000..0e92b9dab99e --- /dev/null +++ b/db/migrate/20241121113638_create_reminder_notifications.rb @@ -0,0 +1,42 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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 CreateReminderNotifications < ActiveRecord::Migration[7.1] + def change + create_table :reminder_notifications do |t| + t.references :reminder, foreign_key: true + t.references :notification, foreign_key: true + + t.timestamps + end + + add_index :reminder_notifications, :notification_id, + unique: true, + name: "index_reminder_notifications_unique" + end +end diff --git a/db/migrate/20241129135602_populate_manage_own_reminders_permission.rb b/db/migrate/20241129135602_populate_manage_own_reminders_permission.rb new file mode 100644 index 000000000000..586be7102374 --- /dev/null +++ b/db/migrate/20241129135602_populate_manage_own_reminders_permission.rb @@ -0,0 +1,40 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require Rails.root.join("db/migrate/migration_utils/permission_adder") + +class PopulateManageOwnRemindersPermission < ActiveRecord::Migration[7.1] + def up + ::Migration::MigrationUtils::PermissionAdder + .add(:view_project, + :manage_own_reminders) + end + + # No-op + def down; end +end diff --git a/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.ts b/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.ts index 641983075d1f..70b4f0c8eea2 100644 --- a/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.ts +++ b/frontend/src/app/features/in-app-notifications/center/in-app-notification-center.component.ts @@ -101,6 +101,10 @@ export class InAppNotificationCenterComponent implements OnInit { key: 'shared', title: this.I18n.t('js.notifications.reasons.shared'), }, + { + key: 'reminder', + title: this.I18n.t('js.notifications.reasons.reminder'), + }, ]; selectedFilter = this.reasonMenuItems.find((item) => item.key === this.urlParams.get('name'))?.title; diff --git a/lib/api/v3/notifications/property_factory.rb b/lib/api/v3/notifications/property_factory.rb index f5786d253ef3..5b450656df9b 100644 --- a/lib/api/v3/notifications/property_factory.rb +++ b/lib/api/v3/notifications/property_factory.rb @@ -88,12 +88,7 @@ def concrete_factory_for(notification) end @concrete_factory_for ||= Hash.new do |h, property_key| - h[property_key] = if property_key == "shared" - # for some reasons - # API::V3::Notifications::PropertyFactory.const_defined?(property_key.camelcase) - # returns true for shared only to fail on the constantize later on. - API::V3::Notifications::PropertyFactory::Default - elsif API::V3::Notifications::PropertyFactory.const_defined?(property_key.camelcase) + h[property_key] = if API::V3::Notifications::PropertyFactory.const_defined?(property_key.camelcase, false) "API::V3::Notifications::PropertyFactory::#{property_key.camelcase}".constantize else API::V3::Notifications::PropertyFactory::Default diff --git a/spec/contracts/reminders/base_contract_spec.rb b/spec/contracts/reminders/base_contract_spec.rb new file mode 100644 index 000000000000..1dc9fcd654d4 --- /dev/null +++ b/spec/contracts/reminders/base_contract_spec.rb @@ -0,0 +1,137 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" +require "contracts/shared/model_contract_shared_context" + +RSpec.describe Reminders::BaseContract do + include_context "ModelContract shared context" + + let(:contract) { described_class.new(reminder, user) } + let(:user) { build_stubbed(:admin) } + let(:creator) { user } + let(:reminder) { build_stubbed(:reminder, creator:) } + + before do + User.current = user + allow(User).to receive(:exists?).with(user.id).and_return(true) + end + + describe "admin user" do + it_behaves_like "contract is valid" + end + + describe "non-admin user" do + context "with valid permissions" do + let(:user) { build_stubbed(:user) } + + before do + mock_permissions_for(user) do |mock| + mock.allow_in_project(:manage_own_reminders, project: reminder.remindable.project) + end + end + + it_behaves_like "contract is valid" + end + + context "without valid permissions" do + let(:user) { build_stubbed(:user) } + + it_behaves_like "contract is invalid", base: :error_unauthorized + end + end + + describe "validate creator exists" do + context "when creator does not exist" do + before { allow(User).to receive(:exists?).with(user.id).and_return(false) } + + it_behaves_like "contract is invalid", creator: :not_found + end + end + + describe "validate acting user" do + context "when the current user is different from the remindable acting user" do + let(:different_user) { build_stubbed(:user) } + + before do + allow(User).to receive(:exists?).with(different_user.id).and_return(true) + reminder.creator = different_user + end + + it_behaves_like "contract is invalid", creator: :invalid + end + end + + describe "validate remindable object" do + context "when remindable is blank" do + before { reminder.remindable = nil } + + it_behaves_like "contract is invalid", remindable: :not_found + end + + context "when remindable is a work package" do + let(:work_package) { build_stubbed(:work_package) } + + before { reminder.remindable = work_package } + + it_behaves_like "contract is valid" + end + end + + describe "validate remind at is in future" do + context "when remind at is in the past" do + before { reminder.remind_at = 1.day.ago } + + it_behaves_like "contract is invalid", remind_at: :datetime_must_be_in_future + end + + context "when remind at is in the future" do + before { reminder.remind_at = 1.day.from_now } + + it_behaves_like "contract is valid" + end + end + + describe "validate note length" do + context "when note is too long" do + before { reminder.note = "a" * (described_class::MAX_NOTE_CHARS_LENGTH + 1) } + + it_behaves_like "contract is invalid", note: :too_long + end + + context "when note is within the limit" do + before { reminder.note = "a" * described_class::MAX_NOTE_CHARS_LENGTH } + + it_behaves_like "contract is valid" + end + end + + include_examples "contract reuses the model errors" +end diff --git a/spec/contracts/reminders/delete_contract_spec.rb b/spec/contracts/reminders/delete_contract_spec.rb new file mode 100644 index 000000000000..2d884059023b --- /dev/null +++ b/spec/contracts/reminders/delete_contract_spec.rb @@ -0,0 +1,48 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" +require "contracts/shared/model_contract_shared_context" + +RSpec.describe Reminders::DeleteContract do + include_context "ModelContract shared context" + + let(:contract) { described_class.new(reminder, current_user) } + let(:current_user) { build_stubbed(:admin) } + let(:reminder) { build_stubbed(:reminder, creator: current_user) } + + context "when user is different from the one that created the reminder" do + let(:another_user) { build_stubbed(:admin) } + + before { reminder.creator = another_user } + + it_behaves_like "contract user is unauthorized" + end + + include_examples "contract reuses the model errors" +end diff --git a/spec/contracts/reminders/update_contract_spec.rb b/spec/contracts/reminders/update_contract_spec.rb new file mode 100644 index 000000000000..3f58ed19b0d2 --- /dev/null +++ b/spec/contracts/reminders/update_contract_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" +require "contracts/shared/model_contract_shared_context" + +RSpec.describe Reminders::UpdateContract do + include_context "ModelContract shared context" + + let(:contract) { described_class.new(reminder, user) } + let(:user) { build_stubbed(:admin) } + let(:creator) { user } + let(:reminder) { build_stubbed(:reminder, creator:) } + + before do + User.current = user + allow(User).to receive(:exists?).with(user.id).and_return(true) + end + + describe "validate unchangeable attributes" do + context "when remindable changed" do + before do + reminder.remindable = build_stubbed(:work_package) + end + + it_behaves_like "contract is invalid", base: :unchangeable + end + + context "when creator_id changed" do + before do + new_creator = build_stubbed(:user) + reminder.creator = new_creator + allow(User).to receive(:exists?).with(new_creator.id).and_return(true) + end + + it_behaves_like "contract is invalid", base: :unchangeable + end + end + + include_examples "contract reuses the model errors" +end diff --git a/spec/factories/reminders_factory.rb b/spec/factories/reminders_factory.rb new file mode 100644 index 000000000000..0c195743a1e9 --- /dev/null +++ b/spec/factories/reminders_factory.rb @@ -0,0 +1,62 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +FactoryBot.define do + factory :reminder do + remindable factory: :work_package + creator factory: :user + remind_at { 1.day.from_now } + note { "This is a reminder" } + + trait :scheduled do + job_id { SecureRandom.uuid } + end + + trait :completed do + job_id { SecureRandom.uuid } + completed_at { Time.zone.now } + end + + trait :with_unread_notifications do + after(:create) do |reminder| + create(:reminder_notification, reminder:, notification: create(:notification, read_ian: false)) + end + end + + trait :with_read_notifications do + after(:create) do |reminder| + create(:reminder_notification, reminder:, notification: create(:notification, read_ian: true)) + end + end + end + + factory :reminder_notification do + reminder + notification + end +end diff --git a/spec/models/concerns/remindable_spec.rb b/spec/models/concerns/remindable_spec.rb new file mode 100644 index 000000000000..c86420aa8f73 --- /dev/null +++ b/spec/models/concerns/remindable_spec.rb @@ -0,0 +1,37 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" + +RSpec.describe Remindable do + let(:remindable) { build_stubbed(:work_package) } + + describe "Associations" do + it { expect(remindable).to have_many(:reminders) } + end +end diff --git a/spec/models/notification_spec.rb b/spec/models/notification_spec.rb index 7e9c73044222..c21c920e2617 100644 --- a/spec/models/notification_spec.rb +++ b/spec/models/notification_spec.rb @@ -28,6 +28,40 @@ require "spec_helper" RSpec.describe Notification do + describe "Associations" do + it { is_expected.to belong_to(:journal) } + it { is_expected.to belong_to(:resource) } + it { is_expected.to belong_to(:actor).class_name("User") } + it { is_expected.to belong_to(:recipient).class_name("User") } + + it { is_expected.to have_one(:reminder_notification).dependent(:destroy) } + it { is_expected.to have_one(:reminder).through(:reminder_notification) } + end + + describe "Enums" do + it do + expect(subject).to define_enum_for(:reason) + .with_values( + mentioned: 0, + assigned: 1, + watched: 2, + subscribed: 3, + commented: 4, + created: 5, + processed: 6, + prioritized: 7, + scheduled: 8, + responsible: 9, + date_alert_start_date: 10, + date_alert_due_date: 11, + shared: 12, + reminder: 13 + ) + .with_prefix + .backed_by_column_of_type(:integer) + end + end + describe ".save" do context "for a non existing journal (e.g. because it has been deleted)" do let(:notification) { build(:notification) } diff --git a/spec/models/reminder_notification_spec.rb b/spec/models/reminder_notification_spec.rb new file mode 100644 index 000000000000..95c8193813c2 --- /dev/null +++ b/spec/models/reminder_notification_spec.rb @@ -0,0 +1,36 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" + +RSpec.describe ReminderNotification do + describe "Associations" do + it { is_expected.to belong_to(:reminder) } + it { is_expected.to belong_to(:notification) } + end +end diff --git a/spec/models/reminder_spec.rb b/spec/models/reminder_spec.rb new file mode 100644 index 000000000000..3c3a9c675a7a --- /dev/null +++ b/spec/models/reminder_spec.rb @@ -0,0 +1,95 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" + +RSpec.describe Reminder do + describe "Associations" do + it { is_expected.to belong_to(:remindable) } + it { is_expected.to belong_to(:creator).class_name("User") } + it { is_expected.to have_many(:reminder_notifications).dependent(:destroy) } + it { is_expected.to have_many(:notifications).through(:reminder_notifications) } + end + + describe "#unread_notifications?" do + context "with an unread notification" do + subject { create(:reminder, :with_unread_notifications) } + + it { is_expected.to be_an_unread_notification } + it { expect(subject.unread_notifications).to be_present } + end + + context "with no unread notifications" do + subject { create(:reminder, :with_read_notifications) } + + it { is_expected.not_to be_an_unread_notification } + it { expect(subject.unread_notifications).to be_empty } + end + + context "with no notifications" do + subject { build_stubbed(:reminder) } + + it { is_expected.not_to be_an_unread_notification } + it { expect(subject.unread_notifications).to be_empty } + end + end + + describe "#scheduled?" do + it "returns true if job_id is present" do + reminder = build_stubbed(:reminder, :scheduled) + + expect(reminder).to be_scheduled + end + + it "returns false if job_id is not present" do + reminder = build(:reminder, job_id: nil) + + expect(reminder).not_to be_scheduled + end + + it "returns false if completed_at is present" do + reminder = build(:reminder, :scheduled, :completed) + + expect(reminder).not_to be_scheduled + end + end + + describe "#completed?" do + it "returns true if completed_at is present" do + reminder = build(:reminder, :completed) + + expect(reminder).to be_completed + end + + it "returns false if completed_at is not present" do + reminder = build(:reminder, completed_at: nil) + + expect(reminder).not_to be_completed + end + end +end diff --git a/spec/services/reminders/create_service_spec.rb b/spec/services/reminders/create_service_spec.rb new file mode 100644 index 000000000000..b220ee90aa75 --- /dev/null +++ b/spec/services/reminders/create_service_spec.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" +require "services/base_services/behaves_like_create_service" + +RSpec.describe Reminders::CreateService do + it_behaves_like "BaseServices create service" do + let(:factory) { :reminder } + + before do + allow(model_instance).to receive(:update_columns).and_return(true) + allow(Reminders::ScheduleReminderJob).to receive(:schedule) + .with(model_instance) + .and_return(instance_double(Reminders::ScheduleReminderJob, job_id: 1)) + end + + it "schedules a reminder job" do + subject + + expect(Reminders::ScheduleReminderJob).to have_received(:schedule).with(model_instance) + expect(model_instance).to have_received(:update_columns).with(job_id: 1) + end + end +end diff --git a/spec/services/reminders/delete_service_spec.rb b/spec/services/reminders/delete_service_spec.rb new file mode 100644 index 000000000000..40607fecb3c9 --- /dev/null +++ b/spec/services/reminders/delete_service_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" +require "services/base_services/behaves_like_delete_service" + +RSpec.describe Reminders::DeleteService do + it_behaves_like "BaseServices delete service" do + let(:factory) { :reminder } + + before do + allow(model_instance).to receive(:update).and_return(model_destroy_result) + end + end + + describe "Remove reminder" do + subject { described_class.new(user:, model: model_instance).call } + + let(:model_instance) { create(:reminder, :scheduled, :with_unread_notifications, creator: user) } + let(:user) { create(:admin) } + + context "with an existing unfinished scheduled job" do + let(:job) { instance_double(GoodJob::Job, finished?: false, destroy: true) } + + before do + model_instance.update(job_id: 1, completed_at: nil) + allow(GoodJob::Job).to receive(:find_by).and_return(job) + end + + it "completes the reminder" do + expect { subject }.to change(model_instance, :completed_at).from(nil) + + aggregate_failures "destroy existing job" do + expect(GoodJob::Job).to have_received(:find_by).with(id: "1") + expect(job).to have_received(:destroy) + end + + aggregate_failures "marks unread notifications as read" do + expect(model_instance.notifications.count).to eq(1) + expect(model_instance.unread_notifications.count).to eq(0) + end + + aggregate_failures "marks the reminder as complete" do + expect(model_instance).to be_completed + end + end + end + + context "with an existing finished scheduled job" do + let(:job) { instance_double(GoodJob::Job, finished?: true, destroy: true) } + + before do + model_instance.update(job_id: 1, completed_at: nil) + allow(GoodJob::Job).to receive(:find_by).and_return(job) + end + + it "completes the reminder" do + expect { subject }.to change(model_instance, :completed_at).from(nil) + + aggregate_failures "does NOT destroy existing job" do + expect(GoodJob::Job).to have_received(:find_by).with(id: "1") + expect(job).not_to have_received(:destroy) + end + + aggregate_failures "marks the reminder as complete" do + expect(model_instance).to be_completed + end + end + end + end +end diff --git a/spec/services/reminders/update_service_spec.rb b/spec/services/reminders/update_service_spec.rb new file mode 100644 index 000000000000..be14bd68b154 --- /dev/null +++ b/spec/services/reminders/update_service_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" +require "services/base_services/behaves_like_update_service" + +RSpec.describe Reminders::UpdateService do + it_behaves_like "BaseServices update service" do + let(:factory) { :reminder } + end + + describe "remind_at changed" do + subject { described_class.new(user:, model: model_instance).call(call_attributes) } + + let(:model_instance) { create(:reminder, :scheduled, :with_unread_notifications, creator: user) } + let(:user) { create(:admin) } + let(:call_attributes) { { remind_at: 2.days.from_now } } + + before do + model_instance.update(job_id: 1) + allow(Reminders::ScheduleReminderJob).to receive(:schedule) + .with(model_instance) + .and_return(instance_double(Reminders::ScheduleReminderJob, job_id: 2)) + end + + context "with an existing unfinished scheduled job" do + let(:job) { instance_double(GoodJob::Job, finished?: false, destroy: true) } + + before do + allow(GoodJob::Job).to receive(:find_by).and_return(job) + end + + it "reschedules the reminder" do + expect { subject }.to change(model_instance, :job_id).from("1").to("2") + + aggregate_failures "destroy existing job" do + expect(GoodJob::Job).to have_received(:find_by).with(id: "1") + expect(job).to have_received(:destroy) + end + + aggregate_failures "marks unread notifications as read" do + expect(model_instance.notifications.count).to eq(1) + expect(model_instance.unread_notifications.count).to eq(0) + end + + aggregate_failures "schedule new job" do + expect(model_instance.remind_at.to_i).to eq(call_attributes[:remind_at].to_i) + expect(Reminders::ScheduleReminderJob).to have_received(:schedule).with(model_instance) + end + end + end + + context "with an existing finished scheduled job" do + let(:job) { instance_double(GoodJob::Job, finished?: true, destroy: true) } + + before do + allow(GoodJob::Job).to receive(:find_by).and_return(job) + end + + it "schedules a new job" do + expect { subject }.to change(model_instance, :job_id).from("1").to("2") + + aggregate_failures "does NOT destroy existing job" do + expect(GoodJob::Job).to have_received(:find_by).with(id: "1") + expect(job).not_to have_received(:destroy) + end + + aggregate_failures "schedule new job" do + expect(model_instance.remind_at.to_i).to eq(call_attributes[:remind_at].to_i) + expect(Reminders::ScheduleReminderJob).to have_received(:schedule).with(model_instance) + end + end + end + + context "with remind_at attribute in non-utc timezone" do + let(:call_attributes) { { remind_at: 2.days.from_now.in_time_zone("Africa/Nairobi") } } + + it "reschedules the reminder" do + expect { subject }.to change(model_instance, :job_id).from("1").to("2") + + aggregate_failures "schedule new job" do + expect(model_instance.remind_at.to_i).to eq(call_attributes[:remind_at].to_i) + expect(Reminders::ScheduleReminderJob).to have_received(:schedule).with(model_instance) + end + end + end + end + + describe "unchangeable attributes" do + let(:original_creator) { create(:user) } + let(:original_remindable) { create(:work_package) } + let(:model_instance) { create(:reminder, creator: original_creator, remindable: original_remindable) } + + context "when attempting to update the creator" do + subject { described_class.new(user: model_instance.creator, model: model_instance).call(creator: another_user) } + + let(:another_user) { create(:user) } + + it "does not update the creator", :aggregate_failures do + update_svc = subject + + expect(update_svc).to be_a_failure + expect(update_svc.result.reload.creator).to eq(original_creator) + expect(update_svc.message).to eq("Creator is invalid. may not be accessed. cannot be changed.") + end + end + + context "when attempting to update the remindable" do + subject { described_class.new(user: model_instance.creator, model: model_instance).call(remindable: another_remindable) } + + let(:another_remindable) { create(:work_package) } + + it "does not update the remindable", :aggregate_failures do + update_svc = subject + + expect(update_svc).to be_a_failure + expect(update_svc.result.reload.remindable).to eq(original_remindable) + expect(update_svc.message).to include("cannot be changed.") + end + end + end +end diff --git a/spec/workers/reminders/schedule_reminder_job_spec.rb b/spec/workers/reminders/schedule_reminder_job_spec.rb new file mode 100644 index 000000000000..2928e20cb68a --- /dev/null +++ b/spec/workers/reminders/schedule_reminder_job_spec.rb @@ -0,0 +1,77 @@ +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) 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. +#++ + +require "spec_helper" + +RSpec.describe Reminders::ScheduleReminderJob do + describe ".schedule" do + let(:reminder) { create(:reminder) } + + subject { described_class.schedule(reminder) } + + it "enqueues a ScheduleReminderJob" do + expect { subject } + .to have_enqueued_job(described_class) + .at(reminder.remind_at) + .with(reminder) + end + end + + describe "#perform" do + let(:reminder) { create(:reminder) } + + subject { described_class.new.perform(reminder) } + + it "creates a notification from the reminder" do + notification_svc = nil + expect { notification_svc = subject }.to change(Notification, :count).by(1) & change(ReminderNotification, :count).by(1) + + aggregate_failures "notification attributes" do + notification = notification_svc.result + + expect(notification.recipient_id).to eq(reminder.creator_id) + expect(notification.resource).to eq(reminder.remindable) + expect(notification.reason).to eq("reminder") + end + + aggregate_failures "marks reminder as having unread notifications" do + expect(reminder.reload).to be_an_unread_notification + end + end + + context "when the reminder is already notified" do + before do + create(:reminder_notification, reminder: reminder, notification: create(:notification, read_ian: false)) + end + + it "does not create a notification from the reminder" do + expect { subject }.not_to change(Notification, :count) + end + end + end +end