Add task complete/uncomplete functionality [SCI-999]

This commit is contained in:
Oleksii Kriuchykhin 2017-02-10 14:27:20 +01:00
parent c2a69982aa
commit cf8566df33
26 changed files with 394 additions and 40 deletions

View file

@ -58,7 +58,7 @@ function bindEditDueDateAjax() {
$(".due-date-link")
.on("ajax:success", function (ev, data, status) {
var dueDateLink = $(".due-date-refresh");
var dueDateLink = $('.task-due-date');
// Load contents
editDueDateModalBody.html(data.html);
@ -217,6 +217,42 @@ function bindEditTagsAjax() {
});
}
// Sets callback for completing/uncompleting task
function applyTaskCompletedCallBack() {
// First, remove old event handlers, as we use turbolinks
$("[data-action='complete-task'], [data-action='uncomplete-task']").off();
$("[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');
}
$('.task-due-date').html(data.module_header_due_date_label);
$('.task-state-label').html(data.module_state_label);
button.find('button').html(data.new_title);
},
error: function() {
}
});
});
}
$(document).ready(function() {
applyTaskCompletedCallBack();
});
bindEditDueDateAjax();
bindEditDescriptionAjax();
bindEditTagsAjax();

View file

@ -75,7 +75,7 @@ function bindEditDueDateAjax() {
$(".due-date-link")
.on("ajax:success", function(ev, data, status) {
var dueDateLink = $(".due-date-refresh");
var dueDateLink = $(".task-due-date");
// Load contents
editDueDateModalBody.html(data.html);

View file

@ -26,6 +26,9 @@ function applyCheckboxCallBack() {
// Sets callback for completing/uncompleting step
function applyStepCompletedCallBack() {
// First, remove old event handlers, as we use turbolinks
$("[data-action='complete-step'], [data-action='uncomplete-step']").off();
$("[data-action='complete-step'], [data-action='uncomplete-step']").on('click', function(e){
var button = $(this);
var step = $(this).parents(".step");
@ -44,6 +47,15 @@ function applyStepCompletedCallBack() {
button = step.find("[data-action='complete-step']");
button.attr("data-action", "uncomplete-step");
button.find(".btn").removeClass("btn-primary").addClass("btn-default");
if (data.task_completed) {
task_button = $("[data-action='complete-task']");
task_button.attr('data-action', 'uncomplete-task');
task_button.find('.btn')
.removeClass('btn-primary').addClass('btn-default');
$('.task-due-date').html(data.module_header_due_date_label);
$('.task-state-label').html(data.module_state_label);
task_button.find('button').html(data.task_button_title);
}
}
else {
step.addClass("not-completed").removeClass("completed");

View file

@ -30,6 +30,7 @@ $color-cloud: rgba(0, 0, 0, .1);
// Miscelaneous colors
$color-mystic: #eaeff2;
$color-candlelight: #ffda23;
$color-saturated-green: #008600;
// Red colors
$color-mojo: #cf4b48;

View file

@ -4,6 +4,22 @@
@import 'constants';
// Protocols index page
.task-due-date,
.task-state-label {
.alert-green {
color: $color-saturated-green;
}
.alert-yellow {
color: $color-candlelight;
}
.alert-red {
color: $color-milano-red;
}
}
/* Results index page */
#results {

View file

@ -207,6 +207,10 @@ path, ._jsPlumb_endpoint {
color: $color-emperor;
}
.panel-body .due-date-label {
margin-left: 30px;
}
&.expanded {
z-index: 30;
}
@ -217,6 +221,16 @@ path, ._jsPlumb_endpoint {
&.module-hover {
@include box-shadow(0 0 0 5px $color-module-hover);
}
&.alert-green .panel-body {
color: $color-saturated-green;
font-weight: bold;
.due-date-link {
color: $color-saturated-green;
}
}
&.alert-yellow .panel-body {
color: $color-candlelight;
font-weight: bold;
@ -249,6 +263,13 @@ path, ._jsPlumb_endpoint {
&.module-hover {
@include box-shadow(0 0 0 5px $color-module-hover);
}
&.alert-green {
border-color: $color-saturated-green;
border-radius: 8px;
border-width: 4px;
}
&.alert-yellow {
border-color: $color-candlelight;
border-width: 4px;
@ -321,6 +342,11 @@ path, ._jsPlumb_endpoint {
&.module-hover {
@include box-shadow(0 0 0 5px $color-module-hover);
}
&.alert-green {
border-color: $color-saturated-green;
}
&.alert-yellow {
border-color: $color-candlelight;
}

View file

@ -8,7 +8,7 @@ class MyModulesController < ApplicationController
:description, :due_date, :protocols, :results,
:samples, :activities, :activities_tab,
:assign_samples, :unassign_samples,
:delete_samples,
:delete_samples, :toggle_task_state,
:samples_index, :archive]
before_action :load_vars_nested, only: [:new, :create]
before_action :check_edit_permissions, only: [
@ -192,8 +192,13 @@ class MyModulesController < ApplicationController
if saved
format.json {
alerts = []
alerts << "alert-red" if @my_module.is_overdue?
alerts << "alert-yellow" if @my_module.is_one_day_prior?
if @my_module.is_overdue? && !@my_module.completed?
alerts << 'alert-red'
elsif @my_module.is_one_day_prior? && !@my_module.completed?
alerts << 'alert-yellow'
elsif @my_module.completed?
alerts << 'alert-green'
end
render json: {
status: :ok,
due_date_label: render_to_string(
@ -339,6 +344,82 @@ class MyModulesController < ApplicationController
end
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
# Create activity
str = if completed
'activities.complete_module'
else
'activities.uncomplete_module'
end
message = t(str,
user: current_user.full_name,
module: @my_module.name)
Activity.create(
user: current_user,
project: @project,
my_module: @my_module,
message: message,
type_of: completed ? :complete_task : :uncomplete_task
)
if completed
title = I18n.t('notifications.types.recent_changes')
message = I18n.t('notifications.task_completed',
user: current_user.name,
module: @my_module.name,
date: l(@my_module.completed_on, format: :full),
project: @project.name,
experiment: @my_module.experiment.name)
notification = Notification.create(
type_of: :recent_changes,
title: title,
message: sanitize_input(message),
generator_user_id: current_user.id
)
if current_user.recent_notification
UserNotification.create(
notification: notification, user: current_user
)
end
end
# Create localized title for complete/uncomplete button
button_title = if completed
t('my_modules.buttons.uncomplete')
else
t('my_modules.buttons.complete')
end
format.json do
render json: {
new_title: button_title,
completed: completed,
module_header_due_date_label: render_to_string(
partial: 'my_modules/module_header_due_date_label.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
private
def load_vars

View file

@ -302,6 +302,10 @@ class StepsController < ApplicationController
end
if step.save
if protocol.in_module?
task_completed = protocol.my_module.check_completness
end
# Create activity
if changed
completed_steps = protocol.steps.where(completed: true).count
@ -336,10 +340,29 @@ class StepsController < ApplicationController
localized_title = !completed ?
t("protocols.steps.options.complete_title") :
t("protocols.steps.options.uncomplete_title")
format.json {
render json: {new_title: localized_title}, status: :accepted
}
task_button_title =
t('my_modules.buttons.uncomplete') if task_completed
format.json do
if task_completed
render json: {
new_title: localized_title,
task_completed: task_completed,
task_button_title: task_button_title,
module_header_due_date_label: render_to_string(
partial: 'my_modules/module_header_due_date_label.html.erb',
locals: { my_module: step.protocol.my_module }
),
module_state_label: render_to_string(
partial: 'my_modules/module_state_label.html.erb',
locals: { my_module: step.protocol.my_module }
)
},
status: :accepted
else
render json: { new_title: localized_title },
status: :accepted
end
end
else
format.json {
render json: {}, status: :unprocessable_entity

View file

@ -530,6 +530,10 @@ module PermissionHelper
is_user_or_higher_of_project(my_module.experiment.project)
end
def can_complete_module(my_module)
is_technician_or_higher_of_project(my_module.experiment.project)
end
# ---- RESULTS PERMISSIONS ----
def can_view_results_in_module(my_module)

View file

@ -52,7 +52,9 @@ class Activity < ActiveRecord::Base
:delete_report,
:edit_report,
:assign_sample,
:unassign_sample
:unassign_sample,
:complete_task,
:uncomplete_task
]
validates :type_of, presence: true

View file

@ -1,6 +1,8 @@
class MyModule < ActiveRecord::Base
include ArchivableModel, SearchableModel
enum state: Extends::TASKS_STATES
before_create :create_blank_protocol
auto_strip_attributes :name, :description, nullify: false
@ -375,6 +377,36 @@ class MyModule < ActiveRecord::Base
{ x: 0, y: positions.last[1] + HEIGHT }
end
def completed?
state == 'completed'
end
# Mark task completed if all steps become completed
def check_completness
if protocol && protocol.steps.count > 0
completed = true
protocol.steps.find_each do |step|
completed = false unless step.completed
end
if completed
complete
save!
return true
end
end
false
end
def complete
self.state = 'completed'
self.completed_on = DateTime.now
end
def uncomplete
self.state = 'uncompleted'
self.completed_on = nil
end
private
def create_blank_protocol

View file

@ -1,4 +1,7 @@
<div class="panel panel-default module-large<%= " alert-red" if my_module.is_overdue? %><%= " alert-yellow" if my_module.is_one_day_prior? %>"
<div class="panel panel-default module-large
<%= " alert-red" if my_module.is_overdue? && !my_module.completed? %>
<%= " alert-yellow" if my_module.is_one_day_prior? && !my_module.completed? %>
<%= " alert-green" if my_module.completed? %>"
id="<%= my_module.id %>"
data-module-id="<%= my_module.id %>"
data-module-name="<%= my_module.name %>"
@ -30,20 +33,13 @@
</div>
<div class="panel-body">
<div class="row">
<div class="col-xs-6">
<%= link_to_if can_edit_module(my_module) && can_edit_modules(my_module.experiment), t("experiments.canvas.full_zoom.due_date"), due_date_my_module_path(my_module, format: :json), remote: true, class: "due-date-link" %>
</div>
<div class="col-xs-6">
<% if can_edit_module(my_module) && can_edit_modules(my_module.experiment) %>
<%= link_to due_date_my_module_path(my_module, format: :json), remote: true, class: "due-date-link due-date-refresh" do %>
<%= render partial: "my_modules/due_date_label.html.erb", locals: { my_module: my_module } %>
<% end %>
<% else %>
<%= render partial: "my_modules/due_date_label.html.erb", locals: { my_module: my_module } %>
<% end %>
</div>
</div>
<% if can_edit_module(my_module) && can_edit_modules(my_module.experiment) %>
<%= link_to due_date_my_module_path(my_module, format: :json), remote: true, class: "due-date-link due-date-refresh" do %>
<%= render partial: "my_modules/due_date_label.html.erb", locals: { my_module: my_module } %>
<% end %>
<% else %>
<%= render partial: "my_modules/due_date_label.html.erb", locals: { my_module: my_module } %>
<% end %>
</div>
<div class="panel-footer panel-footer-scinote buttons-container">

View file

@ -1,4 +1,7 @@
<div class="panel panel-default module-medium<%= " alert-red" if my_module.is_overdue? %><%= " alert-yellow" if my_module.is_one_day_prior? %>"
<div class="panel panel-default module-medium
<%= " alert-red" if my_module.is_overdue? && !my_module.completed? %>
<%= " alert-yellow" if my_module.is_one_day_prior? && !my_module.completed? %>
<%= " alert-green" if my_module.completed? %>"
id="<%= my_module.id %>"
data-module-id="<%= my_module.id %>"
data-module-name="<%= my_module.name %>"

View file

@ -1,4 +1,7 @@
<div class="module-small<%= " alert-red" if my_module.is_overdue? %><%= " alert-yellow" if my_module.is_one_day_prior? %>"
<div class="module-small
<%= " alert-red" if my_module.is_overdue? && !my_module.completed? %>
<%= " alert-yellow" if my_module.is_one_day_prior? && !my_module.completed? %>
<%= " alert-green" if my_module.completed? %>"
id="<%= my_module.id %>"
data-module-id="<%= my_module.id %>"
data-module-name="<%= my_module.name %>"

View file

@ -1,3 +1,3 @@
<%= bootstrap_form_for @my_module, url: my_module_path(@my_module, format: :json), remote: :true do |f| %>
<%= f.datetime_picker :due_date, label: t("my_modules.due_date.label"), clear: true %>
<% end %>
<% end %>

View file

@ -1,8 +1,29 @@
<% if my_module.due_date then %>
<%=l my_module.due_date, format: :full_date %>
<% if my_module.is_overdue? or my_module.is_one_day_prior? %>
<% if my_module.completed? %>
<%= t("my_modules.states.completed") %>
<span class="due-date-label">
<%= l(my_module.completed_on, format: :full_date) %>
<span class="glyphicon glyphicon-ok"></span>
</span>
<% elsif my_module.is_one_day_prior? %>
<%= t("my_modules.states.due_soon") %>
<span class="due-date-label">
<%=l my_module.due_date, format: :full_date %>
<span class="glyphicon glyphicon-alert"></span>
<% end %>
</span>
<% elsif my_module.is_overdue? %>
<%= t("my_modules.states.overdue") %>
<span class="due-date-label">
<%=l my_module.due_date, format: :full_date %>
<span class="glyphicon glyphicon-alert"></span>
</span>
<% elsif my_module.due_date %>
<%= t("experiments.canvas.full_zoom.due_date") %>
<span class="due-date-label">
<%=l my_module.due_date, format: :full_date %>
</span>
<% else %>
<em><%=t "experiments.canvas.full_zoom.no_due_date" %></em>
<%= t("experiments.canvas.full_zoom.due_date") %>
<span class="due-date-label">
<em><%=t "experiments.canvas.full_zoom.no_due_date" %></em>
</span>
<% end %>

View file

@ -1,5 +1,5 @@
<div class="row">
<div class="col-xs-6 col-sm-6 col-md-3">
<div class="col-xs-6 col-sm-6 col-md-4">
<div class="badge-icon bg-primary">
<span class="glyphicon glyphicon-calendar"></span>
</div>
@ -9,7 +9,7 @@
</div>
</div>
<div class="col-xs-6 col-sm-6 col-md-3">
<div class="col-xs-6 col-sm-6 col-md-4">
<div class="badge-icon bg-primary">
<% if can_edit_module(@my_module) then %>
<%= link_to due_date_my_module_path(@my_module, format: :json), remote: true, class: "due-date-link", style: "color: inherit" do %>
@ -22,18 +22,35 @@
<div class="well well-sm">
<span class="hidden-xs hidden-sm hidden-md"><%=t "my_modules.module_header.due_date" %></span>
<% if can_edit_module(@my_module) then %>
<%= link_to due_date_my_module_path(@my_module, format: :json), remote: true, class: "due-date-link due-date-refresh", style: "color: inherit" do %>
<%= link_to due_date_my_module_path(@my_module, format: :json), remote: true, class: "due-date-link", style: "color: inherit" do %>
<span class="task-due-date">
<%= render partial: "module_header_due_date_label.html.erb",
locals: { my_module: @my_module } %>
</span>
<% end %>
<% else %>
<span class="task-due-date">
<%= render partial: "module_header_due_date_label.html.erb",
locals: { my_module: @my_module } %>
</span>
<% end %>
</div>
</div>
<div class="col-xs-12 col-sm-12 col-md-6" id="module-tags" data-module-tags-url="<%= my_module_my_module_tags_url(@my_module, format: :json) %>">
<div class="col-xs-6 col-sm-6 col-md-4">
<div class="badge-icon bg-primary">
<span class="glyphicon glyphicon-ok"></span>
</div>
<div class="well well-sm">
<span class="task-state-label">
<%= render partial: "module_state_label.html.erb",
locals: { my_module: @my_module } %>
</span>
</div>
</div>
</div>
<div class="row">
<div class="col-xs-12 col-sm-12 col-md-12" id="module-tags" data-module-tags-url="<%= my_module_my_module_tags_url(@my_module, format: :json) %>">
<div class="badge-icon bg-primary">
<% if can_edit_tags_for_module(@my_module) %>
<a class="edit-tags-link" data-remote="true" href="<%= my_module_tags_edit_url(@my_module, format: :json) %>" style="color: inherit">

View file

@ -1,5 +1,23 @@
<% if @my_module.due_date.blank? %>
<% if my_module.due_date.blank? %>
<em><%=t "experiments.canvas.full_zoom.no_due_date" %></em>
<% else %>
<strong><%= l(@my_module.due_date, format: :full) %></strong>
<% end %>
<strong>
<% if my_module.completed? %>
<span class="alert-green">
<%= l(my_module.due_date, format: :full) %>
</span>
<% elsif my_module.is_one_day_prior? %>
<span class="alert-yellow">
<%= l(my_module.due_date, format: :full) %>
(<%= t('my_modules.states.due_soon') %>)
</span>
<% elsif my_module.is_overdue? %>
<span class="alert-red">
<%= l(my_module.due_date, format: :full) %>
(<%= t('my_modules.states.overdue') %>)
</span>
<% else %>
<%= l(my_module.due_date, format: :full) %>
<% end %>
</strong>
<% end %>

View file

@ -0,0 +1,8 @@
<%= t('my_modules.states.state_label') %>
<% 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

@ -0,0 +1,15 @@
<div class="btn-group" style="margin-left: 15px;">
<% if !@my_module.completed? %>
<div data-action="complete-task" data-link-url="<%= toggle_task_state_my_module_path(@my_module) %>">
<button class="btn btn-primary">
<%= t("my_modules.buttons.complete") %>
</button>
</div>
<% else @my_module.completed? %>
<div data-action="uncomplete-task" data-link-url="<%= toggle_task_state_my_module_path(@my_module) %>">
<button class="btn btn-default">
<%= t("my_modules.buttons.uncomplete") %>
</button>
</div>
<% end %>
</div>

View file

@ -9,6 +9,7 @@
<%= render partial: "my_modules/protocols/protocol_status_bar.html.erb" %>
</div>
<%= render partial: "my_modules/protocols/protocol_buttons.html.erb" %>
<%= render partial: "my_modules/state_buttons.html.erb" %>
</div>
<div data-role="steps-container">

View file

@ -25,6 +25,10 @@
<% else %>
<em><%=t "projects.reports.elements.module.no_due_date" %></em>
<% end %>
<% if my_module.completed? %>
<%= t("my_modules.states.completed") %>
<%= l(my_module.completed_on, format: :full) %>
<% end %>
</div>
</div>
<div class="row">

View file

@ -14,4 +14,7 @@ class Extends
NOTIFICATIONS_TYPES = { assignment: 0,
recent_changes: 1,
system_message: 2 }
TASKS_STATES = { uncompleted: 0,
completed: 1 }
end

View file

@ -430,12 +430,22 @@ en:
add_user_generic_error: "An error occured. "
my_modules:
buttons:
complete: "Complete task"
uncomplete: "Uncomplete task"
description:
title: "Edit task %{module} description"
label: "Description"
due_date:
title: "Edit task %{module} due date"
label: "Due date"
states:
state_label: "Status:"
in_progress: "In progress"
completed: "Completed on"
completed_on: "Task completed (%{date})"
due_soon: "due soon"
overdue: "overdue"
samples:
head_title: "%{project} | %{module} | Sample library"
module_archive:
@ -979,6 +989,8 @@ en:
archive_module: "<i>%{user}</i> moved task <strong>%{module}</strong> to archive."
restore_module: "<i>%{user}</i> restored task <strong>%{module}</strong> from archive."
change_module_description: "<i>%{user}</i> changed task <strong>%{module}</strong>'s description."
complete_module: "<i>%{user}</i> completed task <strong>%{module}</strong>."
uncomplete_module: "<i>%{user}</i> uncompleted task <strong>%{module}</strong>."
create_step: "<i>%{user}</i> created Step %{step} <strong>%{step_name}</strong>."
destroy_step: "<i>%{user}</i> deleted Step %{step} <strong>%{step_name}</strong>."
add_comment_to_step: "<i>%{user}</i> commented on Step %{step} <strong>%{step_name}</strong>."
@ -1455,6 +1467,7 @@ en:
email_title: "You've received a sciNote notification!"
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}"
atwho:
no_results: "No results found"

View file

@ -268,6 +268,7 @@ Rails.application.routes.draw do
get 'results' # Results view for single module
get 'samples' # Samples view for single module
get 'archive' # Archive view for single module
post 'toggle_task_state'
# Renders sample datatable for single module (ajax action)
post 'samples_index'
post :assign_samples,

View file

@ -0,0 +1,18 @@
class AddStateToTasks < ActiveRecord::Migration
def up
add_column :my_modules,
:state,
:integer,
limit: 1,
default: 0
add_column :my_modules,
:completed_on,
:datetime,
null: true
end
def down
remove_column :my_modules, :state
remove_column :my_modules, :completed_on
end
end