Merge branch 'features/task-flows' into develop_to_task_flows

This commit is contained in:
Urban Rotnik 2020-08-04 14:50:46 +02:00 committed by GitHub
commit 7c14da70d9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
55 changed files with 1313 additions and 431 deletions

View file

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

View file

@ -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();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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) */

View file

@ -16,7 +16,7 @@
border-color: $brand-focus;
.caret {
transform: rotateX(180deg)
transform: rotateX(180deg);
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -154,7 +154,7 @@ module ReportsHelper
style = 'default'
text = t('protocols.steps.uncompleted')
end
"<span class=\"label label-#{style}\">#{text}</span>".html_safe
"<span class=\"label step-label-#{style}\">[#{text}]</span>".html_safe
end
# Fixes issues with avatar images in reports

View file

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

View file

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

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class MyModuleStatusCondition < ApplicationRecord
belongs_to :my_module_status
end

View file

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

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class MyModuleStatusConsequence < ApplicationRecord
belongs_to :my_module_status
end

View file

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

View file

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

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
class MyModuleStatusImplication < ApplicationRecord
belongs_to :my_module_status
end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -37,6 +37,9 @@
<% else %>
<%= render partial: "my_modules/card_due_date_label.html.erb", locals: { my_module: my_module, format: :full_date } %>
<% end %>
<div class="status-label" style="--state-color: #2DBE61">
Completed
</div>
</div>
<div class="panel-footer panel-footer-scinote buttons-container">

View file

@ -20,9 +20,18 @@
</div>
<div class="select-block">
<label><%= t("dashboard.current_tasks.filter.display") %></label>
<select class="view-filter">
<option value="uncompleted" ><%= t("dashboard.current_tasks.filter.uncompleted_tasks") %></option>
<option value="completed" ><%= t("dashboard.current_tasks.filter.completed_tasks") %></option>
<select class="view-filter"
data-combine-tags="true"
data-placeholder="<%= t("dashboard.current_tasks.filter.statuses.placeholder") %>"
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| %>
<option value="<%= status.id %>" selected><%= status.name %></option>
<% end %>
<% end %>
</select>
</div>
<div class="select-block">

View file

@ -1,28 +0,0 @@
<div class="modal fade"
tabindex="-1"
role="dialog"
data-url="<%= complete_my_module_my_module_path(@my_module) %>"
id="completed-task-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button"
class="close"
data-dismiss="modal"
aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h4 class="modal-title"><%=t 'my_modules.buttons.complete' %></h4>
</div>
<div class="modal-body">
<p><%=t 'my_modules.complete_modal.description' %></p>
</div>
<div class="modal-footer">
<button type="button"
class="btn btn-default"
data-dismiss="modal"><%=t 'my_modules.complete_modal.leave_uncompleted' %></button>
<button type="button"
class="btn btn-success"
data-action="complete"><%=t 'my_modules.buttons.complete' %></button>
</div>
</div>
</div>
</div>

View file

@ -1,7 +0,0 @@
<% if my_module.completed? %>
<strong class="alert-green">
<%= t('my_modules.states.completed_on', date: l(my_module.completed_on, format: :full_date)) %>
</strong>
<% else %>
<strong><%= t('my_modules.states.in_progress') %></strong>
<% end %>

View file

@ -20,17 +20,6 @@
</div>
</div>
<div class="flex-block">
<div class="flex-block-label">
<span class="fas block-icon fa-tachometer-alt"></span>
<%= t('my_modules.states.state_label') %>
</div>
<span class="task-state-label">
<%= render partial: "module_state_label.html.erb",
locals: { my_module: @my_module } %>
</span>
</div>
<div class="flex-block">
<div class="flex-block-label">
<span class="fas block-icon fa-users"></span>

View file

@ -1,4 +0,0 @@
<button class="btn btn-primary">
<i class="fas fa-check"></i>
<%= t("my_modules.buttons.complete") %>
</button>

View file

@ -1,4 +0,0 @@
<button class="btn btn-secondary ">
<i class="fas fa-undo-alt"></i>
<%= t("my_modules.buttons.uncomplete") %>
</button>

View file

@ -1,16 +0,0 @@
<div class="pull-right my_module-state-buttons">
<% if can_complete_module?(@my_module) %>
<div class="btn-group">
<% if !@my_module.completed? %>
<div data-action="complete-task" data-link-url="<%= toggle_task_state_my_module_path(@my_module) %>">
<%= render 'my_modules/state_button_complete.html.erb' %>
</div>
<% else @my_module.completed? %>
<div data-action="uncomplete-task" data-link-url="<%= toggle_task_state_my_module_path(@my_module) %>">
<%= render 'my_modules/state_button_uncomplete.html.erb' %>
</div>
<% end %>
</div>
<% end %>
<span data-hook="my_module-state-buttons"></span>
</div>

View file

@ -0,0 +1,14 @@
<div class="modal" id="statusFlowModal" tabindex="-1" role="dialog" aria-labelledby="manage-module-users-modal-label" data-status-flow-url="<%= my_module_status_flow_path(@my_module) %>">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<h2 class="modal-title"><%= t('my_modules.modals.status_flow_modal.title', status_flow: @my_module.my_module_status_flow.name) %></h2>
</div>
<div class="modal-body"></div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"><%= t('my_modules.modals.status_flow_modal.done') %></button>
</div>
</div>
</div>
</div>

View file

@ -0,0 +1,22 @@
<div class="status-flow">
<% my_module_statuses.each_with_index do |status, i| %>
<% unless i.zero? %>
<div class="connector"></div>
<% end %>
<div class="status-container">
<div class="current-status">
<% if status.id == @my_module.my_module_status_id %>
<%= t('my_modules.modals.status_flow_modal.current_status') %><i class="fas fa-long-arrow-alt-right"></i>
<% end %>
</div>
<div class="status-block" style="background: <%= status[:color] %>">
<%= status[:name] %>
</div>
<div class="status-comment"><%= status[:status_comment] %></div>
</div>
<% end %>

View file

@ -6,7 +6,7 @@
<div class="content-pane my-modules-protocols-index" data-task-id="<%= @my_module.id %>">
<!-- Details -->
<div class="task-section">
<div class="task-section task-information">
<div class="task-section-header">
<span id="taskDetailsLabel" class="task-section-title">
<h2>
@ -20,12 +20,13 @@
</div>
</span>
<div class="actions-block">
<%= render partial: "my_modules/state_buttons.html.erb" %>
</div>
</div>
<div class="task-details">
<%= render partial: "my_module_details" %>
<%= render partial: 'my_module_details' %>
</div>
<div class="task-flows">
<%= 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) %>
</div>
</div>
<!-- Notes -->
@ -118,13 +119,16 @@
<!-- Import protocol elements -->
<%= render partial: "protocols/import_export/import_elements.html.erb" %>
<!-- Complete task modal -->
<%= render partial: 'my_modules/complete_task_modal.html.erb' %>
<!-- Status flow modal -->
<% if @my_module.my_module_status_flow %>
<%= render partial: 'my_modules/modals/status_flow_modal.html.erb' %>
<% end %>
<!-- Create new office file modal -->
<%= 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") %>

View file

@ -0,0 +1,31 @@
<% status = my_module.my_module_status %>
<div class="status-label"><%= t('my_module_statuses.dropdown.status_label') %></div>
<div class="dropdown sci-dropdown status-flow-dropdown">
<button class="btn btn-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true"
style="<%= "background-color: #{status.color}" %>;">
<span><%= status.name %></span>
<span class="caret pull-right"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownTaskFlow" id="dropdownTaskFlowList" data-link-url="<%= update_state_my_module_url(my_module) %>">
<% unless status.initial_status? %>
<% previous_s = status.previous_status %>
<li data-state-id="<%= previous_s.id %>">
<span><%= t 'my_module_statuses.dropdown.return_label' %></span> <span style="<%= "background-color: #{previous_s.color}" %>"><%= previous_s.name %></span>
</li>
<% end %>
<% unless status.final_status? %>
<% next_s = status.next_status %>
<li data-state-id="<%= next_s.id %>">
<span><%= t('my_module_statuses.dropdown.move_label') %></span> <span style="<%= "background-color: #{next_s.color}" %>"><%= next_s.name %></span>
</li>
<% end %>
<li id="viewTaskFlow">
<%= t('my_module_statuses.dropdown.view_flow_label') %>
</li>
</ul>
</div>

View file

@ -6,7 +6,7 @@
<div class="report-element-header">
<div class="row">
<div class="pull-left user-time">
<%=t "projects.reports.elements.module.user_time", timestamp: l(timestamp, format: :full) %>
<%= t("projects.reports.elements.module.user_time", timestamp: l(timestamp, format: :full)) %>
</div>
<div class="pull-right controls">
<%= render partial: "reports/elements/element_controls.html.erb", locals: { show_sort: true } %>
@ -24,6 +24,7 @@
<% end %>
</h4>
</div>
<div class="pull-right module-start-date">
<% 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 %>
</div>
</div>
<p class="module-start-date">
<% if my_module.started_on.present? %>
<%= t('projects.reports.elements.module.started_on', started_on: l(my_module.started_on, format: :full)) %>
<% else %>
<em><%= t("projects.reports.elements.module.no_start_date") %></em>
<% end %>
</p>
<p class="module-due-date">
<% if my_module.due_date.present? %>
<%= t("projects.reports.elements.module.due_date", due_date: l(my_module.due_date, format: :full)) %>
<% else %>
<em><%= t("projects.reports.elements.module.no_due_date") %></em>
<% end %>
</p>
<p class="module-status">
<% status = my_module.my_module_status %>
<%= t("projects.reports.elements.module.status") %>
<span class="status-block" style="background: <%= status.color %>"><%= status.name %></span>
</p>
<div class="row module-tags">
<div class="pull-left">
<%=t "projects.reports.elements.module.tags_header" %>
<%= t("projects.reports.elements.module.tags_header") %>
</div>
<% if my_module.tags.any? %>
<% my_module.tags.each do |tag| %>
@ -67,10 +87,21 @@
<% end %>
<% else %>
<div class="pull-left module-no-tag">
<em><%=t "projects.reports.elements.module.no_tags" %></em>
<em><%= t("projects.reports.elements.module.no_tags") %></em>
</div>
<% end %>
</div>
<div class="row">
<div class="col-xs-12">
<% 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 %>
<em><%= t("projects.reports.elements.module.no_description") %></em>
<% end %>
</div>
</div>
</div>
<div class="report-element-children">
<%= children if (defined? children and children.present?) %>

View file

@ -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) %>
<div class="report-element report-module-samples-element" data-sort-hot="3" data-ts="<%= timestamp.to_i %>" data-type="my_module_samples" data-id='{ "my_module_id": <%= my_module.id %> }' data-scroll-id="<%= my_module.id %>" data-order="<%= order == :asc ? "asc" : "desc" %>" data-name="<%=t "projects.reports.elements.module_samples.sidebar_name" %>" data-icon-class="fas fa-tint">
<div class="report-element-header">
<div class="row">

View file

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

View file

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

View file

@ -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 <strong>%{assigned_items}</strong> and unassigned <strong>%{unassigned_items}</strong> 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: "<i>%{assigned_user}</i> was added as %{role} to team <strong>%{team}</strong> by <i>%{assigned_by_user}</i>."
unassign_user_from_team: "<i>%{unassigned_user}</i> was removed from team <strong>%{team}</strong> by <i>%{unassigned_by_user}</i>."
task_completed: "%{user} completed task %{module}. %{date} | Project: %{project} | Experiment: %{experiment}"
start_work_on_next_task: "<i>%{user}</i> has completed the task <strong>%{module}</strong>. 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:

View file

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

View file

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

View file

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

View file

@ -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');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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