From 2175ea3b3fd3af394c21a276ff6b6d4255125100 Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Wed, 13 Aug 2025 15:20:07 +0200 Subject: [PATCH] Improve implementation of team automations observers [SCI-12213] --- app/controllers/application_controller.rb | 17 +++--- app/controllers/my_modules_controller.rb | 2 +- app/controllers/teams_controller.rb | 2 +- app/helpers/teams_helper.rb | 2 +- .../vue/team_automations/container.vue | 25 ++++++--- app/models/asset.rb | 4 -- app/models/checklist.rb | 6 --- app/models/checklist_item.rb | 5 -- app/models/concerns/observable_model.rb | 19 +++++-- app/models/current.rb | 5 ++ app/models/experiment.rb | 7 --- app/models/form_field_value.rb | 5 +- app/models/my_module.rb | 12 +---- app/models/protocol.rb | 1 + app/models/result.rb | 8 +-- app/models/step.rb | 12 ----- app/models/step_comment.rb | 8 ++- app/models/step_orderable_element.rb | 12 +++-- app/models/step_text.rb | 8 +-- app/models/table.rb | 4 -- ...all_completed_steps_automation_observer.rb | 30 ----------- ...ll_experiments_done_automation_observer.rb | 29 ---------- .../all_experiments_done_observer.rb | 28 ++++++++++ ...all_my_modules_done_automation_observer.rb | 30 ----------- .../all_steps_completion_observer.rb | 30 +++++++++++ .../all_tasks_done_observer.rb | 27 ++++++++++ .../automation_observers/base_observer.rb | 9 ++++ ...mpleted_step_change_automation_observer.rb | 30 ----------- ...iment_status_change_automation_observer.rb | 29 ---------- .../experiment_status_change_observer.rb | 30 +++++++++++ ...odule_status_change_automation_observer.rb | 31 ----------- ...col_content_changed_automation_observer.rb | 33 ------------ .../result_create_automation_observer.rb | 30 ----------- .../result_create_observer.rb | 26 +++++++++ .../step_completion_observer.rb | 27 ++++++++++ .../task_protocol_content_change_observer.rb | 54 +++++++++++++++++++ .../task_status_change_observer.rb | 26 +++++++++ config/initializers/extends.rb | 40 +++++++++----- config/locales/en.yml | 22 ++++---- spec/models/step_comment_spec.rb | 4 -- 40 files changed, 366 insertions(+), 363 deletions(-) create mode 100644 app/models/current.rb delete mode 100644 app/services/automation_observers/all_completed_steps_automation_observer.rb delete mode 100644 app/services/automation_observers/all_experiments_done_automation_observer.rb create mode 100644 app/services/automation_observers/all_experiments_done_observer.rb delete mode 100644 app/services/automation_observers/all_my_modules_done_automation_observer.rb create mode 100644 app/services/automation_observers/all_steps_completion_observer.rb create mode 100644 app/services/automation_observers/all_tasks_done_observer.rb create mode 100644 app/services/automation_observers/base_observer.rb delete mode 100644 app/services/automation_observers/completed_step_change_automation_observer.rb delete mode 100644 app/services/automation_observers/experiment_status_change_automation_observer.rb create mode 100644 app/services/automation_observers/experiment_status_change_observer.rb delete mode 100644 app/services/automation_observers/my_module_status_change_automation_observer.rb delete mode 100644 app/services/automation_observers/protocol_content_changed_automation_observer.rb delete mode 100644 app/services/automation_observers/result_create_automation_observer.rb create mode 100644 app/services/automation_observers/result_create_observer.rb create mode 100644 app/services/automation_observers/step_completion_observer.rb create mode 100644 app/services/automation_observers/task_protocol_content_change_observer.rb create mode 100644 app/services/automation_observers/task_status_change_observer.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 49fde23a6..93cbfd507 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,7 +4,7 @@ class ApplicationController < ActionController::Base protect_from_forgery with: :exception, prepend: true before_action :authenticate_user! helper_method :current_team - before_action :update_current_team, if: :user_signed_in? + before_action :set_current_team, if: :user_signed_in? around_action :set_date_format, if: :user_signed_in? around_action :set_time_zone, if: :current_user layout 'main' @@ -29,11 +29,6 @@ class ApplicationController < ActionController::Base controller_name == 'projects' && action_name == 'index' end - # Sets current team for all controllers - def current_team - @current_team ||= current_user.teams.find_by(id: current_user.current_team_id) - end - def to_user_date_format ts = I18n.l(Time.parse(params[:timestamp]), format: params[:ts_format].to_sym) @@ -87,14 +82,18 @@ class ApplicationController < ActionController::Base private - def update_current_team - return if current_team.present? && current_team.id == current_user.current_team_id + def current_team + @current_team ||= current_user.teams.find_by(id: current_user.current_team_id) + end + def set_current_team if current_user.current_team_id @current_team = current_user.teams.find_by(id: current_user.current_team_id) - elsif current_user.teams.any? + elsif current_user.teams.first.present? + @current_team = current_user.teams.first current_user.update(current_team_id: current_user.teams.first.id) end + Current.team = @current_team if @current_team.present? end # With this Devise callback user is redirected directly to sign in page instead diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 2a0883402..24354bc13 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -374,7 +374,7 @@ class MyModulesController < ApplicationController def update_state old_status_id = @my_module.my_module_status_id - @my_module.my_module_status_created_by = current_user + @my_module.status_changed_by = current_user if @my_module.update(my_module_status_id: update_status_params[:status_id]) log_activity(:change_status_on_task_flow, @my_module, my_module_status_old: old_status_id, diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 3fc4adedc..cc85e5022 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -133,7 +133,7 @@ class TeamsController < ApplicationController def settings render json: { teamName: @team.name, - teamAutomationGroups: Extends::TEAM_AUTOMATION_GROUPS, + teamAutomationGroups: Extends::TEAM_AUTOMATIONS_GROUPS, teamSettings: @team.settings, updateUrl: update_settings_team_path(@team) } diff --git a/app/helpers/teams_helper.rb b/app/helpers/teams_helper.rb index c1b1f3787..6291ffca6 100644 --- a/app/helpers/teams_helper.rb +++ b/app/helpers/teams_helper.rb @@ -4,7 +4,7 @@ module TeamsHelper if team != current_team && current_user.member_of_team?(team) current_user.current_team_id = team.id current_user.save - update_current_team + set_current_team end end diff --git a/app/javascript/vue/team_automations/container.vue b/app/javascript/vue/team_automations/container.vue index ff417266e..55abcdb0d 100644 --- a/app/javascript/vue/team_automations/container.vue +++ b/app/javascript/vue/team_automations/container.vue @@ -19,7 +19,7 @@ {{ i18n.t(`team_automations.sub_group_element.${subGroupElement}`) }}
- +
@@ -41,7 +41,7 @@ import axios from '../../packs/custom_axios.js'; export default { - name: 'StorageLocationsContainer', + name: 'TeamAutomationsSettingsContainer', components: {}, props: { teamUrl: String, @@ -57,12 +57,21 @@ export default { }, computed: { emptySettings() { - return Object.entries(this.teamObject.teamAutomationGroups).reduce((settings, [_group, subGroups]) => { - Object.values(subGroups).flat().forEach(element => { - settings[element] = false; - }); - return settings; - }, {}); + const result = {}; + + for (const [group, subGroups] of Object.entries(this.teamObject.teamAutomationGroups)) { + result[group] = {}; + + for (const [subGroup, settingsArray] of Object.entries(subGroups)) { + result[group][subGroup] = {}; + + settingsArray.forEach(setting => { + result[group][subGroup][setting] = false; + }); + } + } + + return result; } }, methods: { diff --git a/app/models/asset.rb b/app/models/asset.rb index 6fa14f728..bb293f321 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -476,8 +476,4 @@ class Asset < ApplicationRecord def reset_file_processing self.file_processing = false end - - def run_observers - AutomationObservers::ProtocolContentChangedAutomationObserver.new(step, last_modified_by || created_by).call - end end diff --git a/app/models/checklist.rb b/app/models/checklist.rb index 0505dc9e1..0f319b12b 100644 --- a/app/models/checklist.rb +++ b/app/models/checklist.rb @@ -58,10 +58,4 @@ class Checklist < ApplicationRecord new_checklist end end - - private - - def run_observers - AutomationObservers::ProtocolContentChangedAutomationObserver.new(step, last_modified_by || created_by).call - end end diff --git a/app/models/checklist_item.rb b/app/models/checklist_item.rb index 97998ccb7..1d9eda932 100644 --- a/app/models/checklist_item.rb +++ b/app/models/checklist_item.rb @@ -23,7 +23,6 @@ class ChecklistItem < ApplicationRecord class_name: 'User', optional: true - after_create :run_observers # conditional touch excluding checked updates after_destroy :touch_checklist after_save :touch_checklist @@ -78,8 +77,4 @@ class ChecklistItem < ApplicationRecord checklist.touch # rubocop:enable Rails/SkipsModelValidations end - - def run_observers - AutomationObservers::ProtocolContentChangedAutomationObserver.new(checklist.step, last_modified_by || created_by).call - end end diff --git a/app/models/concerns/observable_model.rb b/app/models/concerns/observable_model.rb index 2243f4518..b5d888480 100644 --- a/app/models/concerns/observable_model.rb +++ b/app/models/concerns/observable_model.rb @@ -4,12 +4,25 @@ module ObservableModel extend ActiveSupport::Concern included do - after_update :run_observers + after_create :notify_observers_on_create + after_update :notify_observers_on_update end private - def run_observers - raise NotImplemented + def changed_by + last_modified_by || created_by + end + + def notify_observers_on_create + return if Current.team.blank? + + Extends::TEAM_AUTOMATIONS_OBSERVERS_CONFIG[self.class.base_class.name].each { |observer| observer.constantize.on_create(self, changed_by) } + end + + def notify_observers_on_update + return if Current.team.blank? + + Extends::TEAM_AUTOMATIONS_OBSERVERS_CONFIG[self.class.base_class.name].each { |observer| observer.constantize.on_update(self, changed_by) } end end diff --git a/app/models/current.rb b/app/models/current.rb new file mode 100644 index 000000000..f724a4a85 --- /dev/null +++ b/app/models/current.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Current < ActiveSupport::CurrentAttributes + attribute :user, :team +end diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 5e0b1ce49..1a4bab50f 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -567,13 +567,6 @@ class Experiment < ApplicationRecord self.due_date_notification_sent = false end - def run_observers - if status_moved_forward? || saved_change_to_project_id || (saved_change_to_archived && !archived) - AutomationObservers::ExperimentStatusChangeAutomationObserver.new(self, last_modified_by).call - end - AutomationObservers::AllExperimentsDoneAutomationObserver.new(project, last_modified_by).call if status_moved_forward? - end - def log_activity(type_of, current_user, my_module) Activities::CreateActivityService .call(activity_type: type_of, diff --git a/app/models/form_field_value.rb b/app/models/form_field_value.rb index c1cf93054..c8c43e2ca 100644 --- a/app/models/form_field_value.rb +++ b/app/models/form_field_value.rb @@ -47,7 +47,8 @@ class FormFieldValue < ApplicationRecord errors.add(:value, :not_unique_latest) end - def run_observers - AutomationObservers::ProtocolContentChangedAutomationObserver.new(form_response.step, created_by).call + # Override for ObservableModel + def changed_by + created_by end end diff --git a/app/models/my_module.rb b/app/models/my_module.rb index 0bf7727cd..a9008e7b2 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -17,7 +17,7 @@ class MyModule < ApplicationRecord include MetadataModel include ObservableModel - attr_accessor :transition_error_rollback, :my_module_status_created_by + attr_accessor :transition_error_rollback, :status_changed_by enum state: Extends::TASKS_STATES enum provisioning_status: { done: 0, in_progress: 1, failed: 2 } @@ -567,7 +567,7 @@ class MyModule < ApplicationRecord if status_changing_direction == :forward my_module_status.my_module_status_consequences.each do |consequence| - consequence.before_forward_call(self, my_module_status_created_by) + consequence.before_forward_call(self, status_changed_by) end end @@ -601,12 +601,4 @@ class MyModule < ApplicationRecord self.y = new_pos[:y] end end - - def run_observers - if (saved_change_to_my_module_status_id? && my_module_status.previous_status_id == changing_from_my_module_status_id) || saved_change_to_experiment_id || - (saved_change_to_archived && !archived) - AutomationObservers::MyModuleStatusChangeAutomationObserver.new(self, last_modified_by).call - end - AutomationObservers::AllMyModulesDoneAutomationObserver.new(experiment, last_modified_by).call if saved_change_to_my_module_status_id? - end end diff --git a/app/models/protocol.rb b/app/models/protocol.rb index 0e2715aea..b27de982e 100644 --- a/app/models/protocol.rb +++ b/app/models/protocol.rb @@ -16,6 +16,7 @@ class Protocol < ApplicationRecord include Assignable include PermissionCheckableModel include TinyMceImages + include ObservableModel before_create -> { self.skip_user_assignments = true }, if: -> { in_module? } diff --git a/app/models/result.rb b/app/models/result.rb index 926d13b07..95b73606d 100644 --- a/app/models/result.rb +++ b/app/models/result.rb @@ -5,6 +5,7 @@ class Result < ApplicationRecord include SearchableModel include SearchableByNameModel include ViewableModel + include ObservableModel include Discard::Model default_scope -> { kept } @@ -44,8 +45,6 @@ class Result < ApplicationRecord CleanupUserSettingsJob.perform_later('result_states', id) end - after_create :run_observers - def self.search(user, include_archived, query = nil, @@ -201,7 +200,8 @@ class Result < ApplicationRecord private - def run_observers - AutomationObservers::ResultCreateAutomationObserver.new(my_module, user).call + # Override for ObservableModel + def changed_by + last_modified_by || user end end diff --git a/app/models/step.rb b/app/models/step.rb index e8993f403..abd9b02b5 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -22,7 +22,6 @@ class Step < ApplicationRecord before_validation :set_completed_on, if: :completed_changed? before_save :set_last_modified_by - after_create :run_observers before_destroy :cascade_before_destroy after_destroy :adjust_positions_after_destroy, unless: -> { skip_position_adjust } @@ -187,17 +186,6 @@ class Step < ApplicationRecord private - def run_observers - return unless protocol.in_module? - - if saved_change_to_completed? - AutomationObservers::CompletedStepChangeAutomationObserver.new(my_module, last_modified_by).call if completed - AutomationObservers::AllCompletedStepsAutomationObserver.new(my_module, last_modified_by).call - end - - AutomationObservers::ProtocolContentChangedAutomationObserver.new(self, last_modified_by).call - end - def duplicate_table(new_step, user, table) table.duplicate(new_step, user, table.step_table.step_orderable_element.position) end diff --git a/app/models/step_comment.rb b/app/models/step_comment.rb index a37959ac3..fb28aa8e9 100644 --- a/app/models/step_comment.rb +++ b/app/models/step_comment.rb @@ -4,12 +4,9 @@ class StepComment < Comment include ObservableModel before_create :fill_unseen_by - after_create :run_observers belongs_to :step, foreign_key: :associated_id, inverse_of: :step_comments - validates :step, presence: true - def commentable step end @@ -20,7 +17,8 @@ class StepComment < Comment self.unseen_by += step.protocol.my_module.experiment.project.users.where.not(id: user.id).pluck(:id) end - def run_observers - AutomationObservers::ProtocolContentChangedAutomationObserver.new(step, last_modified_by || user).call + # Override for ObservableModel + def changed_by + last_modified_by || user end end diff --git a/app/models/step_orderable_element.rb b/app/models/step_orderable_element.rb index 0ecf9786a..2a46e7c14 100644 --- a/app/models/step_orderable_element.rb +++ b/app/models/step_orderable_element.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true class StepOrderableElement < ApplicationRecord + include ObservableModel + validates :position, uniqueness: { scope: :step } validate :check_step_relations - after_create :run_observers - around_destroy :decrement_following_elements_positions - belongs_to :step, inverse_of: :step_orderable_elements, touch: true belongs_to :orderable, polymorphic: true, inverse_of: :step_orderable_element + around_destroy :decrement_following_elements_positions + private def check_step_relations @@ -28,7 +29,8 @@ class StepOrderableElement < ApplicationRecord end end - def run_observers - AutomationObservers::ProtocolContentChangedAutomationObserver.new(step, step.last_modified_by).call + # Override for ObservableModel + def changed_by + step.last_modified_by end end diff --git a/app/models/step_text.rb b/app/models/step_text.rb index 556c11653..28f086773 100644 --- a/app/models/step_text.rb +++ b/app/models/step_text.rb @@ -2,8 +2,8 @@ class StepText < ApplicationRecord include TinyMceImages - include ActionView::Helpers::TextHelper include ObservableModel + include ActionView::Helpers::TextHelper auto_strip_attributes :name, nullify: false validates :name, length: { maximum: Constants::NAME_MAX_LENGTH } @@ -39,10 +39,4 @@ class StepText < ApplicationRecord new_step_text end end - - private - - def run_observers - AutomationObservers::ProtocolContentChangedAutomationObserver.new(step, step.last_modified_by).call - end end diff --git a/app/models/table.rb b/app/models/table.rb index 0c18a9450..fa9a35835 100644 --- a/app/models/table.rb +++ b/app/models/table.rb @@ -103,8 +103,4 @@ class Table < ApplicationRecord end end end - - def run_observers - AutomationObservers::ProtocolContentChangedAutomationObserver.new(step, step&.last_modified_by).call - end end diff --git a/app/services/automation_observers/all_completed_steps_automation_observer.rb b/app/services/automation_observers/all_completed_steps_automation_observer.rb deleted file mode 100644 index afc5e7b2b..000000000 --- a/app/services/automation_observers/all_completed_steps_automation_observer.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module AutomationObservers - class AllCompletedStepsAutomationObserver - def initialize(my_module, user) - @my_module = my_module - @user = user - end - - def call - return unless @my_module.team.settings.dig('team_automation_settings', 'all_my_module_steps_marked_as_completed') - return unless @my_module.my_module_status.previous_status == @my_module.my_module_status_flow.initial_status && @my_module.steps.where(completed: false).none? - - previous_status_id = @my_module.my_module_status.id - @my_module.update!(my_module_status: @my_module.my_module_status.next_status) - - Activities::CreateActivityService - .call(activity_type: :automation_task_status_changed, - owner: @user, - team: @my_module.team, - project: @my_module.project, - subject: @my_module, - message_items: { - my_module: @my_module.id, - my_module_status_old: previous_status_id, - my_module_status_new: @my_module.my_module_status.id - }) - end - end -end diff --git a/app/services/automation_observers/all_experiments_done_automation_observer.rb b/app/services/automation_observers/all_experiments_done_automation_observer.rb deleted file mode 100644 index ccd36a43e..000000000 --- a/app/services/automation_observers/all_experiments_done_automation_observer.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module AutomationObservers - class AllExperimentsDoneAutomationObserver - def initialize(project, user) - @user = user - @project = project - end - - def call - return unless @project.team.settings.dig('team_automation_settings', 'all_experiments_done') - return unless @project.started? - return if @project.experiments.active.where.not(id: @project.experiments.active.done).exists? - - @project.update!(status: :done) - - Activities::CreateActivityService - .call(activity_type: :automation_project_status_changed, - owner: @user, - team: @project.team, - subject: @project, - message_items: { - project: @project.id, - project_status_old: I18n.t('experiments.table.column.status.in_progress'), - project_status_new: I18n.t("experiments.table.column.status.#{@project.status}") - }) - end - end -end diff --git a/app/services/automation_observers/all_experiments_done_observer.rb b/app/services/automation_observers/all_experiments_done_observer.rb new file mode 100644 index 000000000..de3210cde --- /dev/null +++ b/app/services/automation_observers/all_experiments_done_observer.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module AutomationObservers + class AllExperimentsDoneObserver < BaseObserver + def self.on_update(experiment, user) + return unless Current.team.settings.dig('team_automation_settings', 'projects', 'project_status_done', 'on_all_experiments_done') + return unless experiment.status_moved_forward? + + project = experiment.project + + return unless project.started? + return if project.experiments.active.where.not(id: project.experiments.active.done).exists? + + project.update!(status: :done, last_modified_by: user) + + Activities::CreateActivityService + .call(activity_type: :automation_project_status_changed, + owner: user, + team: project.team, + subject: project, + message_items: { + project: project.id, + project_status_old: I18n.t('experiments.table.column.status.in_progress'), + project_status_new: I18n.t("experiments.table.column.status.#{project.status}") + }) + end + end +end diff --git a/app/services/automation_observers/all_my_modules_done_automation_observer.rb b/app/services/automation_observers/all_my_modules_done_automation_observer.rb deleted file mode 100644 index 78c82e2eb..000000000 --- a/app/services/automation_observers/all_my_modules_done_automation_observer.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module AutomationObservers - class AllMyModulesDoneAutomationObserver - def initialize(experiment, user) - @user = user - @experiment = experiment - end - - def call - return unless @experiment.team.settings.dig('team_automation_settings', 'all_tasks_done') - return unless @experiment.started? - return unless @experiment.my_modules.active.joins(:my_module_status).where.not(my_module_status: MyModuleStatusFlow.first.final_status).none? - - @experiment.update!(status: :done) - - Activities::CreateActivityService - .call(activity_type: :automation_experiment_status_changed, - owner: @user, - team: @experiment.team, - project: @experiment.project, - subject: @experiment, - message_items: { - experiment: @experiment.id, - experiment_status_old: I18n.t('experiments.table.column.status.in_progress'), - experiment_status_new: I18n.t("experiments.table.column.status.#{@experiment.status}") - }) - end - end -end diff --git a/app/services/automation_observers/all_steps_completion_observer.rb b/app/services/automation_observers/all_steps_completion_observer.rb new file mode 100644 index 000000000..1a7c25996 --- /dev/null +++ b/app/services/automation_observers/all_steps_completion_observer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module AutomationObservers + class AllStepsCompletionObserver < BaseObserver + def self.on_update(step, user) + return unless Current.team.settings.dig('team_automation_settings', 'tasks', 'task_status_completed', 'on_all_steps_completion') + return unless step.saved_change_to_completed? && step.completed + return unless step.protocol.in_module? + + my_module = step.protocol.my_module + + return unless my_module.my_module_status.previous_status == my_module.my_module_status_flow.initial_status && my_module.steps.where(completed: false).none? + + previous_status_id = my_module.my_module_status.id + my_module.update!(my_module_status: my_module.my_module_status.next_status, last_modified_by: user) + + Activities::CreateActivityService + .call(activity_type: :automation_task_status_changed, + owner: user, + team: my_module.team, + project: my_module.project, + subject: my_module, + message_items: { + my_module: my_module.id, + my_module_status_old: previous_status_id, + my_module_status_new: my_module.my_module_status.id + }) + end + end +end diff --git a/app/services/automation_observers/all_tasks_done_observer.rb b/app/services/automation_observers/all_tasks_done_observer.rb new file mode 100644 index 000000000..4a3d9411f --- /dev/null +++ b/app/services/automation_observers/all_tasks_done_observer.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module AutomationObservers + class AllTasksDoneObserver < BaseObserver + def self.on_update(my_module, user) + return unless Current.team.settings.dig('team_automation_settings', 'experiments', 'experiment_status_done', 'on_all_tasks_done') + return unless my_module.saved_change_to_my_module_status_id? + return unless my_module.experiment.started? + return unless my_module.experiment.my_modules.active.joins(:my_module_status).where.not(my_module_status: MyModuleStatusFlow.first.final_status).none? + + experiment = my_module.experiment + experiment.update!(status: :done, last_modified_by: user) + + Activities::CreateActivityService + .call(activity_type: :automation_experiment_status_changed, + owner: user, + team: experiment.team, + project: experiment.project, + subject: experiment, + message_items: { + experiment: experiment.id, + experiment_status_old: I18n.t('experiments.table.column.status.in_progress'), + experiment_status_new: I18n.t("experiments.table.column.status.#{experiment.status}") + }) + end + end +end diff --git a/app/services/automation_observers/base_observer.rb b/app/services/automation_observers/base_observer.rb new file mode 100644 index 000000000..2b21c10a7 --- /dev/null +++ b/app/services/automation_observers/base_observer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module AutomationObservers + class BaseObserver + def self.on_create(object, user); end + + def self.on_update(object, user); end + end +end diff --git a/app/services/automation_observers/completed_step_change_automation_observer.rb b/app/services/automation_observers/completed_step_change_automation_observer.rb deleted file mode 100644 index 26390d91c..000000000 --- a/app/services/automation_observers/completed_step_change_automation_observer.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module AutomationObservers - class CompletedStepChangeAutomationObserver - def initialize(my_module, user) - @my_module = my_module - @user = user - end - - def call - return unless @my_module.team.settings.dig('team_automation_settings', 'step_marked_as_completed') - return unless @my_module.my_module_status.initial_status? - - previous_status_id = @my_module.my_module_status.id - @my_module.update!(my_module_status: @my_module.my_module_status.next_status) - - Activities::CreateActivityService - .call(activity_type: :automation_task_status_changed, - owner: @user, - team: @my_module.team, - project: @my_module.project, - subject: @my_module, - message_items: { - my_module: @my_module.id, - my_module_status_old: previous_status_id, - my_module_status_new: @my_module.my_module_status.id - }) - end - end -end diff --git a/app/services/automation_observers/experiment_status_change_automation_observer.rb b/app/services/automation_observers/experiment_status_change_automation_observer.rb deleted file mode 100644 index a2fa907a3..000000000 --- a/app/services/automation_observers/experiment_status_change_automation_observer.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -module AutomationObservers - class ExperimentStatusChangeAutomationObserver - def initialize(experiment, user) - @experiment = experiment - @user = user - @project = @experiment.project - end - - def call - return unless @project.team.settings.dig('team_automation_settings', 'experiment_moves_from_not_started_to_in_progress') - return unless @project.not_started? - return if @experiment.not_started? - - @project.update!(status: :in_progress) - Activities::CreateActivityService - .call(activity_type: :automation_project_status_changed, - owner: @user, - team: @project.team, - subject: @project, - message_items: { - project: @project.id, - project_status_old: I18n.t('experiments.table.column.status.not_started'), - project_status_new: I18n.t("experiments.table.column.status.#{@project.status}") - }) - end - end -end diff --git a/app/services/automation_observers/experiment_status_change_observer.rb b/app/services/automation_observers/experiment_status_change_observer.rb new file mode 100644 index 000000000..b3faf07f8 --- /dev/null +++ b/app/services/automation_observers/experiment_status_change_observer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module AutomationObservers + class ExperimentStatusChangeObserver < BaseObserver + def self.on_update(experiment, user) + return unless Current.team.settings.dig('team_automation_settings', 'projects', 'project_status_in_progress', 'on_experiment_in_progress') + return unless experiment.status_moved_forward? || + experiment.saved_change_to_project_id || + (experiment.saved_change_to_archived && !experiment.archived) + + project = experiment.project + + return unless project.not_started? + return if experiment.not_started? + + project.update!(status: :in_progress, last_modified_by: user) + + Activities::CreateActivityService + .call(activity_type: :automation_project_status_changed, + owner: user, + team: project.team, + subject: project, + message_items: { + project: project.id, + project_status_old: I18n.t('experiments.table.column.status.not_started'), + project_status_new: I18n.t("experiments.table.column.status.#{project.status}") + }) + end + end +end diff --git a/app/services/automation_observers/my_module_status_change_automation_observer.rb b/app/services/automation_observers/my_module_status_change_automation_observer.rb deleted file mode 100644 index c4a715815..000000000 --- a/app/services/automation_observers/my_module_status_change_automation_observer.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -module AutomationObservers - class MyModuleStatusChangeAutomationObserver - def initialize(my_module, user) - @my_module = my_module - @user = user - @experiment = @my_module.experiment - end - - def call - return unless @my_module.team.settings.dig('team_automation_settings', 'task_moves_from_not_started_to_in_progress') - return unless @experiment.not_started? - return if @my_module.my_module_status.initial_status? - - @experiment.update!(status: :in_progress) - - Activities::CreateActivityService - .call(activity_type: :automation_experiment_status_changed, - owner: @user, - team: @experiment.team, - project: @experiment.project, - subject: @experiment, - message_items: { - experiment: @experiment.id, - experiment_status_old: I18n.t('experiments.table.column.status.not_started'), - experiment_status_new: I18n.t("experiments.table.column.status.#{@experiment.status}") - }) - end - end -end diff --git a/app/services/automation_observers/protocol_content_changed_automation_observer.rb b/app/services/automation_observers/protocol_content_changed_automation_observer.rb deleted file mode 100644 index d23b5177e..000000000 --- a/app/services/automation_observers/protocol_content_changed_automation_observer.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -module AutomationObservers - class ProtocolContentChangedAutomationObserver - def initialize(step, user) - @step = step - @my_module = step&.my_module - @user = user - end - - def call - return if @step.blank? - return unless @step.protocol.in_module? - return unless @my_module.team.settings.dig('team_automation_settings', 'protocol_content_added') - return unless @my_module.my_module_status.initial_status? - - previous_status_id = @my_module.my_module_status.id - @my_module.update!(my_module_status: @my_module.my_module_status.next_status) - - Activities::CreateActivityService - .call(activity_type: :automation_task_status_changed, - owner: @user, - team: @my_module.team, - project: @my_module.project, - subject: @my_module, - message_items: { - my_module: @my_module.id, - my_module_status_old: previous_status_id, - my_module_status_new: @my_module.my_module_status.id - }) - end - end -end diff --git a/app/services/automation_observers/result_create_automation_observer.rb b/app/services/automation_observers/result_create_automation_observer.rb deleted file mode 100644 index 765fcda23..000000000 --- a/app/services/automation_observers/result_create_automation_observer.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -module AutomationObservers - class ResultCreateAutomationObserver - def initialize(my_module, user) - @my_module = my_module - @user = user - end - - def call - return @my_module.team.settings.dig('team_automation_settings', 'task_result_added') unless @my_module.team.settings.dig('team_automation_settings', 'task_result_added') - return unless @my_module.my_module_status.initial_status? - - previous_status_id = @my_module.my_module_status.id - @my_module.update!(my_module_status: @my_module.my_module_status.next_status) - - Activities::CreateActivityService - .call(activity_type: :automation_task_status_changed, - owner: @user, - team: @my_module.team, - project: @my_module.project, - subject: @my_module, - message_items: { - my_module: @my_module.id, - my_module_status_old: previous_status_id, - my_module_status_new: @my_module.my_module_status.id - }) - end - end -end diff --git a/app/services/automation_observers/result_create_observer.rb b/app/services/automation_observers/result_create_observer.rb new file mode 100644 index 000000000..89222ec4c --- /dev/null +++ b/app/services/automation_observers/result_create_observer.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module AutomationObservers + class ResultCreateObserver < BaseObserver + def self.on_create(result, user) + return unless Current.team.settings.dig('team_automation_settings', 'tasks', 'task_status_in_progress', 'on_added_result') + return unless result.my_module.my_module_status.initial_status? + + my_module = result.my_module + previous_status_id = my_module.my_module_status.id + my_module.update!(my_module_status: my_module.my_module_status.next_status, last_modified_by: user) + + Activities::CreateActivityService + .call(activity_type: :automation_task_status_changed, + owner: user, + team: my_module.team, + project: my_module.project, + subject: my_module, + message_items: { + my_module: my_module.id, + my_module_status_old: previous_status_id, + my_module_status_new: my_module.my_module_status.id + }) + end + end +end diff --git a/app/services/automation_observers/step_completion_observer.rb b/app/services/automation_observers/step_completion_observer.rb new file mode 100644 index 000000000..7f0b1065b --- /dev/null +++ b/app/services/automation_observers/step_completion_observer.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module AutomationObservers + class StepCompletionObserver < BaseObserver + def self.on_update(step, user) + return unless Current.team.settings.dig('team_automation_settings', 'tasks', 'task_status_in_progress', 'on_step_completion') + return unless step.saved_change_to_completed? && step.completed + return unless step.protocol.in_module? && step.protocol.my_module.my_module_status.initial_status? + + my_module = step.protocol.my_module + previous_status_id = my_module.my_module_status.id + my_module.update!(my_module_status: my_module.my_module_status.next_status, last_modified_by: user) + + Activities::CreateActivityService + .call(activity_type: :automation_task_status_changed, + owner: user, + team: my_module.team, + project: my_module.project, + subject: my_module, + message_items: { + my_module: my_module.id, + my_module_status_old: previous_status_id, + my_module_status_new: my_module.my_module_status.id + }) + end + end +end diff --git a/app/services/automation_observers/task_protocol_content_change_observer.rb b/app/services/automation_observers/task_protocol_content_change_observer.rb new file mode 100644 index 000000000..6709d5f62 --- /dev/null +++ b/app/services/automation_observers/task_protocol_content_change_observer.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module AutomationObservers + class TaskProtocolContentChangeObserver < BaseObserver + def self.on_create(element, user) + # Handle creation of an empty protocol alongside with a task + return if element.is_a?(Protocol) + + on_update(element, user) + end + + def self.on_update(element, user) + return unless Current.team.settings.dig('team_automation_settings', 'tasks', 'task_status_in_progress', 'on_protocol_content_change') + + protocol = nil + + case element.class.name + when 'Asset', 'Table' + return if element.step.blank? + + protocol = element.step.protocol + when 'Checklist', 'StepText', 'StepComment', 'StepOrderableElement' + protocol = element.step.protocol + when 'ChecklistItem' + protocol = element.checklist.step.protocol + when 'FormFieldValue' + protocol = element.form_response.step.protocol + when 'Step' + protocol = element.protocol + when 'Protocol' + protocol = element + end + + return if protocol.blank? + return unless protocol.in_module? && protocol.my_module.my_module_status.initial_status? + + my_module = protocol.my_module + previous_status_id = my_module.my_module_status.id + my_module.update!(my_module_status: my_module.my_module_status.next_status, last_modified_by: user) + + Activities::CreateActivityService + .call(activity_type: :automation_task_status_changed, + owner: user, + team: my_module.team, + project: my_module.project, + subject: my_module, + message_items: { + my_module: my_module.id, + my_module_status_old: previous_status_id, + my_module_status_new: my_module.my_module_status.id + }) + end + end +end diff --git a/app/services/automation_observers/task_status_change_observer.rb b/app/services/automation_observers/task_status_change_observer.rb new file mode 100644 index 000000000..03f432df1 --- /dev/null +++ b/app/services/automation_observers/task_status_change_observer.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module AutomationObservers + class TaskStatusChangeObserver < BaseObserver + def self.on_update(my_module, user) + return unless Current.team.settings.dig('team_automation_settings', 'experiments', 'experiment_status_in_progress', 'on_task_in_progress') + return unless my_module.saved_change_to_my_module_status_id? && !my_module.my_module_status.initial_status? + return unless my_module.experiment.not_started? + + experiment = my_module.experiment + experiment.update!(status: :in_progress, last_modified_by: user) + + Activities::CreateActivityService + .call(activity_type: :automation_experiment_status_changed, + owner: user, + team: experiment.team, + project: experiment.project, + subject: experiment, + message_items: { + experiment: experiment.id, + experiment_status_old: I18n.t('experiments.table.column.status.not_started'), + experiment_status_new: I18n.t("experiments.table.column.status.#{experiment.status}") + }) + end + end +end diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index 9885948bc..37f63a11d 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -805,35 +805,51 @@ class Extends } } - TEAM_AUTOMATION_GROUPS = { - task_automation: { + TEAM_AUTOMATIONS_GROUPS = { + tasks: { task_status_in_progress: %I[ - protocol_content_added - step_marked_as_completed - task_result_added + on_protocol_content_change + on_step_completion + on_added_result ], task_status_completed: %I[ - all_my_module_steps_marked_as_completed + on_all_steps_completion ] }, - experiment_automation: { + experiments: { experiment_status_in_progress: %I[ - task_moves_from_not_started_to_in_progress + on_task_in_progress ], experiment_status_done: %I[ - all_tasks_done + on_all_tasks_done ] }, - project_automation: { + projects: { project_status_in_progress: %I[ - experiment_moves_from_not_started_to_in_progress + on_experiment_in_progress ], project_status_done: %I[ - all_experiments_done + on_all_experiments_done ] } } + TEAM_AUTOMATIONS_OBSERVERS_CONFIG = { + 'Experiment' => ['AutomationObservers::AllExperimentsDoneObserver', 'AutomationObservers::ExperimentStatusChangeObserver'], + 'MyModule' => ['AutomationObservers::AllTasksDoneObserver', 'AutomationObservers::TaskStatusChangeObserver'], + 'Protocol' => ['AutomationObservers::TaskProtocolContentChangeObserver'], + 'Asset' => ['AutomationObservers::TaskProtocolContentChangeObserver'], + 'Table' => ['AutomationObservers::TaskProtocolContentChangeObserver'], + 'StepText' => ['AutomationObservers::TaskProtocolContentChangeObserver'], + 'ChecklistItem' => ['AutomationObservers::TaskProtocolContentChangeObserver'], + 'Checklist' => ['AutomationObservers::TaskProtocolContentChangeObserver'], + 'FormFieldValue' => ['AutomationObservers::TaskProtocolContentChangeObserver'], + 'StepOrderableElement' => ['AutomationObservers::TaskProtocolContentChangeObserver'], + 'StepComment' => ['AutomationObservers::TaskProtocolContentChangeObserver'], + 'Step' => ['AutomationObservers::AllStepsCompletionObserver', 'AutomationObservers::StepCompletionObserver', 'AutomationObservers::TaskProtocolContentChangeObserver'], + 'Result' => ['AutomationObservers::ResultCreateObserver'] + } + DEFAULT_TEAM_SETTINGS = {} WHITELISTED_USER_SETTINGS = %w( diff --git a/config/locales/en.yml b/config/locales/en.yml index e4e2b605e..4749f654f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4320,9 +4320,9 @@ en: team_automations: description: "Automate repetitive work to enhance efficiency. Enabled settings apply to all projects, experiments, and tasks in your current workspace. Automations start when enabled and don't affect past data. Manual changes remain available and will override automated updates." groups: - task_automation: 'Task automations' - experiment_automation: 'Experiment automations' - project_automation: 'Project automations' + tasks: 'Task automations' + experiments: 'Experiment automations' + projects: 'Project automations' sub_groups: task_status_in_progress: 'Automatically update task status to "In progress" when:' task_status_completed: 'Automatically update task status to “Completed” when:' @@ -4332,14 +4332,14 @@ en: project_status_in_progress: 'Automatically update project status to "In progress" when:' project_status_done: 'Automatically update project status to "Done" when:' sub_group_element: - protocol_content_added: 'Protocol content is added (including step comments)' - step_marked_as_completed: 'At least one step is marked as completed' - task_result_added: 'Task result is added' - task_moves_from_not_started_to_in_progress: 'At least one task moves from "Not started" to "In progress" or other status' - all_tasks_done: 'All tasks inside reach their final status.' - experiment_moves_from_not_started_to_in_progress: 'At least one experiment moves to "In progress" or "Done"' - all_experiments_done: 'All experiments inside are marked as "Done"' - all_my_module_steps_marked_as_completed: 'All steps are marked as done' + on_protocol_content_change: 'Protocol content is added (including step comments)' + on_step_completion: 'At least one step is marked as completed' + on_added_result: 'Task result is added' + on_task_in_progress: 'At least one task moves from "Not started" to "In progress" or other status' + on_all_tasks_done: 'All tasks inside reach their final status.' + on_experiment_in_progress: 'At least one experiment moves to "In progress" or "Done"' + on_all_experiments_done: 'All experiments inside are marked as "Done"' + on_all_steps_completion: 'All steps are marked as done' notifications: title: "Notifications" diff --git a/spec/models/step_comment_spec.rb b/spec/models/step_comment_spec.rb index 6d6809a65..e98036e9b 100644 --- a/spec/models/step_comment_spec.rb +++ b/spec/models/step_comment_spec.rb @@ -26,8 +26,4 @@ describe StepComment, type: :model do describe 'Relations' do it { should belong_to :step } end - - describe 'Validations' do - it { should validate_presence_of :step } - end end