diff --git a/app/assets/javascripts/dashboard/current_tasks.js b/app/assets/javascripts/dashboard/current_tasks.js index 27046913a..62f8e8ff7 100644 --- a/app/assets/javascripts/dashboard/current_tasks.js +++ b/app/assets/javascripts/dashboard/current_tasks.js @@ -3,7 +3,7 @@ var DasboardCurrentTasksWidget = (function() { var sortFilter = '.curent-tasks-filters .sort-filter'; - var viewFilter = '.curent-tasks-filters .view-filter'; + var statusFilter = '.curent-tasks-filters .view-filter'; var projectFilter = '.curent-tasks-filters .project-filter'; var experimentFilter = '.curent-tasks-filters .experiment-filter'; @@ -36,7 +36,7 @@ var DasboardCurrentTasksWidget = (function() { params.project_id = dropdownSelector.getValues(projectFilter); params.experiment_id = dropdownSelector.getValues(experimentFilter); params.sort = dropdownSelector.getValues(sortFilter); - params.view = dropdownSelector.getValues(viewFilter); + params.statuses = dropdownSelector.getValues(statusFilter); params.query = $('.current-tasks-widget .task-search-field').val(); params.mode = $('.current-tasks-navbar .active').data('mode'); return params; @@ -48,7 +48,7 @@ var DasboardCurrentTasksWidget = (function() { return dropdownSelector.getValues(experimentFilter) || dropdownSelector.getValues(projectFilter) || $('.current-tasks-widget .task-search-field').val().length > 0 - || dropdownSelector.getValues(viewFilter) !== 'uncompleted'; + || dropdownSelector.getValues(statusFilter) !== 'uncompleted'; } function loadCurrentTasksList(newList) { @@ -57,7 +57,7 @@ var DasboardCurrentTasksWidget = (function() { project_id: dropdownSelector.getValues(projectFilter), experiment_id: dropdownSelector.getValues(experimentFilter), sort: dropdownSelector.getValues(sortFilter), - view: dropdownSelector.getValues(viewFilter), + statuses: dropdownSelector.getValues(statusFilter), query: $('.current-tasks-widget .task-search-field').val(), mode: $('.current-tasks-navbar .active').data('mode') }; @@ -85,7 +85,6 @@ var DasboardCurrentTasksWidget = (function() { e.stopPropagation(); e.preventDefault(); dropdownSelector.selectValue(sortFilter, 'due_date'); - dropdownSelector.selectValue(viewFilter, 'uncompleted'); dropdownSelector.clearData(projectFilter); dropdownSelector.clearData(experimentFilter); }); @@ -98,12 +97,9 @@ var DasboardCurrentTasksWidget = (function() { disableSearch: true }); - dropdownSelector.init(viewFilter, { - noEmptyOption: true, - singleSelect: true, - closeOnSelect: true, + dropdownSelector.init(statusFilter, { selectAppearance: 'simple', - disableSearch: true + optionClass: 'checkbox-icon' }); dropdownSelector.init(projectFilter, { @@ -143,7 +139,7 @@ var DasboardCurrentTasksWidget = (function() { e.stopPropagation(); e.preventDefault(); dropdownSelector.closeDropdown(sortFilter); - dropdownSelector.closeDropdown(viewFilter); + dropdownSelector.closeDropdown(statusFilter); dropdownSelector.closeDropdown(projectFilter); dropdownSelector.closeDropdown(experimentFilter); }); diff --git a/app/assets/javascripts/my_modules.js b/app/assets/javascripts/my_modules.js index bb2505b1a..562625293 100644 --- a/app/assets/javascripts/my_modules.js +++ b/app/assets/javascripts/my_modules.js @@ -1,4 +1,4 @@ -/* global I18n dropdownSelector */ +/* global I18n dropdownSelector HelperModule animateSpinner */ /* eslint-disable no-use-before-define */ function initTaskCollapseState() { @@ -226,34 +226,33 @@ function bindEditTagsAjax() { }); } -// Sets callback for completing/uncompleting task -function applyTaskCompletedCallBack() { - $("[data-action='complete-task'], [data-action='uncomplete-task']") - .on('click', function() { - var button = $(this); - $.ajax({ - url: button.data('link-url'), - type: 'POST', - dataType: 'json', - success: function(data) { - if (data.completed === true) { - button.attr('data-action', 'uncomplete-task'); - button.find('.btn') - .removeClass('btn-primary').addClass('btn-default'); - } else { - button.attr('data-action', 'complete-task'); - button.find('.btn') - .removeClass('btn-default').addClass('btn-primary'); - } - $('#dueDateContainer').html(data.module_header_due_date); - initDueDatePicker(); - $('.task-state-label').html(data.module_state_label); - button.find('button').replaceWith(data.new_btn); - }, - error: function() { +function applyTaskStatusChangeCallBack() { + $('.task-flows').on('click', '#dropdownTaskFlowList > li[data-state-id]', function() { + var list = $('#dropdownTaskFlowList'); + var item = $(this); + var container = list.closest('.task-flows'); + animateSpinner(); + $.ajax({ + url: list.data('link-url'), + type: 'PATCH', + dataType: 'json', + data: { my_module: { status_id: item.data('state-id') } }, + success: function(data) { + container.html(data.content); + animateSpinner(null, false); + }, + error: function(e) { + animateSpinner(null, false); + if (e.status === 403) { + HelperModule.flashAlertMsg(I18n.t('my_module_statuses.update_status.error.no_permission'), 'danger'); + } else if (e.status === 422) { + HelperModule.flashAlertMsg(e.errors, 'danger'); + } else { + HelperModule.flashAlertMsg('error', 'danger'); } - }); + } }); + }); } function initTagsSelector() { @@ -380,7 +379,7 @@ function initAssignedUsersSelector() { } initTaskCollapseState(); -applyTaskCompletedCallBack(); +applyTaskStatusChangeCallBack(); initTagsSelector(); bindEditTagsAjax(); initStartDatePicker(); diff --git a/app/assets/javascripts/my_modules/status_flow.js b/app/assets/javascripts/my_modules/status_flow.js new file mode 100644 index 000000000..bc283d47b --- /dev/null +++ b/app/assets/javascripts/my_modules/status_flow.js @@ -0,0 +1,16 @@ +/* global animateSpinner */ + +(function() { + $('.task-flows').on('click', '#viewTaskFlow', function() { + $('#statusFlowModal').modal('show'); + }); + + $('#statusFlowModal').on('show.bs.modal', function() { + var $modalBody = $(this).find('.modal-body'); + animateSpinner($modalBody); + $.get($(this).data('status-flow-url'), function(result) { + animateSpinner($modalBody, false); + $modalBody.html(result.html); + }); + }); +}()); diff --git a/app/assets/javascripts/sitewide/dropdown_selector.js b/app/assets/javascripts/sitewide/dropdown_selector.js index da29489a9..4c045dbe4 100644 --- a/app/assets/javascripts/sitewide/dropdown_selector.js +++ b/app/assets/javascripts/sitewide/dropdown_selector.js @@ -179,9 +179,20 @@ var dropdownSelector = (function() { } // Add selected option to value - function addSelectedOption(selector, container) { - setData(selector, [convertOptionToJson($(selector).find('option:selected')[0])], true); + function addSelectedOptions(selector, container) { + var selectedOptions = []; + $.each($(selector).find('option:selected'), function(i, option) { + selectedOptions.push(convertOptionToJson(option)); + if (selector.data('config').singleSelect) return false; + return true; + }); + + if (!selectedOptions.length) return false; + + setData(selector, selectedOptions, true); + return true; } + // // Prepare custom dropdown icon function prepareCustomDropdownIcon(config) { @@ -422,8 +433,8 @@ var dropdownSelector = (function() { } // Select default value - if (config.noEmptyOption && config.singleSelect) { - addSelectedOption(selectElement, dropdownContainer); + if (!selectElement.data('ajax-url')) { + addSelectedOptions(selectElement, dropdownContainer); } // Enable simple mode for dropdown selector diff --git a/app/assets/stylesheets/my_modules/protocols/index.scss b/app/assets/stylesheets/my_modules/protocols/index.scss index 8a0782a75..bde9da234 100644 --- a/app/assets/stylesheets/my_modules/protocols/index.scss +++ b/app/assets/stylesheets/my_modules/protocols/index.scss @@ -503,6 +503,26 @@ } } +.task-information { + column-gap: 1em; + display: grid; + grid-template-columns: auto max-content; + + .task-section-header { + grid-column: 1 / span 1; + } + + .task-details { + grid-column: 1 / span 1; + grid-row: 2 / span 1; + } + + .task-flows { + grid-column: 2 / span 1; + grid-row: 1 / span 2; + } +} + @media (max-width: 700px) { .my-module-protocol-status { .status-info-dropdown { @@ -518,4 +538,18 @@ } } } + + .task-information { + grid-template-columns: auto; + row-gap: .5em; + + .task-details { + grid-row: 3 / span 1; + } + + .task-flows { + grid-column: unset; + grid-row: 2 / span 1; + } + } } diff --git a/app/assets/stylesheets/my_modules/status_flow.scss b/app/assets/stylesheets/my_modules/status_flow.scss new file mode 100644 index 000000000..81e8a93f0 --- /dev/null +++ b/app/assets/stylesheets/my_modules/status_flow.scss @@ -0,0 +1,92 @@ +// scss-lint:disable SelectorDepth +// scss-lint:disable NestingDepth +// scss-lint:disable SelectorFormat +// scss-lint:disable ImportantRule + +@import "constants"; +@import "mixins"; + +.content-pane.my-modules-protocols-index { + .status-flow-dropdown { + .dropdown-toggle { + color: $color-white; + text-align: left; + width: 15em; + + .caret { + margin: 8px 0; + } + } + + .dropdown-menu > li { + line-height: 35px; + } + } +} + + +#statusFlowModal { + .status-flow { + padding: 2em; + + .status-container { + align-items: center; + display: grid; + grid-template-columns: 1fr min-content 1fr; + justify-content: space-around; + position: relative; + + .current-status { + @include font-small; + justify-self: end; + + .fas { + margin: 0 .5em; + } + } + + .status-block { + border-radius: $border-radius-tag; + color: $color-white; + font-weight: bold; + line-height: 1em; + padding: .5em; + white-space: nowrap; + } + + .status-comment { + @include font-small; + color: $color-silver-chalice; + padding-left: .5em; + } + } + + .connector { + background: $color-black; + height: 2em; + margin: 0 auto; + position: relative; + width: 2px; + + &:before, + &:after { + border-left: .2em solid transparent; + border-right: .2em solid transparent; + content: ''; + display: block; + margin-left: -.1em; + position: absolute; + } + + &:before { + border-top: .2em solid $color-black; + top: 0; + } + + &:after { + border-bottom: .2em solid $color-black; + bottom: 0; + } + } + } +} diff --git a/app/assets/stylesheets/projects.scss b/app/assets/stylesheets/projects.scss index 19e9fa154..c9125f939 100644 --- a/app/assets/stylesheets/projects.scss +++ b/app/assets/stylesheets/projects.scss @@ -316,10 +316,10 @@ path, ._jsPlumb_endpoint { .module-large .tags-container, .module-medium .tags-container { - padding-top: 2px; + padding-top: 4px; div { - font-size: 22pt; + font-size: 20px; width: 4px; height: 0px; display: inline-block; @@ -335,9 +335,9 @@ path, ._jsPlumb_endpoint { } & span.badge { - margin-left: -8px; - margin-top: -10px; + margin-left: -12px; margin-right: 4px; + margin-top: -7px; } } diff --git a/app/assets/stylesheets/reports.scss b/app/assets/stylesheets/reports.scss index c3d07e9c1..e802b82c9 100644 --- a/app/assets/stylesheets/reports.scss +++ b/app/assets/stylesheets/reports.scss @@ -280,10 +280,17 @@ label { .module-start-date, .module-due-date { - margin-left: 5px; white-space: nowrap; } + .module-status { + .status-block { + border-radius: $border-radius-tag; + color: $color-white; + padding: 2px 4px; + } + } + .module-tags { margin-left: 0; margin-top: 10px; @@ -389,6 +396,16 @@ label { &:hover > .report-element-body .step-name { color: $brand-primary; } + + .step-label-default { + @include font-h3; + color: $color-alto; + } + + .step-label-success { + @include font-h3; + color: $brand-success; + } } /* Step attachment style (table, asset or checklist) */ diff --git a/app/assets/stylesheets/shared_styles/elements/dropdown.scss b/app/assets/stylesheets/shared_styles/elements/dropdown.scss index fbe970ac0..fc248e3da 100644 --- a/app/assets/stylesheets/shared_styles/elements/dropdown.scss +++ b/app/assets/stylesheets/shared_styles/elements/dropdown.scss @@ -16,7 +16,7 @@ border-color: $brand-focus; .caret { - transform: rotateX(180deg) + transform: rotateX(180deg); } } diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 42f54a018..fff2c85c3 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -742,6 +742,44 @@ ul.double-line > li { } } +#canvas-container { + .panel-heading { + padding: 10px 15px 4px; + } + + .panel-body { + padding: 6px 15px; + + .status-label { + background-color: var(--state-color); + color: $color-white; + margin: 3px 0; + padding: 2px 8px; + white-space: nowrap; + width: fit-content; + } + } + + .panel-footer { + .nav > li > a { + padding: 6px 15px; + } + + .btn { + height: 30px; + } + + .badge-indicator { + background: transparent; + color: $color-silver-chalice; + font-size: 12px; + margin-left: 0; + padding: 0; + top: 0; + } + } +} + .panel-options { position: relative; bottom: 8px; diff --git a/app/controllers/dashboard/current_tasks_controller.rb b/app/controllers/dashboard/current_tasks_controller.rb index 30cd0b22a..b8741d667 100644 --- a/app/controllers/dashboard/current_tasks_controller.rb +++ b/app/controllers/dashboard/current_tasks_controller.rb @@ -25,7 +25,7 @@ module Dashboard tasks = tasks.left_outer_joins(:user_my_modules).where(user_my_modules: { user_id: current_user.id }) end - tasks = filter_by_state(tasks) + #tasks = filter_by_state(tasks) case task_filters[:sort] when 'start_date' diff --git a/app/controllers/my_module_status_flow_controller.rb b/app/controllers/my_module_status_flow_controller.rb new file mode 100644 index 000000000..eb738f8db --- /dev/null +++ b/app/controllers/my_module_status_flow_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +class MyModuleStatusFlowController < ApplicationController + before_action :load_my_module + before_action :check_view_permissions + + def show + my_module_statuses = @my_module.my_module_status_flow.my_module_statuses.sort_by_position + render json: { html: render_to_string(partial: 'my_modules/modals/status_flow_modal_body.html.erb', + locals: { my_module_statuses: my_module_statuses }) } + end + + private + + def load_my_module + @my_module = MyModule.find_by(id: params[:my_module_id]) + render_404 unless @my_module + end + + def check_view_permissions + render_403 unless can_read_experiment?(@my_module.experiment) + end +end diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 6f2e8c931..e0d1e9279 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -13,7 +13,7 @@ class MyModulesController < ApplicationController before_action :check_manage_permissions, only: %i(description due_date update_description update_protocol_description) before_action :check_view_permissions, except: %i(update update_description update_protocol_description toggle_task_state) - before_action :check_complete_module_permission, only: %i(complete_my_module toggle_task_state) + before_action :check_update_state_permissions, only: :update_state before_action :set_inline_name_editing, only: %i(protocols results activities archive) layout 'fluid'.freeze @@ -259,99 +259,24 @@ class MyModulesController < ApplicationController def archive @archived_results = @my_module.archived_results - current_team_switch(@my_module - .experiment - .project - .team) + current_team_switch(@my_module.experiment.project.team) end - # Complete/uncomplete task - def toggle_task_state - respond_to do |format| - @my_module.completed? ? @my_module.uncompleted! : @my_module.completed! - task_completion_activity + def update_state + new_status = @my_module.my_module_status_flow.my_module_statuses.find_by(id: update_status_params[:status_id]) + return render_404 unless new_status - # Render new button HTML - new_btn_partial = if @my_module.completed? - 'my_modules/state_button_uncomplete.html.erb' - else - 'my_modules/state_button_complete.html.erb' - end + @my_module.update(my_module_status: new_status) - format.json do - render json: { - new_btn: render_to_string(partial: new_btn_partial), - completed: @my_module.completed?, - module_header_due_date: render_to_string( - partial: 'my_modules/module_header_due_date.html.erb', - locals: { my_module: @my_module } - ), - module_state_label: render_to_string( - partial: 'my_modules/module_state_label.html.erb', - locals: { my_module: @my_module } - ) - } - end - end - end + render json: { content: render_to_string( + partial: 'my_modules/status_flow/task_flow_button.html.erb', + locals: { my_module: @my_module }) + }, status: :ok - def complete_my_module - respond_to do |format| - if @my_module.uncompleted? && @my_module.check_completness_status - @my_module.completed! - task_completion_activity - format.json do - render json: { - task_button_title: t('my_modules.buttons.uncomplete'), - module_header_due_date: render_to_string( - partial: 'my_modules/module_header_due_date.html.erb', - locals: { my_module: @my_module } - ), - module_state_label: render_to_string( - partial: 'my_modules/module_state_label.html.erb', - locals: { my_module: @my_module } - ) - }, status: :ok - end - else - format.json { render json: {}, status: :unprocessable_entity } - end - end end private - def task_completion_activity - completed = @my_module.completed? - log_activity(completed ? :complete_task : :uncomplete_task) - start_work_on_next_task_notification - end - - def start_work_on_next_task_notification - if @my_module.completed? - title = t('notifications.start_work_on_next_task', - user: current_user.full_name, - module: @my_module.name) - message = t('notifications.start_work_on_next_task_message', - project: link_to(@project.name, project_url(@project)), - experiment: link_to(@experiment.name, - canvas_experiment_url(@experiment)), - my_module: link_to(@my_module.name, - protocols_my_module_url(@my_module))) - notification = Notification.create( - type_of: :recent_changes, - title: sanitize_input(title, %w(strong a)), - message: sanitize_input(message, %w(strong a)), - generator_user_id: current_user.id - ) - # create notification for all users on the next modules in the workflow - @my_module.my_modules.map(&:users).flatten.uniq.each do |target_user| - next if target_user == current_user || !target_user.recent_notification - UserNotification.create(notification: notification, user: target_user) - end - end - end - def load_vars @my_module = MyModule.find_by_id(params[:id]) if @my_module @@ -384,8 +309,9 @@ class MyModulesController < ApplicationController render_403 unless can_read_experiment?(@my_module.experiment) end - def check_complete_module_permission - render_403 unless can_complete_module?(@my_module) + def check_update_state_permissions + return render_403 unless can_change_my_module_flow_status?(@my_module) + render_404 unless @my_module.my_module_status end def set_inline_name_editing @@ -414,6 +340,10 @@ class MyModulesController < ApplicationController update_params end + def update_status_params + params.require(:my_module).permit(:status_id) + end + def log_start_date_change_activity(start_date_changes) type_of = if start_date_changes[0].nil? # set started_on message_items = { my_module_started_on: @my_module.started_on } diff --git a/app/helpers/reports_helper.rb b/app/helpers/reports_helper.rb index b442f1b56..23f96ffc3 100644 --- a/app/helpers/reports_helper.rb +++ b/app/helpers/reports_helper.rb @@ -154,7 +154,7 @@ module ReportsHelper style = 'default' text = t('protocols.steps.uncompleted') end - "#{text}".html_safe + "[#{text}]".html_safe end # Fixes issues with avatar images in reports diff --git a/app/models/my_module.rb b/app/models/my_module.rb index 2ed62b6d2..537999eb8 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -8,6 +8,8 @@ class MyModule < ApplicationRecord before_create :create_blank_protocol before_validation :set_completed_on, if: :state_changed? + before_create :assign_default_status_flow + before_save :exec_status_consequences, if: :my_module_status_id_changed? auto_strip_attributes :name, :description, nullify: false validates :name, @@ -20,6 +22,9 @@ class MyModule < ApplicationRecord validate :coordinates_uniqueness_check, if: :active? validates :completed_on, presence: true, if: proc { |mm| mm.completed? } + validate :check_status_conditions, if: :my_module_status_id_changed? + validate :check_status_implications, unless: :my_module_status_id_changed? + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User', @@ -38,6 +43,8 @@ class MyModule < ApplicationRecord optional: true belongs_to :experiment, inverse_of: :my_modules, touch: true belongs_to :my_module_group, inverse_of: :my_modules, optional: true + belongs_to :my_module_status, optional: true + delegate :my_module_status_flow, to: :my_module_status, allow_nil: true has_many :results, inverse_of: :my_module, dependent: :destroy has_many :my_module_tags, inverse_of: :my_module, dependent: :destroy has_many :tags, through: :my_module_tags @@ -375,40 +382,6 @@ class MyModule < ApplicationRecord final end - - # Generate the samples belonging to this module - # in JSON form, suitable for display in handsontable.js - def samples_json_hot(order) - data = [] - samples.order(created_at: order).each do |sample| - sample_json = [] - sample_json << sample.name - if sample.sample_type.present? - sample_json << sample.sample_type.name - else - sample_json << I18n.t("samples.table.no_type") - end - if sample.sample_group.present? - sample_json << sample.sample_group.name - else - sample_json << I18n.t("samples.table.no_group") - end - sample_json << I18n.l(sample.created_at, format: :full) - sample_json << sample.user.full_name - data << sample_json - end - - # Prepare column headers - headers = [ - I18n.t("samples.table.sample_name"), - I18n.t("samples.table.sample_type"), - I18n.t("samples.table.sample_group"), - I18n.t("samples.table.added_on"), - I18n.t("samples.table.added_by") - ] - { data: data, headers: headers } - end - # Generate the repository rows belonging to this module # in JSON form, suitable for display in handsontable.js def repository_json_hot(repository, order) @@ -552,4 +525,34 @@ class MyModule < ApplicationRecord errors.add(:position, I18n.t('activerecord.errors.models.my_module.attributes.position.not_unique')) end end + + def assign_default_status_flow + return unless MyModuleStatusFlow.global.any? + + self.my_module_status = MyModuleStatusFlow.global.first.initial_status + end + + def check_status_conditions + return if my_module_status.blank? + + my_module_status.my_module_status_conditions.each do |condition| + condition.call(self) + end + end + + def check_status_implications + return if my_module_status.blank? + + my_module_status.my_module_status_implications.each do |implication| + implication.call(self) + end + end + + def exec_status_consequences + return if my_module_status.blank? + + my_module_status.my_module_status_consequences.each do |consequence| + consequence.call(self) + end + end end diff --git a/app/models/my_module_status.rb b/app/models/my_module_status.rb new file mode 100644 index 000000000..101606c40 --- /dev/null +++ b/app/models/my_module_status.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +class MyModuleStatus < ApplicationRecord + has_many :my_modules, dependent: :nullify + has_many :my_module_status_conditions, dependent: :destroy + has_many :my_module_status_consequences, dependent: :destroy + has_many :my_module_status_implications, dependent: :destroy + belongs_to :my_module_status_flow + belongs_to :created_by, class_name: 'User', optional: true + belongs_to :last_modified_by, class_name: 'User', optional: true + has_one :next_status, class_name: 'MyModuleStatus', + foreign_key: 'previous_status_id', + inverse_of: :previous_status, + dependent: :nullify + belongs_to :previous_status, class_name: 'MyModuleStatus', inverse_of: :next_status, optional: true + + validates :name, presence: true, length: { minimum: Constants::NAME_MIN_LENGTH, maximum: Constants::NAME_MAX_LENGTH } + validates :color, presence: true + validates :description, length: { maximum: Constants::TEXT_MAX_LENGTH } + validates :next_status, uniqueness: true, if: -> { next_status.present? } + validates :previous_status, uniqueness: true, if: -> { previous_status.present? } + validate :next_in_same_flow, if: -> { next_status.present? } + validate :previous_in_same_flow, if: -> { previous_status.present? } + + def initial_status? + my_module_status_flow.initial_status == self + end + + def final_status? + my_module_status_flow.final_status == self + end + + def self.sort_by_position(order = :asc) + ordered_statuses, statuses = all.to_a.partition { |i| i.previous_status_id.nil? } + + return [] if ordered_statuses.empty? + + until statuses.empty? + next_element, statuses = statuses.partition { |i| ordered_statuses.last.id == i.previous_status_id } + if next_element.empty? + break + else + ordered_statuses.concat(next_element) + end + end + ordered_statuses = ordered_statuses.reverse if order == :desc + ordered_statuses + end + + private + + def next_in_same_flow + errors.add(:next_status, :different_flow) unless next_status.my_module_status_flow == my_module_status_flow + end + + def previous_in_same_flow + errors.add(:previous_status, :different_flow) unless previous_status.my_module_status_flow == my_module_status_flow + end +end diff --git a/app/models/my_module_status_condition.rb b/app/models/my_module_status_condition.rb new file mode 100644 index 000000000..7bc18740e --- /dev/null +++ b/app/models/my_module_status_condition.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MyModuleStatusCondition < ApplicationRecord + belongs_to :my_module_status +end diff --git a/app/models/my_module_status_conditions/active.rb b/app/models/my_module_status_conditions/active.rb new file mode 100644 index 000000000..887eaa739 --- /dev/null +++ b/app/models/my_module_status_conditions/active.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# Just an example, to be replaced with an actual implementation +module MyModuleStatusConditions + class Active < MyModuleStatusCondition + def call(my_module) + my_module.errors.add(:status_conditions, 'MyModule should be active') unless my_module.active? + end + end +end diff --git a/app/models/my_module_status_consequence.rb b/app/models/my_module_status_consequence.rb new file mode 100644 index 000000000..4aa31924d --- /dev/null +++ b/app/models/my_module_status_consequence.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MyModuleStatusConsequence < ApplicationRecord + belongs_to :my_module_status +end diff --git a/app/models/my_module_status_consequences/change_activity.rb b/app/models/my_module_status_consequences/change_activity.rb new file mode 100644 index 000000000..3bc736f4d --- /dev/null +++ b/app/models/my_module_status_consequences/change_activity.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Just an example, to be replaced with an actual implementation +module MyModuleStatusConsequences + class ChangeActivity < MyModuleStatusConsequence + def call(my_module) + # Create new activity here + puts "State changed to #{my_module_status.name}} for #{my_module.name}" + end + end +end diff --git a/app/models/my_module_status_flow.rb b/app/models/my_module_status_flow.rb new file mode 100644 index 000000000..2dbb3e019 --- /dev/null +++ b/app/models/my_module_status_flow.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class MyModuleStatusFlow < ApplicationRecord + enum visibility: { global: 0, in_team: 1 } + + has_many :my_module_statuses, dependent: :destroy + belongs_to :team, optional: true + belongs_to :created_by, class_name: 'User', optional: true + belongs_to :last_modified_by, class_name: 'User', optional: true + + validates :visibility, presence: true + validates :team, presence: true, if: :in_team? + validates :name, uniqueness: { scope: :team_id, case_sensitive: false }, if: :in_team? + validates :name, presence: true, length: { minimum: Constants::NAME_MIN_LENGTH, maximum: Constants::NAME_MAX_LENGTH } + validates :description, length: { maximum: Constants::TEXT_MAX_LENGTH } + + def initial_status + my_module_statuses.find_by(previous_status: nil) + end + + def final_status + my_module_statuses.left_outer_joins(:next_status).find_by('next_statuses_my_module_statuses.id': nil) + end +end diff --git a/app/models/my_module_status_implication.rb b/app/models/my_module_status_implication.rb new file mode 100644 index 000000000..a455500d9 --- /dev/null +++ b/app/models/my_module_status_implication.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class MyModuleStatusImplication < ApplicationRecord + belongs_to :my_module_status +end diff --git a/app/models/my_module_status_implications/read_only.rb b/app/models/my_module_status_implications/read_only.rb new file mode 100644 index 000000000..df9c5a2a2 --- /dev/null +++ b/app/models/my_module_status_implications/read_only.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# Just an example, to be replaced with an actual implementation +module MyModuleStatusImplications + class ReadOnly < MyModuleStatusImplication + def call(my_module) + my_module.errors.add(:status_implication, 'Is read only') + false + end + end +end diff --git a/app/permissions/experiment.rb b/app/permissions/experiment.rb index ec4909f27..8cf52fe4b 100644 --- a/app/permissions/experiment.rb +++ b/app/permissions/experiment.rb @@ -25,7 +25,14 @@ Canaid::Permissions.register_for(Experiment) do # module: create, copy, reposition, create/update/delete connection, # assign/reassign/unassign tags can :manage_experiment do |user, experiment| - user.is_user_or_higher_of_project?(experiment.project) + user.is_user_or_higher_of_project?(experiment.project) && + MyModule.joins(:experiment).where(experiment: experiment).all? do |my_module| + if my_module.my_module_status + my_module.my_module_status.my_module_status_implications.all? { |implication| implication.call(my_module) } + else + true + end + end end # experiment: archive @@ -53,79 +60,6 @@ Canaid::Permissions.register_for(Experiment) do end end -Canaid::Permissions.register_for(MyModule) do - # Module, its experiment and its project must be active for all the specified - # permissions - %i(manage_module - manage_users_in_module - assign_repository_rows_to_module - assign_sample_to_module - complete_module - create_comments_in_module - create_my_module_repository_snapshot - manage_my_module_repository_snapshots) - .each do |perm| - can perm do |_, my_module| - my_module.active? && - my_module.experiment.active? && - my_module.experiment.project.active? - end - end - - # module: update, archive, move - # result: create, update - can :manage_module do |user, my_module| - can_manage_experiment?(user, my_module.experiment) - end - - # NOTE: Must not be dependent on canaid parmision for which we check if it's - # active - # module: restore - can :restore_module do |user, my_module| - user.is_user_or_higher_of_project?(my_module.experiment.project) && - my_module.archived? - end - - # module: assign/reassign/unassign users - can :manage_users_in_module do |user, my_module| - user.is_owner_of_project?(my_module.experiment.project) - end - - # module: assign/unassign repository record - # NOTE: Use 'module_page? &&' before calling this permission! - can :assign_repository_rows_to_module do |user, my_module| - user.is_technician_or_higher_of_project?(my_module.experiment.project) - end - - # module: assign/unassign sample - # NOTE: Use 'module_page? &&' before calling this permission! - can :assign_sample_to_module do |user, my_module| - user.is_technician_or_higher_of_project?(my_module.experiment.project) - end - - # module: complete/uncomplete - can :complete_module do |user, my_module| - user.is_technician_or_higher_of_project?(my_module.experiment.project) - end - - # module: create comment - # result: create comment - # step: create comment - can :create_comments_in_module do |user, my_module| - can_create_comments_in_project?(user, my_module.experiment.project) - end - - # module: create a snapshot of repository item - can :create_my_module_repository_snapshot do |user, my_module| - user.is_technician_or_higher_of_project?(my_module.experiment.project) - end - - # module: make a repository snapshot selected - can :manage_my_module_repository_snapshots do |user, my_module| - user.is_technician_or_higher_of_project?(my_module.experiment.project) - end -end - Canaid::Permissions.register_for(Protocol) do # Protocol needs to be in a module for all Protocol permissions below # experiment level @@ -167,7 +101,7 @@ Canaid::Permissions.register_for(Protocol) do # step: complete/uncomplete can :complete_or_checkbox_step do |user, protocol| - can_complete_module?(user, protocol.my_module) + can_change_my_module_flow_status?(user, protocol.my_module) end end diff --git a/app/permissions/my_module.rb b/app/permissions/my_module.rb new file mode 100644 index 000000000..c3fa763fd --- /dev/null +++ b/app/permissions/my_module.rb @@ -0,0 +1,72 @@ +Canaid::Permissions.register_for(MyModule) do + # Module, its experiment and its project must be active for all the specified + # permissions + %i(manage_module + manage_users_in_module + assign_repository_rows_to_module + assign_sample_to_module + change_my_module_flow_status + create_comments_in_module + create_my_module_repository_snapshot + manage_my_module_repository_snapshots) + .each do |perm| + can perm do |_, my_module| + my_module.active? && + my_module.experiment.active? && + my_module.experiment.project.active? + end + end + + # module: update, archive, move + # result: create, update + can :manage_module do |user, my_module| + can_manage_experiment?(user, my_module.experiment) + end + + # NOTE: Must not be dependent on canaid parmision for which we check if it's + # active + # module: restore + can :restore_module do |user, my_module| + user.is_user_or_higher_of_project?(my_module.experiment.project) && + my_module.archived? + end + + # module: assign/reassign/unassign users + can :manage_users_in_module do |user, my_module| + user.is_owner_of_project?(my_module.experiment.project) + end + + # module: assign/unassign repository record + # NOTE: Use 'module_page? &&' before calling this permission! + can :assign_repository_rows_to_module do |user, my_module| + user.is_technician_or_higher_of_project?(my_module.experiment.project) + end + + # module: assign/unassign sample + # NOTE: Use 'module_page? &&' before calling this permission! + can :assign_sample_to_module do |user, my_module| + user.is_technician_or_higher_of_project?(my_module.experiment.project) + end + + # module: change_flow_status + can :change_my_module_flow_status do |user, my_module| + user.is_technician_or_higher_of_project?(my_module.experiment.project) + end + + # module: create comment + # result: create comment + # step: create comment + can :create_comments_in_module do |user, my_module| + can_create_comments_in_project?(user, my_module.experiment.project) + end + + # module: create a snapshot of repository item + can :create_my_module_repository_snapshot do |user, my_module| + user.is_technician_or_higher_of_project?(my_module.experiment.project) + end + + # module: make a repository snapshot selected + can :manage_my_module_repository_snapshots do |user, my_module| + user.is_technician_or_higher_of_project?(my_module.experiment.project) + end +end diff --git a/app/permissions/project.rb b/app/permissions/project.rb index 3747d438c..5b026e637 100644 --- a/app/permissions/project.rb +++ b/app/permissions/project.rb @@ -37,7 +37,14 @@ Canaid::Permissions.register_for(Project) do # project: update/delete, assign/reassign/unassign users can :manage_project do |user, project| - user.is_owner_of_project?(project) + user.is_owner_of_project?(project) && + MyModule.joins(experiment: :project).where(experiments: { project: project }).all? do |my_module| + if my_module.my_module_status + my_module.my_module_status.my_module_status_implications.all? { |implication| implication.call(my_module) } + else + true + end + end end # project: archive diff --git a/app/services/reports/docx/draw_my_module.rb b/app/services/reports/docx/draw_my_module.rb index c7640a5cc..04edfeccf 100644 --- a/app/services/reports/docx/draw_my_module.rb +++ b/app/services/reports/docx/draw_my_module.rb @@ -13,17 +13,6 @@ module Reports::Docx::DrawMyModule @docx.p do text I18n.t('projects.reports.elements.module.user_time', timestamp: I18n.l(my_module.created_at, format: :full)), color: color[:gray] - text ' | ' - if my_module.due_date.present? - text I18n.t('projects.reports.elements.module.due_date', - due_date: I18n.l(my_module.due_date, format: :full)), color: color[:gray] - else - text I18n.t('projects.reports.elements.module.no_due_date'), color: color[:gray] - end - if my_module.completed? - text " #{I18n.t('my_modules.states.completed')}", bold: true, color: color[:green] - text " #{I18n.l(my_module.completed_on, format: :full)}", color: color[:gray] - end if my_module.archived? text ' | ' text I18n.t('search.index.archived'), color: color[:gray] @@ -33,15 +22,34 @@ module Reports::Docx::DrawMyModule scinote_url + Rails.application.routes.url_helpers.protocols_my_module_path(my_module), link_style end - if my_module.description.present? - html = custom_auto_link(my_module.description, team: @report_team) - html_to_word_converter(html) - else - @docx.p I18n.t 'projects.reports.elements.module.no_description' + + @docx.p do + if my_module.started_on.present? + text I18n.t('projects.reports.elements.module.started_on', + started_on: I18n.l(my_module.started_on, format: :full)) + else + text I18n.t('projects.reports.elements.module.no_due_date') + end end @docx.p do - text I18n.t 'projects.reports.elements.module.tags_header' + if my_module.due_date.present? + text I18n.t('projects.reports.elements.module.due_date', + due_date: I18n.l(my_module.due_date, format: :full)) + else + text I18n.t('projects.reports.elements.module.no_due_date') + end + end + + status = my_module.my_module_status + @docx.p do + text I18n.t('projects.reports.elements.module.status') + text ' ' + text "[#{status.name}]", color: status.color.delete('#') + end + + @docx.p do + text I18n.t('projects.reports.elements.module.tags_header') if tags.any? my_module.tags.each do |tag| text ' ' @@ -49,10 +57,17 @@ module Reports::Docx::DrawMyModule end else text ' ' - text I18n.t 'projects.reports.elements.module.no_tags' + text I18n.t('projects.reports.elements.module.no_tags') end end + if my_module.description.present? + html = custom_auto_link(my_module.description, team: @report_team) + html_to_word_converter(html) + else + @docx.p I18n.t('projects.reports.elements.module.no_description') + end + @docx.p subject['children'].each do |child| public_send("draw_#{child['type_of']}", child, my_module) diff --git a/app/views/canvas/full_zoom/_my_module.html.erb b/app/views/canvas/full_zoom/_my_module.html.erb index 314f8e38f..3f4ef6fa7 100644 --- a/app/views/canvas/full_zoom/_my_module.html.erb +++ b/app/views/canvas/full_zoom/_my_module.html.erb @@ -37,6 +37,9 @@ <% else %> <%= render partial: "my_modules/card_due_date_label.html.erb", locals: { my_module: my_module, format: :full_date } %> <% end %> +
+ Completed +
- " + data-select-multiple-all-selected="<%= t("dashboard.current_tasks.filter.statuses.all_selected") %>" + data-select-multiple-name="<%= t("dashboard.current_tasks.filter.statuses.selected") %>" + multiple + > + <% MyModuleStatusFlow.find_each do |status_flow| %> + <% status_flow.my_module_statuses.each do |status| %> + + <% end %> + <% end %>
diff --git a/app/views/my_modules/_complete_task_modal.html.erb b/app/views/my_modules/_complete_task_modal.html.erb deleted file mode 100644 index 49e2b4238..000000000 --- a/app/views/my_modules/_complete_task_modal.html.erb +++ /dev/null @@ -1,28 +0,0 @@ - diff --git a/app/views/my_modules/_module_state_label.html.erb b/app/views/my_modules/_module_state_label.html.erb deleted file mode 100644 index 8385837f4..000000000 --- a/app/views/my_modules/_module_state_label.html.erb +++ /dev/null @@ -1,7 +0,0 @@ -<% if my_module.completed? %> - - <%= t('my_modules.states.completed_on', date: l(my_module.completed_on, format: :full_date)) %> - -<% else %> - <%= t('my_modules.states.in_progress') %> -<% end %> diff --git a/app/views/my_modules/_my_module_details.html.erb b/app/views/my_modules/_my_module_details.html.erb index e6d6b57aa..8e217ff7c 100644 --- a/app/views/my_modules/_my_module_details.html.erb +++ b/app/views/my_modules/_my_module_details.html.erb @@ -20,17 +20,6 @@
-
-
- - <%= t('my_modules.states.state_label') %> -
- - <%= render partial: "module_state_label.html.erb", - locals: { my_module: @my_module } %> - -
-
diff --git a/app/views/my_modules/_state_button_complete.html.erb b/app/views/my_modules/_state_button_complete.html.erb deleted file mode 100644 index 9ef59cff1..000000000 --- a/app/views/my_modules/_state_button_complete.html.erb +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/app/views/my_modules/_state_button_uncomplete.html.erb b/app/views/my_modules/_state_button_uncomplete.html.erb deleted file mode 100644 index bd4ca47aa..000000000 --- a/app/views/my_modules/_state_button_uncomplete.html.erb +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/app/views/my_modules/_state_buttons.html.erb b/app/views/my_modules/_state_buttons.html.erb deleted file mode 100644 index b00ea460e..000000000 --- a/app/views/my_modules/_state_buttons.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -
- <% if can_complete_module?(@my_module) %> -
- <% if !@my_module.completed? %> -
- <%= render 'my_modules/state_button_complete.html.erb' %> -
- <% else @my_module.completed? %> -
- <%= render 'my_modules/state_button_uncomplete.html.erb' %> -
- <% end %> -
- <% end %> - -
diff --git a/app/views/my_modules/modals/_status_flow_modal.html.erb b/app/views/my_modules/modals/_status_flow_modal.html.erb new file mode 100644 index 000000000..bd16bfc96 --- /dev/null +++ b/app/views/my_modules/modals/_status_flow_modal.html.erb @@ -0,0 +1,14 @@ + diff --git a/app/views/my_modules/modals/_status_flow_modal_body.html.erb b/app/views/my_modules/modals/_status_flow_modal_body.html.erb new file mode 100644 index 000000000..49724efd7 --- /dev/null +++ b/app/views/my_modules/modals/_status_flow_modal_body.html.erb @@ -0,0 +1,22 @@ +
+ <% my_module_statuses.each_with_index do |status, i| %> + + <% unless i.zero? %> +
+ <% end %> + +
+
+ <% if status.id == @my_module.my_module_status_id %> + <%= t('my_modules.modals.status_flow_modal.current_status') %> + <% end %> +
+
+ <%= status[:name] %> +
+
<%= status[:status_comment] %>
+
+ <% end %> + + + diff --git a/app/views/my_modules/protocols.html.erb b/app/views/my_modules/protocols.html.erb index 0a985c580..6a89c4c32 100644 --- a/app/views/my_modules/protocols.html.erb +++ b/app/views/my_modules/protocols.html.erb @@ -6,7 +6,7 @@
-
+

@@ -20,12 +20,13 @@

-
- <%= render partial: "my_modules/state_buttons.html.erb" %> -
+
- <%= render partial: "my_module_details" %> + <%= render partial: 'my_module_details' %> +
+
+ <%= render partial: 'my_modules/status_flow/task_flow_button', locals: { my_module: @my_module } if @my_module.my_module_status_flow && can_change_my_module_flow_status?(@my_module) %>
@@ -118,13 +119,16 @@ <%= render partial: "protocols/import_export/import_elements.html.erb" %> - -<%= render partial: 'my_modules/complete_task_modal.html.erb' %> + +<% if @my_module.my_module_status_flow %> + <%= render partial: 'my_modules/modals/status_flow_modal.html.erb' %> +<% end %> <%= render partial: 'assets/wopi/create_wopi_file_modal.html.erb' %> <%= stylesheet_link_tag 'datatables' %> <%= javascript_include_tag("my_modules/protocols") %> +<%= javascript_include_tag("my_modules/status_flow") %> <%= javascript_pack_tag 'emoji_button' %> <%= javascript_include_tag("my_modules/repositories") %> diff --git a/app/views/my_modules/status_flow/_task_flow_button.html.erb b/app/views/my_modules/status_flow/_task_flow_button.html.erb new file mode 100644 index 000000000..0978b99b6 --- /dev/null +++ b/app/views/my_modules/status_flow/_task_flow_button.html.erb @@ -0,0 +1,31 @@ +<% status = my_module.my_module_status %> +
<%= t('my_module_statuses.dropdown.status_label') %>
+ diff --git a/app/views/reports/elements/_my_module_element.html.erb b/app/views/reports/elements/_my_module_element.html.erb index b2a184416..3c8a54732 100644 --- a/app/views/reports/elements/_my_module_element.html.erb +++ b/app/views/reports/elements/_my_module_element.html.erb @@ -6,7 +6,7 @@
- <%=t "projects.reports.elements.module.user_time", timestamp: l(timestamp, format: :full) %> + <%= t("projects.reports.elements.module.user_time", timestamp: l(timestamp, format: :full)) %>
<%= render partial: "reports/elements/element_controls.html.erb", locals: { show_sort: true } %> @@ -24,6 +24,7 @@ <% end %>
+
<% if my_module.started_on.present? %> <%= t('projects.reports.elements.module.started_on', started_on: l(my_module.started_on, format: :full)) %> @@ -55,9 +56,28 @@ <% end %>
+

+ <% if my_module.started_on.present? %> + <%= t('projects.reports.elements.module.started_on', started_on: l(my_module.started_on, format: :full)) %> + <% else %> + <%= t("projects.reports.elements.module.no_start_date") %> + <% end %> +

+

+ <% if my_module.due_date.present? %> + <%= t("projects.reports.elements.module.due_date", due_date: l(my_module.due_date, format: :full)) %> + <% else %> + <%= t("projects.reports.elements.module.no_due_date") %> + <% end %> +

+

+ <% status = my_module.my_module_status %> + <%= t("projects.reports.elements.module.status") %> + <%= status.name %> +

- <%=t "projects.reports.elements.module.tags_header" %> + <%= t("projects.reports.elements.module.tags_header") %>
<% if my_module.tags.any? %> <% my_module.tags.each do |tag| %> @@ -67,10 +87,21 @@ <% end %> <% else %>
- <%=t "projects.reports.elements.module.no_tags" %> + <%= t("projects.reports.elements.module.no_tags") %>
<% end %>
+
+
+ <% if my_module.description.present? %> + <%= custom_auto_link(my_module.prepare_for_report(:description, for_export_all), + team: current_team, + base64_encoded_imgs: for_export_all) %> + <% else %> + <%= t("projects.reports.elements.module.no_description") %> + <% end %> +
+
<%= children if (defined? children and children.present?) %> diff --git a/app/views/reports/elements/_my_module_samples_element.html.erb b/app/views/reports/elements/_my_module_samples_element.html.erb index 1fc6cb3cd..20de96eb7 100644 --- a/app/views/reports/elements/_my_module_samples_element.html.erb +++ b/app/views/reports/elements/_my_module_samples_element.html.erb @@ -1,7 +1,6 @@ <% if my_module.blank? and @my_module.present? then my_module = @my_module end %> <% if order.blank? and @order.present? then order = @order end %> <% timestamp = Time.current + 1.year - 1.days %> -<% samples_json = my_module.samples_json_hot(order) %>
" data-name="<%=t "projects.reports.elements.module_samples.sidebar_name" %>" data-icon-class="fas fa-tint">
diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 188732aef..0ac23b04a 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -34,6 +34,7 @@ Rails.application.config.assets.precompile += Rails.application.config.assets.precompile += %w(my_modules/activities.js) Rails.application.config.assets.precompile += %w(my_modules/protocols.js) Rails.application.config.assets.precompile += %w(my_modules/repositories.js) +Rails.application.config.assets.precompile += %w(my_modules/status_flow.js) Rails.application.config.assets.precompile += %w(my_modules/protocols/protocol_status_bar.js) Rails.application.config.assets.precompile += %w(my_modules/results.js) diff --git a/config/locales/dashboard/en.yml b/config/locales/dashboard/en.yml index 0fa346e4e..4ac70a53a 100644 --- a/config/locales/dashboard/en.yml +++ b/config/locales/dashboard/en.yml @@ -14,7 +14,11 @@ en: due_date: "Due date" atoz: "From A to Z" ztoa: "From Z to A" - display: "Display" + display: "Display statuses" + statuses: + placeholder: "Select statuses" + all_selected: "All selected" + selected: "selected" uncompleted_tasks: "Tasks in progress" completed_tasks: "Tasks completed" project: "Project" diff --git a/config/locales/en.yml b/config/locales/en.yml index e9a413c10..441e87a76 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -110,6 +110,12 @@ en: attributes: position: not_unique: "X and Y position has already been taken by another task in the experiment." + my_module_status: + attributes: + next_status: + different_flow: "Should belong to the same flow" + previous_status: + different_flow: "Should belong to the same flow" asset: attributes: file: @@ -527,6 +533,7 @@ en: module: user_time: "Task created on %{timestamp}." started_on: "Start date: %{started_on}" + status: "Status:" no_start_date: "No start date" due_date: "Due date: %{due_date}" no_due_date: "No due date" @@ -658,12 +665,6 @@ en: import: "Import protocol" export: "Export protocol" save_to_repo: "Save to repository" - buttons: - complete: "Complete task" - uncomplete: "Uncomplete task" - complete_modal: - description: 'You have completed all steps in the task. Would you like to mark entire task as completed?' - leave_uncompleted: 'Leave task in progress' description: title: "Edit task %{module} description" label: "Description" @@ -838,6 +839,10 @@ en: assign_and_unassign_from_task_and_downstream_html: "Successfully assigned %{assigned_items} and unassigned %{unassigned_items} item(s) from the task and downstream tasks." update_error: "There was an error in updating your item(s)." modals: + status_flow_modal: + title: "Task status flow: %{status_flow}" + current_status: "Current status" + done: "Done" update_repository_record: title: "Update %{repository_name} items to %{my_module_name} task" message: "Do you want to update %{size} items only from this task, or update them from this task & downstream tasks in the workflow also?" @@ -2238,8 +2243,6 @@ en: assign_user_to_team: "%{assigned_user} was added as %{role} to team %{team} by %{assigned_by_user}." unassign_user_from_team: "%{unassigned_user} was removed from team %{team} by %{unassigned_by_user}." task_completed: "%{user} completed task %{module}. %{date} | Project: %{project} | Experiment: %{experiment}" - start_work_on_next_task: "%{user} has completed the task %{module}. You can now start working on the next task in the workflow." - start_work_on_next_task_message: "Project: %{project} | Experiment: %{experiment} | Task: %{my_module}" assets: head_title: diff --git a/config/locales/my_module_statuses/en.yml b/config/locales/my_module_statuses/en.yml new file mode 100644 index 000000000..e6ac1e5c2 --- /dev/null +++ b/config/locales/my_module_statuses/en.yml @@ -0,0 +1,10 @@ +en: + my_module_statuses: + dropdown: + status_label: Status + move_label: Move to -> + return_label: Return to -> + view_flow_label: View task flow + update_status: + error: + no_permission: You dont have permission to change the status diff --git a/config/routes.rb b/config/routes.rb index 50c39fae5..58897b6d3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -18,28 +18,6 @@ Rails.application.routes.draw do root 'dashboards#show' - # # Client APP endpoints - # get '/settings', to: 'client_api/settings#index' - # get '/settings/*all', to: 'client_api/settings#index' - # - # namespace :client_api, defaults: { format: 'json' } do - # post '/premissions', to: 'permissions#status' - # %i(activities teams notifications users configurations).each do |path| - # draw path - # end - # end - - # Save sample table state - # post '/state_save/:team_id/:user_id', - # to: 'user_samples#save_samples_table_status', - # as: 'save_samples_table_status', - # defaults: { format: 'json' } - # - # post '/state_load/:team_id/:user_id', - # to: 'user_samples#load_samples_table_status', - # as: 'load_samples_table_status', - # defaults: { format: 'json' } - resources :activities, only: [:index] get 'forbidden', to: 'application#forbidden', as: 'forbidden' @@ -187,18 +165,7 @@ Rails.application.routes.draw do end end end - # resources :samples, only: [:new, :create] - # resources :sample_types, except: [:show, :new] do - # get 'sample_type_element', to: 'sample_types#sample_type_element' - # get 'destroy_confirmation', to: 'sample_types#destroy_confirmation' - # end - # resources :sample_groups, except: [:show, :new] do - # get 'sample_group_element', to: 'sample_groups#sample_group_element' - # get 'destroy_confirmation', to: 'sample_groups#destroy_confirmation' - # end - # resources :custom_fields, only: [:create, :edit, :update, :destroy] do - # get 'destroy_html' - # end + member do post 'parse_sheet', defaults: { format: 'json' } post 'export_repository', to: 'repositories#export_repository' @@ -374,6 +341,9 @@ Rails.application.routes.draw do get :index_old end end + + resource :status_flow, controller: :my_module_status_flow, only: :show + resources :my_module_comments, path: '/comments', only: [:index, :create, :edit, :update, :destroy] @@ -426,11 +396,10 @@ Rails.application.routes.draw do patch 'protocol_description', to: 'my_modules#update_protocol_description', as: 'update_protocol_description' + patch 'state', to: 'my_modules#update_state', as: 'update_state' get 'protocols' # Protocols view for single module get 'results' # Results view for single module get 'archive' # Archive view for single module - get 'complete_my_module' - post 'toggle_task_state' end # Those routes are defined outside of member block diff --git a/db/migrate/20200713142353_create_task_flows_models.rb b/db/migrate/20200713142353_create_task_flows_models.rb new file mode 100644 index 000000000..af85aac31 --- /dev/null +++ b/db/migrate/20200713142353_create_task_flows_models.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class CreateTaskFlowsModels < ActiveRecord::Migration[6.0] + def change + change_table :my_modules do |t| + t.references :my_module_status + end + + create_table :my_module_status_flows do |t| + t.string :name, null: false + t.string :description + t.integer :visibility, index: true, default: 0 + t.references :team + t.references :created_by, index: false, foreign_key: { to_table: :users } + t.references :last_modified_by, index: false, foreign_key: { to_table: :users } + + t.timestamps + end + + create_table :my_module_statuses do |t| + t.string :name, null: false + t.string :description + t.string :color, null: false + t.references :my_module_status_flow, index: true + t.references :previous_status, index: { unique: true }, foreign_key: { to_table: :my_module_statuses } + t.references :created_by, index: false, foreign_key: { to_table: :users } + t.references :last_modified_by, index: false, foreign_key: { to_table: :users } + + t.timestamps + end + + create_table :my_module_status_consequences do |t| + t.references :my_module_status + t.string :type + + t.timestamps + end + + create_table :my_module_status_conditions do |t| + t.references :my_module_status + t.string :type + + t.timestamps + end + + create_table :my_module_status_implications do |t| + t.references :my_module_status + t.string :type + + t.timestamps + end + end +end diff --git a/db/structure.sql b/db/structure.sql index 392cd7df3..1efe7eb1c 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -623,6 +623,175 @@ CREATE SEQUENCE public.my_module_repository_rows_id_seq ALTER SEQUENCE public.my_module_repository_rows_id_seq OWNED BY public.my_module_repository_rows.id; +-- +-- Name: my_module_status_conditions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.my_module_status_conditions ( + id bigint NOT NULL, + my_module_status_id bigint, + type character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: my_module_status_conditions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.my_module_status_conditions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: my_module_status_conditions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.my_module_status_conditions_id_seq OWNED BY public.my_module_status_conditions.id; + + +-- +-- Name: my_module_status_consequences; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.my_module_status_consequences ( + id bigint NOT NULL, + my_module_status_id bigint, + type character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: my_module_status_consequences_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.my_module_status_consequences_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: my_module_status_consequences_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.my_module_status_consequences_id_seq OWNED BY public.my_module_status_consequences.id; + + +-- +-- Name: my_module_status_flows; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.my_module_status_flows ( + id bigint NOT NULL, + name character varying NOT NULL, + description character varying, + visibility integer DEFAULT 0, + team_id bigint, + created_by_id bigint, + last_modified_by_id bigint, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: my_module_status_flows_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.my_module_status_flows_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: my_module_status_flows_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.my_module_status_flows_id_seq OWNED BY public.my_module_status_flows.id; + + +-- +-- Name: my_module_status_implications; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.my_module_status_implications ( + id bigint NOT NULL, + my_module_status_id bigint, + type character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: my_module_status_implications_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.my_module_status_implications_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: my_module_status_implications_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.my_module_status_implications_id_seq OWNED BY public.my_module_status_implications.id; + + +-- +-- Name: my_module_statuses; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.my_module_statuses ( + id bigint NOT NULL, + name character varying NOT NULL, + description character varying, + color character varying NOT NULL, + my_module_status_flow_id bigint, + previous_status_id bigint, + created_by_id bigint, + last_modified_by_id bigint, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: my_module_statuses_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.my_module_statuses_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: my_module_statuses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.my_module_statuses_id_seq OWNED BY public.my_module_statuses.id; + + -- -- Name: my_module_tags; Type: TABLE; Schema: public; Owner: - -- @@ -681,7 +850,8 @@ CREATE TABLE public.my_modules ( experiment_id bigint DEFAULT 0 NOT NULL, state smallint DEFAULT 0, completed_on timestamp without time zone, - started_on timestamp without time zone + started_on timestamp without time zone, + my_module_status_id bigint ); @@ -2951,6 +3121,41 @@ ALTER TABLE ONLY public.my_module_groups ALTER COLUMN id SET DEFAULT nextval('pu ALTER TABLE ONLY public.my_module_repository_rows ALTER COLUMN id SET DEFAULT nextval('public.my_module_repository_rows_id_seq'::regclass); +-- +-- Name: my_module_status_conditions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_status_conditions ALTER COLUMN id SET DEFAULT nextval('public.my_module_status_conditions_id_seq'::regclass); + + +-- +-- Name: my_module_status_consequences id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_status_consequences ALTER COLUMN id SET DEFAULT nextval('public.my_module_status_consequences_id_seq'::regclass); + + +-- +-- Name: my_module_status_flows id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_status_flows ALTER COLUMN id SET DEFAULT nextval('public.my_module_status_flows_id_seq'::regclass); + + +-- +-- Name: my_module_status_implications id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_status_implications ALTER COLUMN id SET DEFAULT nextval('public.my_module_status_implications_id_seq'::regclass); + + +-- +-- Name: my_module_statuses id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_statuses ALTER COLUMN id SET DEFAULT nextval('public.my_module_statuses_id_seq'::regclass); + + -- -- Name: my_module_tags id; Type: DEFAULT; Schema: public; Owner: - -- @@ -3512,6 +3717,46 @@ ALTER TABLE ONLY public.my_module_repository_rows ADD CONSTRAINT my_module_repository_rows_pkey PRIMARY KEY (id); +-- +-- Name: my_module_status_conditions my_module_status_conditions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_status_conditions + ADD CONSTRAINT my_module_status_conditions_pkey PRIMARY KEY (id); + + +-- +-- Name: my_module_status_consequences my_module_status_consequences_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_status_consequences + ADD CONSTRAINT my_module_status_consequences_pkey PRIMARY KEY (id); + + +-- +-- Name: my_module_status_flows my_module_status_flows_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_status_flows + ADD CONSTRAINT my_module_status_flows_pkey PRIMARY KEY (id); + + +-- +-- Name: my_module_status_implications my_module_status_implications_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_status_implications + ADD CONSTRAINT my_module_status_implications_pkey PRIMARY KEY (id); + + +-- +-- Name: my_module_statuses my_module_statuses_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_statuses + ADD CONSTRAINT my_module_statuses_pkey PRIMARY KEY (id); + + -- -- Name: my_module_tags my_module_tags_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -4367,6 +4612,55 @@ CREATE INDEX index_my_module_ids_repository_row_ids ON public.my_module_reposito CREATE INDEX index_my_module_repository_rows_on_repository_row_id ON public.my_module_repository_rows USING btree (repository_row_id); +-- +-- Name: index_my_module_status_conditions_on_my_module_status_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_my_module_status_conditions_on_my_module_status_id ON public.my_module_status_conditions USING btree (my_module_status_id); + + +-- +-- Name: index_my_module_status_consequences_on_my_module_status_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_my_module_status_consequences_on_my_module_status_id ON public.my_module_status_consequences USING btree (my_module_status_id); + + +-- +-- Name: index_my_module_status_flows_on_team_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_my_module_status_flows_on_team_id ON public.my_module_status_flows USING btree (team_id); + + +-- +-- Name: index_my_module_status_flows_on_visibility; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_my_module_status_flows_on_visibility ON public.my_module_status_flows USING btree (visibility); + + +-- +-- Name: index_my_module_status_implications_on_my_module_status_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_my_module_status_implications_on_my_module_status_id ON public.my_module_status_implications USING btree (my_module_status_id); + + +-- +-- Name: index_my_module_statuses_on_my_module_status_flow_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_my_module_statuses_on_my_module_status_flow_id ON public.my_module_statuses USING btree (my_module_status_flow_id); + + +-- +-- Name: index_my_module_statuses_on_previous_status_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE UNIQUE INDEX index_my_module_statuses_on_previous_status_id ON public.my_module_statuses USING btree (previous_status_id); + + -- -- Name: index_my_module_tags_on_created_by_id; Type: INDEX; Schema: public; Owner: - -- @@ -4430,6 +4724,13 @@ CREATE INDEX index_my_modules_on_last_modified_by_id ON public.my_modules USING CREATE INDEX index_my_modules_on_my_module_group_id ON public.my_modules USING btree (my_module_group_id); +-- +-- Name: index_my_modules_on_my_module_status_id; Type: INDEX; Schema: public; Owner: - +-- + +CREATE INDEX index_my_modules_on_my_module_status_id ON public.my_modules USING btree (my_module_status_id); + + -- -- Name: index_my_modules_on_name; Type: INDEX; Schema: public; Owner: - -- @@ -6048,6 +6349,14 @@ ALTER TABLE ONLY public.oauth_access_grants ADD CONSTRAINT fk_rails_330c32d8d9 FOREIGN KEY (resource_owner_id) REFERENCES public.users(id); +-- +-- Name: my_module_statuses fk_rails_357ee33309; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_statuses + ADD CONSTRAINT fk_rails_357ee33309 FOREIGN KEY (last_modified_by_id) REFERENCES public.users(id); + + -- -- Name: experiments fk_rails_35ad21e487; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -6552,6 +6861,14 @@ ALTER TABLE ONLY public.results ADD CONSTRAINT fk_rails_9be849c454 FOREIGN KEY (archived_by_id) REFERENCES public.users(id); +-- +-- Name: my_module_status_flows fk_rails_9c3936bd7a; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_status_flows + ADD CONSTRAINT fk_rails_9c3936bd7a FOREIGN KEY (last_modified_by_id) REFERENCES public.users(id); + + -- -- Name: repository_status_values fk_rails_9d357798c5; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -6632,6 +6949,14 @@ ALTER TABLE ONLY public.repository_status_values ADD CONSTRAINT fk_rails_a3a2aede5b FOREIGN KEY (repository_status_item_id) REFERENCES public.repository_status_items(id); +-- +-- Name: my_module_statuses fk_rails_a3f7cd509a; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_statuses + ADD CONSTRAINT fk_rails_a3f7cd509a FOREIGN KEY (previous_status_id) REFERENCES public.my_module_statuses(id); + + -- -- Name: result_assets fk_rails_a418904d39; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -6680,6 +7005,14 @@ ALTER TABLE ONLY public.repository_list_items ADD CONSTRAINT fk_rails_ace46bca57 FOREIGN KEY (repository_column_id) REFERENCES public.repository_columns(id); +-- +-- Name: my_module_statuses fk_rails_b024d15104; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_statuses + ADD CONSTRAINT fk_rails_b024d15104 FOREIGN KEY (created_by_id) REFERENCES public.users(id); + + -- -- Name: protocols fk_rails_b2c86b4f11; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -6720,6 +7053,14 @@ ALTER TABLE ONLY public.repository_asset_values ADD CONSTRAINT fk_rails_bb983a4d66 FOREIGN KEY (created_by_id) REFERENCES public.users(id); +-- +-- Name: my_module_status_flows fk_rails_c19dc6b9e9; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.my_module_status_flows + ADD CONSTRAINT fk_rails_c19dc6b9e9 FOREIGN KEY (created_by_id) REFERENCES public.users(id); + + -- -- Name: sample_types fk_rails_c227b918b2; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -7283,6 +7624,5 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200622140843'), ('20200622155632'), ('20200709142830'), +('20200713142353'), ('20200714082503'); - - diff --git a/features/protocol.feature b/features/protocol.feature index 524cbed43..3009eaf27 100644 --- a/features/protocol.feature +++ b/features/protocol.feature @@ -105,16 +105,3 @@ Given default screen size2 And I fill in "I will go to Krn one day." in "#my_module_description_textarea" rich text editor field And I click element with css ".tinymce-save-button" Then I should see "I will go to Krn one day." - -@javascript - Scenario: Successful Complete task - Given I'm on the Protocols page of a "Experiment design" task - And I click "Complete task" button - Then I should see "Uncomplete task" - -@javascript - Scenario: Successful Uncomplete task - Given I'm on the Protocols page of a "Experiment design" task - And I click "Complete task" button - And I click "Uncomplete task" button - Then I should see "Complete task" diff --git a/spec/controllers/my_modules_controller_spec.rb b/spec/controllers/my_modules_controller_spec.rb index 288f1d8dc..68780086f 100644 --- a/spec/controllers/my_modules_controller_spec.rb +++ b/spec/controllers/my_modules_controller_spec.rb @@ -123,39 +123,77 @@ describe MyModulesController, type: :controller do end end - describe 'POST toggle_task_state' do - let(:action) { post :toggle_task_state, params: params, format: :json } - let(:params) { { id: my_module.id } } + describe 'PUT update_state' do + let(:action) { put :update_state, params: params, format: :json } + let(:my_module_id) { my_module.id } + let(:status_id) { 'some-state-id' } + let(:params) do + { + id: my_module_id, + my_module: { status_id: status_id } + } + end + let(:my_module_status_flow) { create :my_module_status_flow, :in_team, team: team} + let(:status1) {create :my_module_status, my_module_status_flow: my_module_status_flow} + let(:status2) {create :my_module_status, my_module_status_flow: my_module_status_flow} - context 'when completing task' do - let(:my_module) do - create :my_module, state: 'uncompleted', experiment: experiment + context 'when states updated' do + let(:status_id) { status2.id } + + before do + my_module.update(my_module_status: status1) end - it 'calls create activity for completing task' do - expect(Activities::CreateActivityService) - .to(receive(:call) - .with(hash_including(activity_type: :complete_task))) + it 'changes status' do action + + expect(my_module.reload.my_module_status.name).to be_eql(status2.name) + expect(response).to have_http_status 200 end end - context 'when uncompleting task' do - let(:my_module) do - create :my_module, state: 'completed', experiment: experiment + context 'when status not found' do + let(:status_id) { -1 } + + before do + my_module.update(my_module_status: status1) end - it 'calls create activity for uncompleting task' do - expect(Activities::CreateActivityService) - .to(receive(:call) - .with(hash_including(activity_type: :uncomplete_task))) + it 'renders 404' do action + + expect(response).to have_http_status 404 end end - it 'adds activity in DB' do - expect { action } - .to(change { Activity.count }) + context 'when my_module does not have assign flow yet' do + let(:status_id) { -1 } + + it 'renders 404' do + action + + expect(response).to have_http_status 404 + end + end + + context 'when user does not have permissions' do + it 'renders 403' do + # Remove user from project + UserProject.where(user: user, project: project).destroy_all + action + + expect(response).to have_http_status 403 + end + end + + context 'when my_module not found' do + let(:my_module_id) { -1 } + + it 'renders 404' do + action + + expect(response).to have_http_status 404 + end end end end diff --git a/spec/factories/my_module_status_flows.rb b/spec/factories/my_module_status_flows.rb new file mode 100644 index 000000000..1112c8dea --- /dev/null +++ b/spec/factories/my_module_status_flows.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :my_module_status_flow do + name { Faker::Name.unique.name } + description { Faker::Lorem.sentence } + trait :in_team do + team { create :team } + visibility { :in_team } + end + end +end diff --git a/spec/factories/my_module_statuses.rb b/spec/factories/my_module_statuses.rb new file mode 100644 index 000000000..42a6d86eb --- /dev/null +++ b/spec/factories/my_module_statuses.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :my_module_status do + name { Faker::Name.unique.name } + description { Faker::Lorem.sentence } + color { Faker::Color.hex_color } + my_module_status_flow + end +end diff --git a/spec/models/my_module_spec.rb b/spec/models/my_module_spec.rb index 2dcd8e920..9deb8281b 100644 --- a/spec/models/my_module_spec.rb +++ b/spec/models/my_module_spec.rb @@ -36,6 +36,7 @@ describe MyModule, type: :model do it { should have_db_column :state } it { should have_db_column :completed_on } it { should have_db_column :started_on } + it { should have_db_column :my_module_status_id } end describe 'Relations' do diff --git a/spec/models/my_module_status_flow_spec.rb b/spec/models/my_module_status_flow_spec.rb new file mode 100644 index 000000000..1c2976e4c --- /dev/null +++ b/spec/models/my_module_status_flow_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe MyModuleStatusFlow, type: :model do + let(:my_module_global_workflow) { build :my_module_status_flow } + let(:my_module_team_workflow) { build :my_module_status_flow, :in_team } + + it 'is valid' do + expect(my_module_global_workflow).to be_valid + end + + it 'should be of class MyModuleStatusFlow' do + expect(subject.class).to eq MyModuleStatusFlow + end + + describe 'Database table' do + it { should have_db_column :name } + it { should have_db_column :description } + it { should have_db_column :visibility } + it { should have_db_column :created_by_id } + it { should have_db_column :last_modified_by_id } + it { should have_db_column :created_at } + it { should have_db_column :updated_at } + end + + describe 'Relations' do + it { should have_many(:my_module_statuses).dependent(:destroy) } + end + + describe 'Validations' do + describe '#visibility' do + it { is_expected.to validate_presence_of :visibility } + end + + describe '#name' do + it { is_expected.to validate_length_of(:name).is_at_most(Constants::NAME_MAX_LENGTH) } + it { expect(my_module_team_workflow).to validate_uniqueness_of(:name).scoped_to(:team_id).case_insensitive } + end + + describe '#description' do + it { is_expected.to validate_length_of(:description).is_at_most(Constants::TEXT_MAX_LENGTH) } + end + + describe '#team' do + it { expect(my_module_team_workflow).to validate_presence_of :team } + end + end +end diff --git a/spec/models/my_module_status_spec.rb b/spec/models/my_module_status_spec.rb new file mode 100644 index 000000000..bdefb0c76 --- /dev/null +++ b/spec/models/my_module_status_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'rails_helper' + +describe MyModuleStatus, type: :model do + let(:my_module_status) { build :my_module_status } + + it 'is valid' do + expect(my_module_status).to be_valid + end + + it 'should be of class MyModuleStatus' do + expect(subject.class).to eq MyModuleStatus + end + + describe 'Database table' do + it { should have_db_column :name } + it { should have_db_column :description } + it { should have_db_column :color } + it { should have_db_column :my_module_status_flow_id } + it { should have_db_column :created_by_id } + it { should have_db_column :last_modified_by_id } + it { should have_db_column :previous_status_id } + it { should have_db_column :created_at } + it { should have_db_column :updated_at } + end + + describe 'Relations' do + it { should belong_to :my_module_status_flow } + it { should have_many(:my_modules).dependent(:nullify) } + it { should have_many(:my_module_status_conditions).dependent(:destroy) } + it { should have_many(:my_module_status_consequences).dependent(:destroy) } + it { should have_many(:my_module_status_implications).dependent(:destroy) } + end + + describe 'Validations' do + describe '#name' do + it { is_expected.to validate_length_of(:name).is_at_most(Constants::NAME_MAX_LENGTH) } + end + + describe '#description' do + it { is_expected.to validate_length_of(:description).is_at_most(Constants::TEXT_MAX_LENGTH) } + end + end +end