Merge pull request #8787 from okriuchykhin/ok_SCI_12213

Improve implementation of team automations observers [SCI-12213]
This commit is contained in:
Alex Kriuchykhin 2025-08-26 10:32:43 +02:00 committed by GitHub
commit a95ab59e89
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 366 additions and 363 deletions

View file

@ -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

View file

@ -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,

View file

@ -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)
}

View file

@ -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

View file

@ -19,7 +19,7 @@
<td class="py-3 pl-6">{{ i18n.t(`team_automations.sub_group_element.${subGroupElement}`) }}</td>
<td class="p-3">
<div class="sci-toggle-checkbox-container">
<input v-model="teamAutomationSettings[subGroupElement]" type="checkbox" class="sci-toggle-checkbox" @change="setTeamAutomationsSettings"/>
<input v-model="teamAutomationSettings[group][subGroup][subGroupElement]" type="checkbox" class="sci-toggle-checkbox" @change="setTeamAutomationsSettings"/>
<label class="sci-toggle-checkbox-label"></label>
</div>
</td>
@ -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: {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

5
app/models/current.rb Normal file
View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class Current < ActiveSupport::CurrentAttributes
attribute :user, :team
end

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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? }

View file

@ -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,
@ -205,7 +204,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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -103,8 +103,4 @@ class Table < ApplicationRecord
end
end
end
def run_observers
AutomationObservers::ProtocolContentChangedAutomationObserver.new(step, step&.last_modified_by).call
end
end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -807,35 +807,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(

View file

@ -4330,9 +4330,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:'
@ -4342,14 +4342,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"

View file

@ -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