From 8d98f65c92cf456da6d2a9c1ace6dad4e71127f9 Mon Sep 17 00:00:00 2001 From: Andrej Date: Thu, 5 Jun 2025 17:14:03 +0200 Subject: [PATCH 01/20] Add initial team automation settings page [SCI-11929] --- app/controllers/navigations_controller.rb | 2 + app/controllers/teams_controller.rb | 46 +++++++++- app/javascript/packs/vue/team_automations.js | 10 +++ .../vue/team_automations/container.vue | 84 +++++++++++++++++++ app/models/team.rb | 5 ++ app/views/teams/automations.html.erb | 12 +++ .../users/settings/teams/_header.html.erb | 1 + config/initializers/extends.rb | 29 +++++++ config/locales/en.yml | 24 ++++++ config/routes.rb | 3 + config/webpack/webpack.config.js | 3 +- ...20250605061957_add_setting_team_columns.rb | 13 +++ db/schema.rb | 3 +- 13 files changed, 229 insertions(+), 6 deletions(-) create mode 100644 app/javascript/packs/vue/team_automations.js create mode 100644 app/javascript/vue/team_automations/container.vue create mode 100644 app/views/teams/automations.html.erb create mode 100644 db/migrate/20250605061957_add_setting_team_columns.rb diff --git a/app/controllers/navigations_controller.rb b/app/controllers/navigations_controller.rb index 964ca12e4..34fd9f69d 100644 --- a/app/controllers/navigations_controller.rb +++ b/app/controllers/navigations_controller.rb @@ -68,6 +68,8 @@ class NavigationsController < ApplicationController } ] + links.insert(1, { name: I18n.t('users.settings.sidebar.account_nav.automations'), url: automations_team_path(current_team) }) if can_manage_team?(current_team) + if can_create_acitivity_filters? links.push({ name: I18n.t('users.settings.sidebar.webhooks'), url: users_settings_webhooks_path }) end diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 61707f01b..72b40158b 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -7,10 +7,13 @@ class TeamsController < ApplicationController helper_method :current_folder before_action :load_vars, only: %i(sidebar export_projects export_projects_modal - disable_tasks_sharing_modal shared_tasks_toggle) + disable_tasks_sharing_modal shared_tasks_toggle + settings update_settings automations) before_action :load_current_folder, only: :sidebar - before_action :check_read_permissions, except: %i(view_type visible_teams visible_users) + before_action :check_read_permissions, except: %i(view_type visible_teams visible_users settings update_settings automations shared_tasks_toggle) + before_action :check_manage_permissions, only: %i(settings update_settings automations shared_tasks_toggle) before_action :check_export_projects_permissions, only: %i(export_projects_modal export_projects) + before_action :set_breadcrumbs_items, only: %i(automations) def visible_teams teams = current_user.teams.order(:name) @@ -94,8 +97,6 @@ class TeamsController < ApplicationController end def shared_tasks_toggle - return render_403 unless can_manage_team?(@team) - @team.toggle!(:shareable_links_enabled) if @team.shareable_links_enabled? @@ -121,6 +122,28 @@ class TeamsController < ApplicationController render json: { cards_view_type_class: cards_view_type_class(view_type_params) }, status: :ok end + def automations + @active_tab = :automations + end + + def settings + render json: { + teamName: @team.name, + teamAutomationGroups: Extends::TEAM_AUTOMATION_GROUPS, + teamSettings: @team.settings, + updateUrl: update_settings_team_path(@team) + } + end + + def update_settings + @team.settings.merge!(update_settings_params) + if @team.save + render json: {} + else + render json: @team.errors, status: :unprocessable_entity + end + end + private def load_vars @@ -140,6 +163,10 @@ class TeamsController < ApplicationController render_403 unless can_read_team?(@team) end + def check_manage_permissions + render_403 unless can_manage_team?(@team) + end + def load_current_folder if current_team && params[:project_folder_id].present? @current_folder = current_team.project_folders.find_by(id: params[:project_folder_id]) @@ -163,6 +190,17 @@ class TeamsController < ApplicationController end end + def update_settings_params + params.require(:team).permit(team_automation_settings: {}) + end + + def set_breadcrumbs_items + @breadcrumbs_items = [ + { label: t('breadcrumbs.teams'), url: teams_path }, + { label: @team.name, url: team_path(@team) } + ] + end + def log_activity(type_of, message_items = {}) Activities::CreateActivityService .call(activity_type: type_of, diff --git a/app/javascript/packs/vue/team_automations.js b/app/javascript/packs/vue/team_automations.js new file mode 100644 index 000000000..53cc5bc97 --- /dev/null +++ b/app/javascript/packs/vue/team_automations.js @@ -0,0 +1,10 @@ +import { PerfectScrollbar } from 'vue3-perfect-scrollbar'; +import { createApp } from 'vue/dist/vue.esm-bundler.js'; +import TeamAutomations from '../../vue/team_automations/container.vue'; +import { mountWithTurbolinks } from './helpers/turbolinks.js'; + +const app = createApp({}); +app.component('TeamAutomations', TeamAutomations); +app.component('PerfectScrollbar', PerfectScrollbar); +app.config.globalProperties.i18n = window.I18n; +mountWithTurbolinks(app, '#team_automations'); diff --git a/app/javascript/vue/team_automations/container.vue b/app/javascript/vue/team_automations/container.vue new file mode 100644 index 000000000..ff417266e --- /dev/null +++ b/app/javascript/vue/team_automations/container.vue @@ -0,0 +1,84 @@ + + + + diff --git a/app/models/team.rb b/app/models/team.rb index 6edec7c03..78dbd9335 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -12,6 +12,7 @@ class Team < ApplicationRecord include ActionView::Helpers::NumberHelper before_save -> { shareable_links.destroy_all }, if: -> { !shareable_links_enabled? } + before_create :init_default_settings after_create :generate_template_project after_create :create_default_label_templates after_create :create_default_repository_templates @@ -204,4 +205,8 @@ class Team < ApplicationRecord RepositoryTemplate.equipment.update(team: self) RepositoryTemplate.chemicals_and_reagents.update(team: self) end + + def init_default_settings + self.settings = Extends::DEFAULT_TEAM_SETTINGS + end end diff --git a/app/views/teams/automations.html.erb b/app/views/teams/automations.html.erb new file mode 100644 index 000000000..faeb760e3 --- /dev/null +++ b/app/views/teams/automations.html.erb @@ -0,0 +1,12 @@ +<% provide(:head_title, t("users.settings.account.preferences.head_title")) %> + +
+ <%= render partial: 'users/settings/teams/header' %> +
+ +
+
+ +<%= javascript_include_tag "vue_team_automations" %> diff --git a/app/views/users/settings/teams/_header.html.erb b/app/views/users/settings/teams/_header.html.erb index c30852006..6c9d42198 100644 --- a/app/views/users/settings/teams/_header.html.erb +++ b/app/views/users/settings/teams/_header.html.erb @@ -21,4 +21,5 @@ <%= link_to t("users.settings.teams.navigation.details"), team_path(@team), class: "p-2.5 hover:no-underline #{ @active_tab == :details ? "text-sn-blue" : "text-sn-grey" }"%> <%= link_to t("users.settings.teams.navigation.members"), members_users_settings_team_path(@team), class: "p-2.5 hover:no-underline #{ @active_tab == :members ? "text-sn-blue" : "text-sn-grey" }"%> <%= link_to t("users.settings.teams.navigation.groups"), users_settings_team_user_groups_path(@team), class: "p-2.5 hover:no-underline #{ @active_tab == :user_groups ? "text-sn-blue" : "text-sn-grey" }"%> + <%= link_to t("users.settings.sidebar.account_nav.automations"), automations_team_path(@team), class: "p-2.5 hover:no-underline #{ @active_tab == :automations ? "text-sn-blue" : "text-sn-grey" }"%> diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index a1f5fd9c7..c8ac58f51 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -781,6 +781,7 @@ class Extends teams/members user_groups/index user_groups/show + teams/automations ) DEFAULT_USER_NOTIFICATION_SETTINGS = { @@ -798,6 +799,34 @@ class Extends } } + TEAM_AUTOMATION_GROUPS = { + task_automation: { + task_status_in_progress: %I[ + protocol_content_added + step_marked_as_completed + task_result_added + ] + }, + experiment_automation: { + experiment_status_in_progress: %I[ + task_moves_from_not_started_to_in_progress + ], + experiment_status_done: %I[ + all_tasks_done + ] + }, + project_automation: { + project_status_in_progress: %I[ + experiment_moves_from_not_started_to_in_progress + ], + project_status_done: %I[ + all_experiments_done + ] + } + } + + DEFAULT_TEAM_SETTINGS = {} + WHITELISTED_USER_SETTINGS = %w( LabelTemplates_active_state LabelTemplates_archived_state diff --git a/config/locales/en.yml b/config/locales/en.yml index b5cfa73ca..24892704d 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3493,6 +3493,7 @@ en: preferences: "My preferences" addons: "Add-ons" connected_accounts: "Connected accounts" + automations: "Automations" account: preferences: head_title: "Settings | My preferences" @@ -4331,6 +4332,28 @@ en: time: "%H:%M" short: "%H" + 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' + sub_groups: + task_status_in_progress: 'Automatically update task status to "In progress" when:' + task_status_done: 'Automatically update task status to "Done" when:' + experiment_status_in_progress: 'Automatically update experiment status to "In progress" when:' + experiment_status_done: 'Automatically update experiment status to "Done" when:' + 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"' + notifications: title: "Notifications" sub_title: "Select the updates you want to be notified about and where you receive them." @@ -4933,6 +4956,7 @@ en: label_printer: "Label printer" fluics_printer: "Fluics printer" forms: "Forms" + settings: "Settings" Add: "Add" Asset: "File" diff --git a/config/routes.rb b/config/routes.rb index 01ac4687c..a6c3e373d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -274,6 +274,9 @@ Rails.application.routes.draw do get 'atwho_experiments', to: 'at_who#experiments' get 'atwho_my_modules', to: 'at_who#my_modules' get 'atwho_menu_items', to: 'at_who#menu_items' + get :automations + get :settings + put :update_settings end # External protocols routes diff --git a/config/webpack/webpack.config.js b/config/webpack/webpack.config.js index 2ef9dc729..91b265dfb 100644 --- a/config/webpack/webpack.config.js +++ b/config/webpack/webpack.config.js @@ -76,7 +76,8 @@ const entryList = { vue_favorites_widget: './app/javascript/packs/vue/favorites_widget.js', vue_experiment_description_modal: './app/javascript/packs/vue/experiment_description_modal.js', vue_user_groups_table: './app/javascript/packs/vue/user_groups_table.js', - vue_user_groups_show: './app/javascript/packs/vue/user_groups_show.js' + vue_user_groups_show: './app/javascript/packs/vue/user_groups_show.js', + vue_team_automations: './app/javascript/packs/vue/team_automations.js' }; // Engine pack loading based on https://github.com/rails/webpacker/issues/348#issuecomment-635480949 diff --git a/db/migrate/20250605061957_add_setting_team_columns.rb b/db/migrate/20250605061957_add_setting_team_columns.rb new file mode 100644 index 000000000..6002d24e1 --- /dev/null +++ b/db/migrate/20250605061957_add_setting_team_columns.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +class AddSettingTeamColumns < ActiveRecord::Migration[7.0] + def change + add_column :teams, :settings, :jsonb, default: {}, null: false + + # rubocop:disable Rails/SkipsModelValidations + Team.find_each do |team| + team.update_columns(settings: Extends::DEFAULT_TEAM_SETTINGS) + end + # rubocop:enable Rails/SkipsModelValidations + end +end diff --git a/db/schema.rb b/db/schema.rb index a5dfee916..cea49c24c 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.0].define(version: 2025_06_06_082935) do +ActiveRecord::Schema[7.0].define(version: 20250605061957) do # These are extensions that must be enabled in order to support this database enable_extension "btree_gist" enable_extension "pg_trgm" @@ -1323,6 +1323,7 @@ ActiveRecord::Schema[7.0].define(version: 2025_06_06_082935) do t.string "description" t.bigint "space_taken", default: 1048576, null: false t.boolean "shareable_links_enabled", default: false, null: false + t.jsonb "settings", default: {}, null: false t.index ["created_by_id"], name: "index_teams_on_created_by_id" t.index ["last_modified_by_id"], name: "index_teams_on_last_modified_by_id" t.index ["name"], name: "index_teams_on_name" From e5f870fac196cdd4c65933d9e10bf68ce28b9735 Mon Sep 17 00:00:00 2001 From: Andrej Date: Wed, 30 Jul 2025 09:48:35 +0200 Subject: [PATCH 02/20] Add automation for steps completed on task [SCI-12076] --- app/models/concerns/observable_model.rb | 15 ++++++++++ app/models/step.rb | 5 ++++ .../all_checked_steps_automation_observer.rb | 30 +++++++++++++++++++ config/initializers/extends.rb | 8 +++-- config/locales/en.yml | 2 ++ config/locales/global_activities/en.yml | 2 ++ 6 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 app/models/concerns/observable_model.rb create mode 100644 app/services/automation_observers/all_checked_steps_automation_observer.rb diff --git a/app/models/concerns/observable_model.rb b/app/models/concerns/observable_model.rb new file mode 100644 index 000000000..2243f4518 --- /dev/null +++ b/app/models/concerns/observable_model.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module ObservableModel + extend ActiveSupport::Concern + + included do + after_update :run_observers + end + + private + + def run_observers + raise NotImplemented + end +end diff --git a/app/models/step.rb b/app/models/step.rb index 16fbcff72..7c66f94e2 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -3,6 +3,7 @@ class Step < ApplicationRecord include SearchableByNameModel include TinyMceImages include ViewableModel + include ObservableModel attr_accessor :skip_position_adjust # to be used in bulk deletion @@ -185,6 +186,10 @@ class Step < ApplicationRecord private + def run_observers + AutomationObservers::AllCheckedStepsAutomationObserver.new(my_module, last_modified_by).call if saved_change_to_completed? && completed + 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/services/automation_observers/all_checked_steps_automation_observer.rb b/app/services/automation_observers/all_checked_steps_automation_observer.rb new file mode 100644 index 000000000..ce2b0cd47 --- /dev/null +++ b/app/services/automation_observers/all_checked_steps_automation_observer.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module AutomationObservers + class AllCheckedStepsAutomationObserver + 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/config/initializers/extends.rb b/config/initializers/extends.rb index c8ac58f51..ac5171a7d 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -617,14 +617,15 @@ class Extends repository_access_revoked_all_team_members: 402, repository_access_granted_user_group: 403, repository_access_changed_user_group: 404, - repository_access_revoked_user_group: 405 + repository_access_revoked_user_group: 405, + automation_task_status_changed: 406 } ACTIVITY_GROUPS = { projects: [*0..7, 32, 33, 34, 95, 108, 65, 109, *158..162, 241, 242, 243, *370..378, *390..392], task_results: [23, 26, 25, 42, 24, 40, 41, 99, 110, 122, 116, 128, *246..248, *257..273, *284..291, 301, 303, 306, 328], task: [8, 58, 9, 59, *10..14, 35, 36, 37, 53, 54, *60..63, 138, 139, 140, 64, 66, 106, 126, 120, 132, - 148, 166, 394, 395, 396], + 148, 166, 394, 395, 396, 406], task_protocol: [15, 22, 16, 18, 19, 20, 21, 17, 38, 39, 100, 111, 45, 46, 47, 121, 124, 115, 118, 127, 130, 137, 184, 185, 188, 189, *192..203, 221, 222, 224, 225, 226, 236, *249..252, *274..278, 299, 302, 305, 327, *347..352, 359], task_inventory: [55, 56, 146, 147, 183], @@ -805,6 +806,9 @@ class Extends protocol_content_added step_marked_as_completed task_result_added + ], + task_status_completed: %I[ + all_my_module_steps_marked_as_completed ] }, experiment_automation: { diff --git a/config/locales/en.yml b/config/locales/en.yml index 24892704d..ef14a172f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -4340,6 +4340,7 @@ en: project_automation: '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:' task_status_done: 'Automatically update task status to "Done" when:' experiment_status_in_progress: 'Automatically update experiment status to "In progress" when:' experiment_status_done: 'Automatically update experiment status to "Done" when:' @@ -4353,6 +4354,7 @@ en: 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' notifications: title: "Notifications" diff --git a/config/locales/global_activities/en.yml b/config/locales/global_activities/en.yml index 2303640b3..d64b981a0 100644 --- a/config/locales/global_activities/en.yml +++ b/config/locales/global_activities/en.yml @@ -421,6 +421,7 @@ en: repository_access_granted_user_group_html: "%{user} granted access to %{user_group} with user role %{role} to inventory template %{repository}." repository_access_changed_user_group_html: "%{user} changed %{user_group}'s role on inventory template %{repository} to %{role}." repository_access_revoked_user_group_html: "%{user} removed group %{user_group} with user role %{role} from inventory template %{repository}." + automation_task_status_changed_html: "%{user} triggered automatic status change from %{my_module_status_old} to %{my_module_status_new} for task %{my_module}." activity_name: create_project: "Project created" edit_project: "Project edited" @@ -797,6 +798,7 @@ en: repository_access_granted_user_group: "Grant access to group" repository_access_changed_user_group: "Change role of group" repository_access_revoked_user_group: "Remove access to group" + automation_task_status_changed: "Task status changed automatically" activity_group: projects: "Projects" task_results: "Task results" From 17a71563599ea51fd0f0d46837750408468f6147 Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 5 Aug 2025 11:22:40 +0200 Subject: [PATCH 03/20] Add experiment automation observers [SCI-11933] --- app/models/my_module.rb | 9 ++++++ ...all_my_modules_done_automation_observer.rb | 30 ++++++++++++++++++ ...odule_status_change_automation_observer.rb | 31 +++++++++++++++++++ config/initializers/extends.rb | 5 +-- config/locales/global_activities/en.yml | 2 ++ 5 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 app/services/automation_observers/all_my_modules_done_automation_observer.rb create mode 100644 app/services/automation_observers/my_module_status_change_automation_observer.rb diff --git a/app/models/my_module.rb b/app/models/my_module.rb index c967c73e7..9f47edc79 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -15,6 +15,7 @@ class MyModule < ApplicationRecord include Cloneable include Favoritable include MetadataModel + include ObservableModel attr_accessor :transition_error_rollback, :my_module_status_created_by @@ -604,4 +605,12 @@ 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/services/automation_observers/all_my_modules_done_automation_observer.rb b/app/services/automation_observers/all_my_modules_done_automation_observer.rb new file mode 100644 index 000000000..8cb832376 --- /dev/null +++ b/app/services/automation_observers/all_my_modules_done_automation_observer.rb @@ -0,0 +1,30 @@ +# 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.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/my_module_status_change_automation_observer.rb b/app/services/automation_observers/my_module_status_change_automation_observer.rb new file mode 100644 index 000000000..c4a715815 --- /dev/null +++ b/app/services/automation_observers/my_module_status_change_automation_observer.rb @@ -0,0 +1,31 @@ +# 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/config/initializers/extends.rb b/config/initializers/extends.rb index ac5171a7d..f9f9e9837 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -618,7 +618,8 @@ class Extends repository_access_granted_user_group: 403, repository_access_changed_user_group: 404, repository_access_revoked_user_group: 405, - automation_task_status_changed: 406 + automation_task_status_changed: 406, + automation_experiment_status_changed: 407 } ACTIVITY_GROUPS = { @@ -629,7 +630,7 @@ class Extends task_protocol: [15, 22, 16, 18, 19, 20, 21, 17, 38, 39, 100, 111, 45, 46, 47, 121, 124, 115, 118, 127, 130, 137, 184, 185, 188, 189, *192..203, 221, 222, 224, 225, 226, 236, *249..252, *274..278, 299, 302, 305, 327, *347..352, 359], task_inventory: [55, 56, 146, 147, 183], - experiment: [*27..31, 57, 141, 165, *363..369, 393], + experiment: [*27..31, 57, 141, 165, *363..369, 393, 407], reports: [48, 50, 49, 163, 164], inventories: [70, 71, 105, 144, 145, 72, 73, 74, 102, 142, 143, 75, 76, 77, 78, 96, 107, 113, 114, *133..136, 180, 181, 182, *292..298, 308, 329, *397..405], diff --git a/config/locales/global_activities/en.yml b/config/locales/global_activities/en.yml index d64b981a0..9fb997a3c 100644 --- a/config/locales/global_activities/en.yml +++ b/config/locales/global_activities/en.yml @@ -422,6 +422,7 @@ en: repository_access_changed_user_group_html: "%{user} changed %{user_group}'s role on inventory template %{repository} to %{role}." repository_access_revoked_user_group_html: "%{user} removed group %{user_group} with user role %{role} from inventory template %{repository}." automation_task_status_changed_html: "%{user} triggered automatic status change from %{my_module_status_old} to %{my_module_status_new} for task %{my_module}." + automation_experiment_status_changed_html: "%{user} triggered automatic status change from %{experiment_status_old} to %{experiment_status_new} for experiment %{experiment}." activity_name: create_project: "Project created" edit_project: "Project edited" @@ -799,6 +800,7 @@ en: repository_access_changed_user_group: "Change role of group" repository_access_revoked_user_group: "Remove access to group" automation_task_status_changed: "Task status changed automatically" + automation_experiment_status_changed: "Experiment status changed automatically" activity_group: projects: "Projects" task_results: "Task results" From df069e87b5006a380fca79013bfadaa869e654dd Mon Sep 17 00:00:00 2001 From: Andrej Date: Tue, 5 Aug 2025 14:53:08 +0200 Subject: [PATCH 04/20] Add automation observers for project [SCI-11934] --- app/models/concerns/time_trackable.rb | 32 +++++++++++++++---- app/models/experiment.rb | 8 +++++ ...ll_experiments_done_automation_observer.rb | 29 +++++++++++++++++ ...all_my_modules_done_automation_observer.rb | 2 +- ...iment_status_change_automation_observer.rb | 29 +++++++++++++++++ config/initializers/extends.rb | 5 +-- config/locales/global_activities/en.yml | 2 ++ 7 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 app/services/automation_observers/all_experiments_done_automation_observer.rb create mode 100644 app/services/automation_observers/experiment_status_change_automation_observer.rb diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb index b9063e953..0d6e38130 100644 --- a/app/models/concerns/time_trackable.rb +++ b/app/models/concerns/time_trackable.rb @@ -3,6 +3,8 @@ module TimeTrackable extend ActiveSupport::Concern + STATUS_ORDER = %i(not_started in_progress done).freeze + included do scope :not_started, -> { where(started_at: nil).where(done_at: nil) } scope :in_progress, -> { where.not(started_at: nil).where(done_at: nil) } @@ -25,13 +27,11 @@ module TimeTrackable end def status - if started_at.nil? && done_at.nil? - :not_started - elsif started_at && !done_at - :in_progress - else - :done - end + compute_status(started_at, done_at) + end + + def status_was + compute_status(started_at_before_last_save, done_at_before_last_save) end def not_started? @@ -70,4 +70,22 @@ module TimeTrackable def complete! update!(done_at: DateTime.now) end + + def status_moved_forward? + return false unless started_at_previously_changed? || done_at_previously_changed? + + STATUS_ORDER.index(status) > STATUS_ORDER.index(status_was) + end + + private + + def compute_status(started_at_value, done_at_value) + if started_at_value.nil? && done_at_value.nil? + :not_started + elsif started_at_value && !done_at_value + :in_progress + else + :done + end + end end diff --git a/app/models/experiment.rb b/app/models/experiment.rb index a85b86c7b..05e4734c2 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -16,6 +16,7 @@ class Experiment < ApplicationRecord include TimeTrackable include Favoritable include MetadataModel + include ObservableModel before_save -> { report_elements.destroy_all }, if: -> { !new_record? && project_id_changed? } before_save :reset_due_date_notification_sent, if: -> { due_date_changed? } @@ -574,6 +575,13 @@ 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/services/automation_observers/all_experiments_done_automation_observer.rb b/app/services/automation_observers/all_experiments_done_automation_observer.rb new file mode 100644 index 000000000..ccd36a43e --- /dev/null +++ b/app/services/automation_observers/all_experiments_done_automation_observer.rb @@ -0,0 +1,29 @@ +# 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_my_modules_done_automation_observer.rb b/app/services/automation_observers/all_my_modules_done_automation_observer.rb index 8cb832376..78c82e2eb 100644 --- a/app/services/automation_observers/all_my_modules_done_automation_observer.rb +++ b/app/services/automation_observers/all_my_modules_done_automation_observer.rb @@ -10,7 +10,7 @@ module AutomationObservers def call return unless @experiment.team.settings.dig('team_automation_settings', 'all_tasks_done') return unless @experiment.started? - return unless @experiment.my_modules.joins(:my_module_status).where.not(my_module_status: MyModuleStatusFlow.first.final_status).none? + return unless @experiment.my_modules.active.joins(:my_module_status).where.not(my_module_status: MyModuleStatusFlow.first.final_status).none? @experiment.update!(status: :done) diff --git a/app/services/automation_observers/experiment_status_change_automation_observer.rb b/app/services/automation_observers/experiment_status_change_automation_observer.rb new file mode 100644 index 000000000..a2fa907a3 --- /dev/null +++ b/app/services/automation_observers/experiment_status_change_automation_observer.rb @@ -0,0 +1,29 @@ +# 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/config/initializers/extends.rb b/config/initializers/extends.rb index f9f9e9837..16e83f2bb 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -619,11 +619,12 @@ class Extends repository_access_changed_user_group: 404, repository_access_revoked_user_group: 405, automation_task_status_changed: 406, - automation_experiment_status_changed: 407 + automation_experiment_status_changed: 407, + automation_project_status_changed: 408 } ACTIVITY_GROUPS = { - projects: [*0..7, 32, 33, 34, 95, 108, 65, 109, *158..162, 241, 242, 243, *370..378, *390..392], + projects: [*0..7, 32, 33, 34, 95, 108, 65, 109, *158..162, 241, 242, 243, *370..378, *390..392, 408], task_results: [23, 26, 25, 42, 24, 40, 41, 99, 110, 122, 116, 128, *246..248, *257..273, *284..291, 301, 303, 306, 328], task: [8, 58, 9, 59, *10..14, 35, 36, 37, 53, 54, *60..63, 138, 139, 140, 64, 66, 106, 126, 120, 132, 148, 166, 394, 395, 396, 406], diff --git a/config/locales/global_activities/en.yml b/config/locales/global_activities/en.yml index 9fb997a3c..e62b52951 100644 --- a/config/locales/global_activities/en.yml +++ b/config/locales/global_activities/en.yml @@ -423,6 +423,7 @@ en: repository_access_revoked_user_group_html: "%{user} removed group %{user_group} with user role %{role} from inventory template %{repository}." automation_task_status_changed_html: "%{user} triggered automatic status change from %{my_module_status_old} to %{my_module_status_new} for task %{my_module}." automation_experiment_status_changed_html: "%{user} triggered automatic status change from %{experiment_status_old} to %{experiment_status_new} for experiment %{experiment}." + automation_project_status_changed_html: "%{user} triggered automatic status change from %{project_status_old} to %{project_status_new} for project %{project}." activity_name: create_project: "Project created" edit_project: "Project edited" @@ -801,6 +802,7 @@ en: repository_access_revoked_user_group: "Remove access to group" automation_task_status_changed: "Task status changed automatically" automation_experiment_status_changed: "Experiment status changed automatically" + automation_project_status_changed: "Project status changed automatically" activity_group: projects: "Projects" task_results: "Task results" From 0f5cf347ef6fced8deca4691cc35cf575986650f Mon Sep 17 00:00:00 2001 From: Andrej Date: Wed, 6 Aug 2025 12:52:24 +0200 Subject: [PATCH 05/20] Add automation observers for the task [SCI-11930] --- app/models/asset.rb | 5 +++ app/models/checklist.rb | 7 ++++ app/models/checklist_item.rb | 6 ++++ app/models/form_field_value.rb | 6 ++++ app/models/result.rb | 8 +++++ app/models/step.rb | 10 +++++- app/models/step_comment.rb | 7 ++++ app/models/step_orderable_element.rb | 5 +++ app/models/step_text.rb | 7 ++++ app/models/table.rb | 5 +++ ...ll_completed_steps_automation_observer.rb} | 2 +- ...mpleted_step_change_automation_observer.rb | 30 +++++++++++++++++ ...col_content_changed_automation_observer.rb | 33 +++++++++++++++++++ .../result_create_automation_observer.rb | 30 +++++++++++++++++ 14 files changed, 159 insertions(+), 2 deletions(-) rename app/services/automation_observers/{all_checked_steps_automation_observer.rb => all_completed_steps_automation_observer.rb} (96%) create mode 100644 app/services/automation_observers/completed_step_change_automation_observer.rb create mode 100644 app/services/automation_observers/protocol_content_changed_automation_observer.rb create mode 100644 app/services/automation_observers/result_create_automation_observer.rb diff --git a/app/models/asset.rb b/app/models/asset.rb index 3f221346a..6fa14f728 100644 --- a/app/models/asset.rb +++ b/app/models/asset.rb @@ -8,6 +8,7 @@ class Asset < ApplicationRecord include ActiveStorageConcerns include ActiveStorageHelper include VersionedAttachments + include ObservableModel require 'tempfile' # Lock duration set to 30 minutes @@ -475,4 +476,8 @@ 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 15580506f..0505dc9e1 100644 --- a/app/models/checklist.rb +++ b/app/models/checklist.rb @@ -1,5 +1,6 @@ class Checklist < ApplicationRecord include SearchableModel + include ObservableModel auto_strip_attributes :name, nullify: false validates :name, @@ -57,4 +58,10 @@ 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 17482023a..97998ccb7 100644 --- a/app/models/checklist_item.rb +++ b/app/models/checklist_item.rb @@ -1,4 +1,5 @@ class ChecklistItem < ApplicationRecord + include ObservableModel attr_accessor :with_paragraphs @@ -22,6 +23,7 @@ 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 @@ -76,4 +78,8 @@ 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/form_field_value.rb b/app/models/form_field_value.rb index 1412219cd..c1cf93054 100644 --- a/app/models/form_field_value.rb +++ b/app/models/form_field_value.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class FormFieldValue < ApplicationRecord + include ObservableModel + belongs_to :form_response belongs_to :form_field belongs_to :created_by, class_name: 'User' @@ -44,4 +46,8 @@ class FormFieldValue < ApplicationRecord errors.add(:value, :not_unique_latest) end + + def run_observers + AutomationObservers::ProtocolContentChangedAutomationObserver.new(form_response.step, created_by).call + end end diff --git a/app/models/result.rb b/app/models/result.rb index a93a4dcce..378677fce 100644 --- a/app/models/result.rb +++ b/app/models/result.rb @@ -44,6 +44,8 @@ class Result < ApplicationRecord CleanupUserSettingsJob.perform_later('result_states', id) end + after_create :run_observers + def self.search(user, include_archived, query = nil, @@ -196,4 +198,10 @@ class Result < ApplicationRecord def delete_step_results step_results.destroy_all end + + private + + def run_observers + AutomationObservers::ResultCreateAutomationObserver.new(my_module, user).call + end end diff --git a/app/models/step.rb b/app/models/step.rb index 7c66f94e2..547d5d19e 100644 --- a/app/models/step.rb +++ b/app/models/step.rb @@ -22,6 +22,7 @@ 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,7 +188,14 @@ class Step < ApplicationRecord private def run_observers - AutomationObservers::AllCheckedStepsAutomationObserver.new(my_module, last_modified_by).call if saved_change_to_completed? && completed + 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) diff --git a/app/models/step_comment.rb b/app/models/step_comment.rb index fabf999ef..a37959ac3 100644 --- a/app/models/step_comment.rb +++ b/app/models/step_comment.rb @@ -1,7 +1,10 @@ # frozen_string_literal: true 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 @@ -16,4 +19,8 @@ class StepComment < Comment def fill_unseen_by 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 + end end diff --git a/app/models/step_orderable_element.rb b/app/models/step_orderable_element.rb index 125b8d578..0ecf9786a 100644 --- a/app/models/step_orderable_element.rb +++ b/app/models/step_orderable_element.rb @@ -4,6 +4,7 @@ class StepOrderableElement < ApplicationRecord 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 @@ -26,4 +27,8 @@ class StepOrderableElement < ApplicationRecord step.normalize_elements_position end end + + def run_observers + AutomationObservers::ProtocolContentChangedAutomationObserver.new(step, step.last_modified_by).call + end end diff --git a/app/models/step_text.rb b/app/models/step_text.rb index 9ae6cabf1..556c11653 100644 --- a/app/models/step_text.rb +++ b/app/models/step_text.rb @@ -3,6 +3,7 @@ class StepText < ApplicationRecord include TinyMceImages include ActionView::Helpers::TextHelper + include ObservableModel auto_strip_attributes :name, nullify: false validates :name, length: { maximum: Constants::NAME_MAX_LENGTH } @@ -38,4 +39,10 @@ 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 2f80a3c9b..0c18a9450 100644 --- a/app/models/table.rb +++ b/app/models/table.rb @@ -3,6 +3,7 @@ class Table < ApplicationRecord include SearchableModel include TableHelper + include ObservableModel auto_strip_attributes :name, nullify: false validates :name, @@ -102,4 +103,8 @@ 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_checked_steps_automation_observer.rb b/app/services/automation_observers/all_completed_steps_automation_observer.rb similarity index 96% rename from app/services/automation_observers/all_checked_steps_automation_observer.rb rename to app/services/automation_observers/all_completed_steps_automation_observer.rb index ce2b0cd47..afc5e7b2b 100644 --- a/app/services/automation_observers/all_checked_steps_automation_observer.rb +++ b/app/services/automation_observers/all_completed_steps_automation_observer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module AutomationObservers - class AllCheckedStepsAutomationObserver + class AllCompletedStepsAutomationObserver def initialize(my_module, user) @my_module = my_module @user = user diff --git a/app/services/automation_observers/completed_step_change_automation_observer.rb b/app/services/automation_observers/completed_step_change_automation_observer.rb new file mode 100644 index 000000000..26390d91c --- /dev/null +++ b/app/services/automation_observers/completed_step_change_automation_observer.rb @@ -0,0 +1,30 @@ +# 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/protocol_content_changed_automation_observer.rb b/app/services/automation_observers/protocol_content_changed_automation_observer.rb new file mode 100644 index 000000000..d23b5177e --- /dev/null +++ b/app/services/automation_observers/protocol_content_changed_automation_observer.rb @@ -0,0 +1,33 @@ +# 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 new file mode 100644 index 000000000..765fcda23 --- /dev/null +++ b/app/services/automation_observers/result_create_automation_observer.rb @@ -0,0 +1,30 @@ +# 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 From aae493c1ed38589229d0919449028af921e45fdd Mon Sep 17 00:00:00 2001 From: Anton Date: Thu, 7 Aug 2025 15:18:34 +0200 Subject: [PATCH 06/20] Improve repository duplicate [SCI-11941] --- app/models/repository.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/repository.rb b/app/models/repository.rb index 98dfe4bf3..434c1431d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -136,7 +136,7 @@ class Repository < RepositoryBase # Clone columns (only if new_repo was saved) repository_columns.find_each do |col| - new_col = col.dup + new_col = col.deep_dup new_col.repository = new_repo new_col.created_by = created_by new_col.save! From afa9f47a4c6089254926a643a70543c8436014b4 Mon Sep 17 00:00:00 2001 From: Anton Date: Fri, 8 Aug 2025 12:41:39 +0200 Subject: [PATCH 07/20] Merge expand/collapse button in protocol --- app/javascript/vue/protocol/container.vue | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/app/javascript/vue/protocol/container.vue b/app/javascript/vue/protocol/container.vue index 6ebd7843a..f7f374483 100644 --- a/app/javascript/vue/protocol/container.vue +++ b/app/javascript/vue/protocol/container.vue @@ -199,13 +199,26 @@ {{ i18n.t("protocols.steps.new_step") }}
- - Date: Fri, 8 Aug 2025 13:51:44 +0200 Subject: [PATCH 08/20] Add activity for import protocol from protocols.io [SCI-12112] --- app/controllers/external_protocols_controller.rb | 14 +++++++++++++- app/views/protocol_importers/_import_form.html.erb | 3 ++- config/initializers/extends.rb | 5 +++-- config/locales/global_activities/en.yml | 2 ++ 4 files changed, 20 insertions(+), 4 deletions(-) diff --git a/app/controllers/external_protocols_controller.rb b/app/controllers/external_protocols_controller.rb index 8bfe34f37..da314c9e5 100644 --- a/app/controllers/external_protocols_controller.rb +++ b/app/controllers/external_protocols_controller.rb @@ -65,7 +65,8 @@ class ExternalProtocolsController < ApplicationController partial: 'protocol_importers/import_form', locals: { protocol: @protocol, steps_json: service_call.serialized_steps, - steps_assets: service_call.steps_assets } + steps_assets: service_call.steps_assets, + source: new_params[:protocol_source] } ), title: t('protocol_importers.new.modal_title', protocol_name: @protocol.name), footer: render_to_string( @@ -88,6 +89,17 @@ class ExternalProtocolsController < ApplicationController ) if service_call.succeed? + if params[:source] == 'protocolsio/v3' + protocol = service_call.protocol + Activities::CreateActivityService + .call(activity_type: :import_protocol_in_repository_from_protocols_io, + owner: current_user, + subject: protocol, + team: protocol.team, + message_items: { + protocol: protocol.id + }) + end message = t('protocols.index.protocolsio.import.success_flash', name: service_call.protocol.name) render json: { protocol: service_call.protocol, message: message } else diff --git a/app/views/protocol_importers/_import_form.html.erb b/app/views/protocol_importers/_import_form.html.erb index cd84bbe80..69780a121 100644 --- a/app/views/protocol_importers/_import_form.html.erb +++ b/app/views/protocol_importers/_import_form.html.erb @@ -35,7 +35,7 @@ <%= f.label :published_on_label, t('protocols.import_export.import_modal.published_on_label'), :"data-e2e" => "e2e-TX-protocolTemplates-previewProtocolsIo-publishedOnLabel" %> - <%= f.text_field :published_on_label, + <%= f.text_field :published_on_label, value: I18n.l(protocol.published_on, format: :full), class: 'form-control', disabled: true, @@ -50,6 +50,7 @@ <%= f.hidden_field(:protocol_type, value: protocol.protocol_type) %> <%= f.hidden_field(:visibility) %> <%= f.hidden_field(:default_public_user_role_id) %> + <%= hidden_field_tag(:source, source) %> <% end %> diff --git a/config/initializers/extends.rb b/config/initializers/extends.rb index a717e24c5..9885948bc 100644 --- a/config/initializers/extends.rb +++ b/config/initializers/extends.rb @@ -622,7 +622,8 @@ class Extends repository_access_revoked_user_group: 405, automation_task_status_changed: 406, automation_experiment_status_changed: 407, - automation_project_status_changed: 408 + automation_project_status_changed: 408, + import_protocol_in_repository_from_protocols_io: 409 } ACTIVITY_GROUPS = { @@ -640,7 +641,7 @@ class Extends protocol_repository: [80, 103, 89, 87, 79, 90, 91, 88, 85, 86, 84, 81, 82, 83, 101, 112, 123, 125, 117, 119, 129, 131, 187, 186, 190, 191, *204..215, 220, 223, 227, 228, 229, *230..235, - *237..240, *253..256, *279..283, 300, 304, 307, 330, *353..355, 360, *387..389], + *237..240, *253..256, *279..283, 300, 304, 307, 330, *353..355, 360, *387..389, 409], team: [92, 94, 93, 97, 104, 244, 245, *379..383], label_templates: [*216..219], storage_locations: [*309..315, 361], diff --git a/config/locales/global_activities/en.yml b/config/locales/global_activities/en.yml index e62b52951..f87aba343 100644 --- a/config/locales/global_activities/en.yml +++ b/config/locales/global_activities/en.yml @@ -424,6 +424,7 @@ en: automation_task_status_changed_html: "%{user} triggered automatic status change from %{my_module_status_old} to %{my_module_status_new} for task %{my_module}." automation_experiment_status_changed_html: "%{user} triggered automatic status change from %{experiment_status_old} to %{experiment_status_new} for experiment %{experiment}." automation_project_status_changed_html: "%{user} triggered automatic status change from %{project_status_old} to %{project_status_new} for project %{project}." + import_protocol_in_repository_from_protocols_io_html: "%{user} imported protocol %{protocol} to Protocol repository from protocols.io." activity_name: create_project: "Project created" edit_project: "Project edited" @@ -803,6 +804,7 @@ en: automation_task_status_changed: "Task status changed automatically" automation_experiment_status_changed: "Experiment status changed automatically" automation_project_status_changed: "Project status changed automatically" + import_protocol_in_repository_from_protocols_io: "Protocol imported from protocols.io" activity_group: projects: "Projects" task_results: "Task results" From 89b196fd4a5e640c512f35f988d8840b88ac7c55 Mon Sep 17 00:00:00 2001 From: Anton Date: Tue, 12 Aug 2025 14:13:01 +0200 Subject: [PATCH 09/20] Show private objects for SA [SCI-12090] --- app/services/smart_annotations/tag_to_html.rb | 27 ++++++++++++++++--- app/services/smart_annotations/tag_to_text.rb | 25 ++++++++++++++--- config/locales/en.yml | 8 ++++++ 3 files changed, 52 insertions(+), 8 deletions(-) diff --git a/app/services/smart_annotations/tag_to_html.rb b/app/services/smart_annotations/tag_to_html.rb index 83d14af2a..4cd1f255a 100644 --- a/app/services/smart_annotations/tag_to_html.rb +++ b/app/services/smart_annotations/tag_to_html.rb @@ -28,9 +28,11 @@ module SmartAnnotations if type == 'rep_item' repository_item(value[:name], user, team, type, object, preview_repository) else - next unless object && SmartAnnotations::PermissionEval.check(user, type, object) - - SmartAnnotations::HtmlPreview.html(nil, type, object) + if object && SmartAnnotations::PermissionEval.check(user, type, object) + SmartAnnotations::HtmlPreview.html(nil, type, object) + else + private_placeholder(object) + end end rescue ActiveRecord::RecordNotFound next @@ -40,7 +42,7 @@ module SmartAnnotations def repository_item(name, user, team, type, object, preview_repository) if object&.repository - return unless SmartAnnotations::PermissionEval.check(user, type, object) + return private_placeholder(object) unless SmartAnnotations::PermissionEval.check(user, type, object) return SmartAnnotations::HtmlPreview.html(nil, type, object, preview_repository) end @@ -62,5 +64,22 @@ module SmartAnnotations end klass.find_by(id: id) end + + def private_placeholder(object = nil) + label = case object + when Project + I18n.t('smart_annotations.private.project') + when Experiment + I18n.t('smart_annotations.private.experiment') + when MyModule + I18n.t('smart_annotations.private.my_module') + when RepositoryRow + I18n.t('smart_annotations.private.repository_row') + else + I18n.t('smart_annotations.private.object') + end + + "#{label}" + end end end diff --git a/app/services/smart_annotations/tag_to_text.rb b/app/services/smart_annotations/tag_to_text.rb index a1d2996bd..a02276047 100644 --- a/app/services/smart_annotations/tag_to_text.rb +++ b/app/services/smart_annotations/tag_to_text.rb @@ -29,9 +29,11 @@ module SmartAnnotations if type == 'rep_item' repository_item(value[:name], user, team, type, object, is_shared_object) else - next unless object && (is_shared_object || SmartAnnotations::PermissionEval.check(user, type, object)) - - SmartAnnotations::TextPreview.text(nil, type, object) + if object && (is_shared_object || SmartAnnotations::PermissionEval.check(user, type, object)) + SmartAnnotations::TextPreview.text(nil, type, object) + else + private_placeholder(object) + end end rescue ActiveRecord::RecordNotFound next @@ -56,7 +58,7 @@ module SmartAnnotations def repository_item(name, user, team, type, object, is_shared_object) if object - return unless is_shared_object || SmartAnnotations::PermissionEval.check(user, type, object) + return private_placeholder(object) unless is_shared_object || SmartAnnotations::PermissionEval.check(user, type, object) return SmartAnnotations::TextPreview.text(nil, type, object) end @@ -78,5 +80,20 @@ module SmartAnnotations end klass.find_by_id(id) end + + def private_placeholder(object = nil) + case object + when Project + I18n.t('smart_annotations.private.project') + when Experiment + I18n.t('smart_annotations.private.experiment') + when MyModule + I18n.t('smart_annotations.private.my_module') + when RepositoryRow + I18n.t('smart_annotations.private.repository_row') + else + I18n.t('smart_annotations.private.object') + end + end end end diff --git a/config/locales/en.yml b/config/locales/en.yml index e4e2b605e..266f9154f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -428,6 +428,14 @@ en: activities: "Activity" archive: "Archived results" + smart_annotations: + private: + project: "#private project" + experiment: "#private experiment" + my_module: "#private task" + repository_row: "#private inventory item" + object: "#private object" + attachments: menu: office_file: "New Microsoft 365 file" From 2175ea3b3fd3af394c21a276ff6b6d4255125100 Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Wed, 13 Aug 2025 15:20:07 +0200 Subject: [PATCH 10/20] 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 From 677d438f85888ab142cf3b97ccd5da2f7303ca8c Mon Sep 17 00:00:00 2001 From: Anton Date: Mon, 18 Aug 2025 12:11:23 +0200 Subject: [PATCH 11/20] Add archived label to results dropdown and add link to results in shared tasks [SCI-12067] --- .../my_module_shareable_links_controller.rb | 11 +++++++++-- app/javascript/vue/protocol/step.vue | 5 ++++- app/javascript/vue/results/result.vue | 4 ++-- app/javascript/vue/results/results.vue | 2 ++ app/models/result.rb | 5 +++++ app/views/shareable_links/my_modules/_step.html.erb | 9 ++++++++- .../my_modules/results/_result.html.erb | 2 +- config/locales/en.yml | 1 + 8 files changed, 32 insertions(+), 7 deletions(-) diff --git a/app/controllers/my_module_shareable_links_controller.rb b/app/controllers/my_module_shareable_links_controller.rb index fe9055901..db7450b4c 100644 --- a/app/controllers/my_module_shareable_links_controller.rb +++ b/app/controllers/my_module_shareable_links_controller.rb @@ -43,8 +43,6 @@ class MyModuleShareableLinksController < ApplicationController @results_order = params[:order] || 'new' @results = @my_module.results.active - @results = @results.page(params[:page]).per(Constants::RESULTS_PER_PAGE_LIMIT) - @results = case @results_order when 'old' then @results.order(created_at: :asc) when 'old_updated' then @results.order(updated_at: :asc) @@ -54,6 +52,15 @@ class MyModuleShareableLinksController < ApplicationController else @results.order(created_at: :desc) end + if params[:result_id].present? + page = @results.find_page_number(params[:result_id].to_i, Constants::RESULTS_PER_PAGE_LIMIT) + params[:result_id] = nil + else + page = params[:page] + end + + @results = @results.page(page).per(Constants::RESULTS_PER_PAGE_LIMIT) + @gallery = @results.left_joins(:assets).pluck('assets.id').compact @disable_smart_annotation_links = true diff --git a/app/javascript/vue/protocol/step.vue b/app/javascript/vue/protocol/step.vue index af27fa4da..39f7018fc 100644 --- a/app/javascript/vue/protocol/step.vue +++ b/app/javascript/vue/protocol/step.vue @@ -105,9 +105,12 @@ :key="result.id" :title="result.name" :href="resultUrl(result.id, result.archived)" - class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer block hover:no-underline text-sn-blue truncate" + class="py-2.5 px-3 hover:bg-sn-super-light-grey cursor-pointer hover:no-underline text-sn-blue truncate flex items-center gap-2" > {{ result.name }} +
+ {{ i18n.t('protocols.steps.archived_result') }} +