Add actions for update status, clean old complete actions

This commit is contained in:
Urban Rotnik 2020-07-14 18:48:18 +02:00
parent aacee14fc6
commit 512d6f3229
17 changed files with 226 additions and 365 deletions

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',
contentType: 'application/json',
data: JSON.stringify({ 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

@ -17,5 +17,9 @@
margin: 8px 0;
}
}
.dropdown-menu > li {
line-height: 35px;
}
}
}

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: :complete_my_module
before_action :check_update_state_permissions, only: :update_state
before_action :set_inline_name_editing, only: %i(protocols results activities archive)
layout 'fluid'.freeze
@ -249,109 +249,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|
if can_complete_module?(@my_module)
@my_module.completed? ? @my_module.uncomplete : @my_module.complete
completed = @my_module.completed?
if @my_module.save
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
if completed
new_btn_partial = 'my_modules/state_button_uncomplete.html.erb'
else
new_btn_partial = '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: 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
else
format.json { render json: {}, status: :unprocessable_entity }
end
else
format.json { render json: {}, status: :unauthorized }
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.complete
@my_module.save
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 +299,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 +330,10 @@ class MyModulesController < ApplicationController
update_params
end
def update_status_params
params.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

@ -22,6 +22,14 @@ class MyModuleStatus < ApplicationRecord
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
private
def next_in_same_flow

View file

@ -19,6 +19,6 @@ class MyModuleStatusFlow < ApplicationRecord
end
def final_status
my_module_statuses.find_by(next_status: nil)
my_module_statuses.left_outer_joins(:next_status).find_by('next_statuses_my_module_statuses.id': nil)
end
end

View file

@ -60,85 +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
# Also checking status implications
%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? &&
(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
# 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
@ -180,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

@ -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,44 +0,0 @@
<div>
<div class="status-label">Status</div>
<div class="dropdown sci-dropdown status-flow-dropdown">
<button class="btn btn-secondary dropdown-toggle"
type="button"
id="dropdownTaskFlow"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="true"
style="background-color: #4cae4c;">
<span>In progress</span>
<span class="caret pull-right"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownTaskFlow">
<li>
<span><%= t('my_modules.protocol.options_dropdown.load_from_repo') %></span>
</li>
<li>
<span><%= t('my_modules.protocol.options_dropdown.import') %></span>
</li>
<li>
<span><%= t('my_modules.protocol.options_dropdown.export') %></span>
</li>
<li>
<span><%= t('my_modules.protocol.options_dropdown.save_to_repo') %></span>
</li>
</ul>
</div>
<% if false && 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

@ -23,10 +23,10 @@
</div>
<div class="task-details">
<%= render partial: "my_module_details" %>
<%= render partial: 'my_module_details' %>
</div>
<div class="task-flows">
<%= render partial: "my_modules/state_buttons.html.erb" %>
<%= render partial: 'my_modules/status_flow/task_flow_button', locals: { my_module: @my_module } if @my_module.my_module_status_flow %>
</div>
</div>
<!-- Notes -->
@ -119,9 +119,6 @@
<!-- Import protocol elements -->
<%= render partial: "protocols/import_export/import_elements.html.erb" %>
<!-- Complete task modal -->
<%= render partial: 'my_modules/complete_task_modal.html.erb' %>
<!-- Create new office file modal -->
<%= render partial: 'assets/wopi/create_wopi_file_modal.html.erb' %>

View file

@ -0,0 +1,31 @@
<% status = my_module.my_module_status %>
<div class="status-label">Status</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>Return to -></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>Move to -></span> <span style="<%= "background-color: ##{next_s.color}" %>"><%= next_s.name %></span>
</li>
<% end %>
<li>
<span><a href="#">View task flow</a></span>
</li>
</ul>
</div>

View file

@ -650,12 +650,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"
@ -2191,8 +2185,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,5 @@
en:
my_module_statuses:
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'
@ -425,11 +392,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

@ -123,39 +123,75 @@ 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,
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