From cf8566df336e1a0b74f270a5610d0359978bfae0 Mon Sep 17 00:00:00 2001 From: Oleksii Kriuchykhin Date: Fri, 10 Feb 2017 14:27:20 +0100 Subject: [PATCH] Add task complete/uncomplete functionality [SCI-999] --- app/assets/javascripts/my_modules.js | 38 +++++++- .../javascripts/my_modules/protocols.js.erb | 2 +- app/assets/javascripts/protocols/steps.js.erb | 12 +++ app/assets/stylesheets/constants.scss | 1 + app/assets/stylesheets/my_modules.scss | 16 ++++ app/assets/stylesheets/projects.scss | 26 ++++++ app/controllers/my_modules_controller.rb | 87 ++++++++++++++++++- app/controllers/steps_controller.rb | 31 ++++++- app/helpers/permission_helper.rb | 4 + app/models/activity.rb | 4 +- app/models/my_module.rb | 32 +++++++ .../canvas/full_zoom/_my_module.html.erb | 26 +++--- .../canvas/medium_zoom/_my_module.html.erb | 5 +- .../canvas/small_zoom/_my_module.html.erb | 5 +- app/views/my_modules/_due_date.html.erb | 2 +- app/views/my_modules/_due_date_label.html.erb | 31 +++++-- app/views/my_modules/_module_header.html.erb | 25 +++++- .../_module_header_due_date_label.html.erb | 24 ++++- .../my_modules/_module_state_label.html.erb | 8 ++ app/views/my_modules/_state_buttons.html.erb | 15 ++++ app/views/my_modules/protocols.html.erb | 1 + .../elements/_my_module_element.html.erb | 4 + config/initializers/extends.rb | 3 + config/locales/en.yml | 13 +++ config/routes.rb | 1 + .../20170207100731_add_state_to_tasks.rb | 18 ++++ 26 files changed, 394 insertions(+), 40 deletions(-) create mode 100644 app/views/my_modules/_module_state_label.html.erb create mode 100644 app/views/my_modules/_state_buttons.html.erb create mode 100644 db/migrate/20170207100731_add_state_to_tasks.rb diff --git a/app/assets/javascripts/my_modules.js b/app/assets/javascripts/my_modules.js index 5717b94fd..5da8865b8 100644 --- a/app/assets/javascripts/my_modules.js +++ b/app/assets/javascripts/my_modules.js @@ -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(); diff --git a/app/assets/javascripts/my_modules/protocols.js.erb b/app/assets/javascripts/my_modules/protocols.js.erb index f0b3d0e9c..7beee19be 100644 --- a/app/assets/javascripts/my_modules/protocols.js.erb +++ b/app/assets/javascripts/my_modules/protocols.js.erb @@ -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); diff --git a/app/assets/javascripts/protocols/steps.js.erb b/app/assets/javascripts/protocols/steps.js.erb index f94befa87..8e108b001 100644 --- a/app/assets/javascripts/protocols/steps.js.erb +++ b/app/assets/javascripts/protocols/steps.js.erb @@ -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"); diff --git a/app/assets/stylesheets/constants.scss b/app/assets/stylesheets/constants.scss index 136383817..a37164bda 100644 --- a/app/assets/stylesheets/constants.scss +++ b/app/assets/stylesheets/constants.scss @@ -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; diff --git a/app/assets/stylesheets/my_modules.scss b/app/assets/stylesheets/my_modules.scss index 96ee124ac..e2f96337d 100644 --- a/app/assets/stylesheets/my_modules.scss +++ b/app/assets/stylesheets/my_modules.scss @@ -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 { diff --git a/app/assets/stylesheets/projects.scss b/app/assets/stylesheets/projects.scss index 731c902ef..3f867afdd 100644 --- a/app/assets/stylesheets/projects.scss +++ b/app/assets/stylesheets/projects.scss @@ -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; } diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 38be3a7ae..99e6665e6 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -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 diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb index cf0480acc..837aa3d40 100644 --- a/app/controllers/steps_controller.rb +++ b/app/controllers/steps_controller.rb @@ -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 diff --git a/app/helpers/permission_helper.rb b/app/helpers/permission_helper.rb index 6936d81b9..fd74d2971 100644 --- a/app/helpers/permission_helper.rb +++ b/app/helpers/permission_helper.rb @@ -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) diff --git a/app/models/activity.rb b/app/models/activity.rb index 9c8de5207..5816ef7e8 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -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 diff --git a/app/models/my_module.rb b/app/models/my_module.rb index 5e70cc9ff..02bc70fb1 100644 --- a/app/models/my_module.rb +++ b/app/models/my_module.rb @@ -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 diff --git a/app/views/canvas/full_zoom/_my_module.html.erb b/app/views/canvas/full_zoom/_my_module.html.erb index 25784a24b..73fc963af 100644 --- a/app/views/canvas/full_zoom/_my_module.html.erb +++ b/app/views/canvas/full_zoom/_my_module.html.erb @@ -1,4 +1,7 @@ -
<%= " alert-yellow" if my_module.is_one_day_prior? %>" +
+ <%= " 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 @@
-
-
- <%= 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" %> -
-
- <% 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 %> -
-
+ <% 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 %>