mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
eae369ec1f
https://community.openproject.org/wp/68402 There was a bug where when switching to manual mode and clearing the dates, the controller would also clear the duration and consider it as being done by the user (touched field). Then when switching to automatic mode, before calling the SetAttributesService, start date and finish date would be discarded because the work package is in automatic mode. The SetAttributesService would then think that the duration being empty was done by the user, and would then also unset the finish date. That lead to the incorrect display of the dates and duration in the date picker: they would appear as empty instead of having the dates inherited from the child. Fix is to also discard the duration if the work package is automatically scheduled and has children.
305 lines
8.9 KiB
Ruby
305 lines
8.9 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
# -- copyright
|
|
# OpenProject is an open source project management software.
|
|
# Copyright (C) the OpenProject GmbH
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License version 3.
|
|
#
|
|
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
|
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
|
# Copyright (C) 2010-2013 the ChiliProject Team
|
|
#
|
|
# This program is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU General Public License
|
|
# as published by the Free Software Foundation; either version 2
|
|
# of the License, or (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
|
#
|
|
# See COPYRIGHT and LICENSE files for more details.
|
|
# ++
|
|
|
|
class WorkPackages::DatePickerController < ApplicationController
|
|
ERROR_PRONE_ATTRIBUTES = %i[start_date
|
|
due_date
|
|
duration].freeze
|
|
|
|
layout false
|
|
|
|
before_action :find_work_package, except: %i[new create preview]
|
|
authorization_checked! :show, :preview, :update, :edit, :new, :create
|
|
|
|
attr_accessor :work_package
|
|
|
|
def show
|
|
set_date_attributes_to_work_package
|
|
render_form
|
|
end
|
|
|
|
def new
|
|
make_fake_initial_work_package
|
|
set_date_attributes_to_work_package
|
|
render_form
|
|
end
|
|
|
|
def edit
|
|
set_date_attributes_to_work_package
|
|
render_form
|
|
end
|
|
|
|
def preview
|
|
if params[:work_package_id]
|
|
find_work_package
|
|
else
|
|
make_fake_initial_work_package
|
|
end
|
|
|
|
set_date_attributes_to_work_package
|
|
render_form(preview: true)
|
|
end
|
|
|
|
def create
|
|
make_fake_initial_work_package
|
|
service_call = set_date_attributes_to_work_package
|
|
|
|
if service_call.errors
|
|
.map(&:attribute)
|
|
.intersect?(ERROR_PRONE_ATTRIBUTES)
|
|
render_form(status: :unprocessable_entity)
|
|
else
|
|
render json: {
|
|
startDate: @work_package.start_date,
|
|
dueDate: @work_package.due_date,
|
|
duration: @work_package.duration,
|
|
scheduleManually: @work_package.schedule_manually,
|
|
includeNonWorkingDays: if @work_package.ignore_non_working_days.nil?
|
|
false
|
|
else
|
|
@work_package.ignore_non_working_days
|
|
end
|
|
}
|
|
end
|
|
end
|
|
|
|
def update
|
|
wp_params = work_package_datepicker_params
|
|
wp_params = manage_params_for_automatic_mode(wp_params)
|
|
|
|
service_call = WorkPackages::UpdateService
|
|
.new(user: current_user,
|
|
model: @work_package)
|
|
.call(wp_params)
|
|
|
|
if service_call.success?
|
|
head :ok
|
|
else
|
|
render_form(status: :unprocessable_entity)
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def render_form(status: :ok, preview: false)
|
|
render :show,
|
|
locals: {
|
|
work_package:,
|
|
schedule_manually:,
|
|
focused_field:,
|
|
touched_field_map:,
|
|
date_mode:,
|
|
preview:
|
|
},
|
|
status:
|
|
end
|
|
|
|
def focused_field
|
|
return params[:focused_field] if params[:focused_field].present?
|
|
|
|
trigger = params[:field]
|
|
|
|
# For automatic scheduling, we focus the due date initially and do not switch to start date after touching it
|
|
if !ActiveModel::Type::Boolean.new.cast(schedule_manually) && trigger != "duration"
|
|
return :due_date
|
|
end
|
|
|
|
# Decide which field to focus next
|
|
case trigger
|
|
when "startDate"
|
|
:start_date
|
|
when "work_package[start_date]"
|
|
handle_focus_order_for_fields(:start_date, :due_date)
|
|
when "work_package[duration]", "duration"
|
|
:duration
|
|
when "dueDate"
|
|
:due_date
|
|
when "work_package[due_date]"
|
|
handle_focus_order_for_fields(:due_date, :start_date)
|
|
else
|
|
:start_date
|
|
end
|
|
end
|
|
|
|
def find_work_package
|
|
@work_package = WorkPackage.visible.find(params[:work_package_id])
|
|
end
|
|
|
|
def touched_field_map
|
|
if params[:work_package]
|
|
params.require(:work_package)
|
|
.slice("schedule_manually_touched",
|
|
"ignore_non_working_days_touched",
|
|
"start_date_touched",
|
|
"due_date_touched",
|
|
"duration_touched")
|
|
.transform_values { it == "true" }
|
|
.permit!
|
|
else
|
|
{}
|
|
end
|
|
end
|
|
|
|
def schedule_manually
|
|
find_if_present(params[:schedule_manually]) ||
|
|
find_if_present(params.dig(:work_package, :schedule_manually)) ||
|
|
work_package.schedule_manually
|
|
end
|
|
|
|
def date_mode
|
|
# Once in range mode, always in range mode
|
|
return params[:date_mode] if params[:date_mode].present? && params[:date_mode] == "range"
|
|
|
|
if work_package.start_date.nil? || work_package.due_date.nil?
|
|
"single"
|
|
else
|
|
"range"
|
|
end
|
|
end
|
|
|
|
def find_if_present(value)
|
|
value.presence
|
|
end
|
|
|
|
def work_package_datepicker_params
|
|
if params[:work_package]
|
|
handle_milestone_dates
|
|
handle_form_cleared
|
|
|
|
params.require(:work_package)
|
|
.slice(*allowed_touched_params)
|
|
.merge(schedule_manually:, date_mode:, triggering_field: params[:triggering_field])
|
|
.permit!
|
|
else
|
|
{}
|
|
end
|
|
end
|
|
|
|
def allowed_touched_params
|
|
allowed_params.filter { touched?(it) }
|
|
end
|
|
|
|
def allowed_params
|
|
%i[schedule_manually ignore_non_working_days start_date due_date duration]
|
|
end
|
|
|
|
def touched?(field)
|
|
touched_field_map[:"#{field}_touched"]
|
|
end
|
|
|
|
def make_fake_initial_work_package
|
|
initial_params = params.require(:work_package)
|
|
.require(:initial)
|
|
.permit(:start_date, :due_date, :duration, :ignore_non_working_days)
|
|
@work_package = WorkPackage.new(initial_params)
|
|
@work_package.clear_changes_information
|
|
end
|
|
|
|
def set_date_attributes_to_work_package
|
|
wp_params = work_package_datepicker_params
|
|
wp_params = manage_params_for_automatic_mode(wp_params)
|
|
|
|
if wp_params.present?
|
|
WorkPackages::SetAttributesService
|
|
.new(user: current_user,
|
|
model: @work_package,
|
|
contract_class:)
|
|
.call(wp_params)
|
|
end
|
|
end
|
|
|
|
def contract_class
|
|
if @work_package.new_record?
|
|
WorkPackages::CreateContract
|
|
else
|
|
WorkPackages::UpdateContract
|
|
end
|
|
end
|
|
|
|
def handle_milestone_dates
|
|
if work_package.is_milestone? && params.require(:work_package).has_key?(:start_date)
|
|
# Set the dueDate as the SetAttributesService will otherwise throw an error because the fields do not match
|
|
params.require(:work_package)[:due_date] = params.require(:work_package)[:start_date]
|
|
params.require(:work_package)[:due_date_touched] = "true"
|
|
end
|
|
end
|
|
|
|
def handle_form_cleared
|
|
touched_params = params.require(:work_package).slice(*allowed_touched_params)
|
|
|
|
if two_fields_cleared?(touched_params)
|
|
# If two fields are already manually cleared, we assume that the user wants to clear the whole form
|
|
params_array = %i[start_date due_date duration]
|
|
|
|
params_array.each do |param|
|
|
if touched_params[param].nil?
|
|
params.require(:work_package)[param] = ""
|
|
params.require(:work_package)["#{param}_touched"] = "true"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def two_fields_cleared?(wp_params)
|
|
start_date = wp_params[:start_date]
|
|
due_date = wp_params[:due_date]
|
|
duration = wp_params[:duration]
|
|
|
|
# Check which params are set to an empty string
|
|
empty_params = [start_date, due_date, duration].select { |param| param == "" }
|
|
# Check which param was not touched
|
|
missing_param = [start_date, due_date, duration].select(&:nil?)
|
|
|
|
# If two values are deleted and one is untouched, return tru
|
|
empty_params.length == 2 && missing_param.length == 1
|
|
end
|
|
|
|
def handle_focus_order_for_fields(trigger_field, alternative_field)
|
|
if !!params[:work_package][:"#{trigger_field}_touched"] && params[:work_package][:"#{trigger_field}"].blank?
|
|
# Special case, when deleting a value: we want to keep the focus on that field instead of moving to the next field
|
|
trigger_field
|
|
else
|
|
alternative_field
|
|
end
|
|
end
|
|
|
|
def manage_params_for_automatic_mode(wp_params)
|
|
return wp_params if wp_params["schedule_manually"] != "false"
|
|
|
|
# For WP with children the dates and duration are always fixed
|
|
return wp_params.without("start_date", "due_date", "duration") if work_package.children.any?
|
|
|
|
# the start should be preserved and will thus be send as a parameter
|
|
wp_params["start_date"] = work_package.start_date.to_s
|
|
|
|
wp_params
|
|
end
|
|
end
|