mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
[#73798] Remove scrum_projects feature flag
Make Backlogs use the sprint-based behavior unconditionally and remove the old feature-flagged branches from controllers, routes, representers, and supporting helpers. Update the affected Backlogs specs and PDF export expectations to match the permanent sprint model and keep list reordering stable when moving work packages between backlog and sprint scopes. https://community.openproject.org/wp/73798
This commit is contained in:
@@ -85,7 +85,7 @@ module Versions
|
||||
end
|
||||
|
||||
def button_links
|
||||
[edit_link, delete_link, backlogs_edit_link].compact
|
||||
[edit_link, delete_link].compact
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
@@ -110,35 +110,6 @@ module Versions
|
||||
end
|
||||
end
|
||||
|
||||
if backlogs_enabled?
|
||||
setting = version_setting_for_project
|
||||
|
||||
f.select_list(
|
||||
name: "version[version_settings_attributes][][display]",
|
||||
scope_name_to_model: false,
|
||||
label: I18n.t(:label_column_in_backlog),
|
||||
input_width: :small
|
||||
) do |list|
|
||||
position_display_options.each do |label, value|
|
||||
list.option(label:, value:, selected: setting.display == value)
|
||||
end
|
||||
end
|
||||
|
||||
if setting.persisted?
|
||||
f.hidden(
|
||||
name: "version[version_settings_attributes][][id]",
|
||||
value: setting.id,
|
||||
scope_name_to_model: false
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
f.hidden(
|
||||
name: "project_id",
|
||||
value: project.id,
|
||||
scope_name_to_model: false
|
||||
)
|
||||
|
||||
render_custom_fields(form: f)
|
||||
|
||||
f.submit(
|
||||
@@ -177,35 +148,5 @@ module Versions
|
||||
def wiki_pages_disabled?
|
||||
contract.assignable_wiki_pages.none?
|
||||
end
|
||||
|
||||
def backlogs_enabled?
|
||||
resolved_project.backlogs_enabled? && !OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
end
|
||||
|
||||
def resolved_project
|
||||
@project || version.project
|
||||
end
|
||||
|
||||
def version_setting_for_project
|
||||
setting = version.version_settings.detect { |vs| vs.project_id == resolved_project.id || vs.project_id.nil? }
|
||||
setting || version.version_settings.new(display: VersionSetting::DISPLAY_LEFT, project: resolved_project)
|
||||
end
|
||||
|
||||
def position_display_options
|
||||
[VersionSetting::DISPLAY_NONE,
|
||||
VersionSetting::DISPLAY_LEFT,
|
||||
VersionSetting::DISPLAY_RIGHT].map { |s| [humanize_display_option(s), s] }
|
||||
end
|
||||
|
||||
def humanize_display_option(option)
|
||||
case option
|
||||
when VersionSetting::DISPLAY_NONE
|
||||
I18n.t("version_settings_display_option_none")
|
||||
when VersionSetting::DISPLAY_LEFT
|
||||
I18n.t("version_settings_display_option_left")
|
||||
when VersionSetting::DISPLAY_RIGHT
|
||||
I18n.t("version_settings_display_option_right")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -359,9 +359,6 @@ class PermittedParams
|
||||
end
|
||||
|
||||
def version
|
||||
# `version_settings_attributes` is from a plugin. Unfortunately as it stands
|
||||
# now it is less work to do it this way than have the plugin override this
|
||||
# method. We hopefully will change this in the future.
|
||||
permitted_params = params.fetch(:version, {}).permit(:name,
|
||||
:description,
|
||||
:effective_date,
|
||||
@@ -369,8 +366,7 @@ class PermittedParams
|
||||
:start_date,
|
||||
:wiki_page_title,
|
||||
:status,
|
||||
:sharing,
|
||||
version_settings_attributes: %i(id display project_id))
|
||||
:sharing)
|
||||
|
||||
permitted_params.merge(custom_field_values(:version, required: false))
|
||||
end
|
||||
|
||||
@@ -61,11 +61,6 @@ OpenProject::FeatureDecisions.add :jira_import,
|
||||
description: "Enables Jira Migration Tool.",
|
||||
force_active: false
|
||||
|
||||
OpenProject::FeatureDecisions.add :scrum_projects,
|
||||
description: "Enables an overhauled version of the backlogs module to " \
|
||||
"support Scrum projects with a new sprint planning experience. ",
|
||||
force_active: true
|
||||
|
||||
OpenProject::FeatureDecisions.add :user_working_times,
|
||||
description: "Enables tracking of user working hours and non-working days."
|
||||
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
t.with_text { t("backlogs.done_status") }
|
||||
end
|
||||
|
||||
if OpenProject::FeatureDecisions.scrum_projects_active? && User.current.allowed_in_project?(:share_sprint, project)
|
||||
if User.current.allowed_in_project?(:share_sprint, project)
|
||||
tab_nav.with_tab(
|
||||
selected: selected_tab?(:sharing),
|
||||
href: project_settings_backlog_sharing_path(project)
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
class InboxController < RbApplicationController
|
||||
include OpTurbo::ComponentStream
|
||||
|
||||
before_action :not_authorized_on_feature_flag_inactive
|
||||
before_action :load_work_package
|
||||
|
||||
# Deferred ActionMenu items (Primer include-fragment).
|
||||
|
||||
@@ -42,11 +42,7 @@ class Projects::Settings::BacklogsController < Projects::SettingsController
|
||||
end
|
||||
|
||||
def rebuild_positions
|
||||
if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
WorkPackages::RebuildPositionsService.new(project: @project).call
|
||||
else
|
||||
@project.rebuild_positions
|
||||
end
|
||||
WorkPackages::RebuildPositionsService.new(project: @project).call
|
||||
flash[:notice] = I18n.t("backlogs.positions_rebuilt_successfully")
|
||||
|
||||
redirect_to_backlogs_settings
|
||||
|
||||
@@ -33,7 +33,6 @@ class RbApplicationController < ApplicationController
|
||||
helper :rb_common
|
||||
|
||||
before_action :load_sprint_and_project,
|
||||
:check_if_plugin_is_configured,
|
||||
:authorize
|
||||
|
||||
private
|
||||
@@ -54,25 +53,7 @@ class RbApplicationController < ApplicationController
|
||||
@sprint_id = params.delete(:sprint_id)
|
||||
return unless @sprint_id
|
||||
|
||||
@sprint = if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
Agile::Sprint.for_project(@project).visible.find(@sprint_id)
|
||||
else
|
||||
Sprint.visible.apply_to(@project).find(@sprint_id)
|
||||
end
|
||||
end
|
||||
|
||||
def check_if_plugin_is_configured
|
||||
return if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
settings = Setting.plugin_openproject_backlogs
|
||||
if settings["story_types"].blank? || settings["task_type"].blank?
|
||||
respond_to do |format|
|
||||
format.html { render template: "shared/not_configured" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def not_authorized_on_feature_flag_inactive
|
||||
render_403 unless OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
@sprint = Agile::Sprint.for_project(@project).visible.find_by(id: @sprint_id) ||
|
||||
Sprint.visible.apply_to(@project).find(@sprint_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -32,7 +32,7 @@ class RbBurndownChartsController < RbApplicationController
|
||||
helper :burndown_charts
|
||||
|
||||
def show
|
||||
@burndown = if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
@burndown = if @sprint.is_a?(Agile::Sprint)
|
||||
Burndown.new(@sprint, @project)
|
||||
else
|
||||
@sprint.burndown(@project)
|
||||
|
||||
@@ -31,13 +31,10 @@
|
||||
class RbMasterBacklogsController < RbApplicationController
|
||||
include WorkPackages::WithSplitView
|
||||
|
||||
# Without the feature flag, there is only the top level menu item, select it
|
||||
menu_item :backlogs_legacy, only: :index
|
||||
current_menu_item [:backlog, :details] do
|
||||
:backlog
|
||||
end
|
||||
|
||||
# With the feature flag, we have a proper menu, select the correct sub entry
|
||||
menu_item :backlog, only: %i[backlog details]
|
||||
|
||||
before_action :not_authorized_on_feature_flag_inactive, only: :backlog
|
||||
before_action :load_backlogs, only: %i[index backlog]
|
||||
|
||||
def backlog
|
||||
@@ -50,13 +47,10 @@ class RbMasterBacklogsController < RbApplicationController
|
||||
end
|
||||
|
||||
def index
|
||||
return redirect_to action: :backlog if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
case turbo_frame_request_id
|
||||
when "backlogs_container"
|
||||
render partial: "list", layout: false
|
||||
if turbo_frame_request?
|
||||
render partial: "backlog_list", layout: false
|
||||
else
|
||||
render :index
|
||||
render :backlog
|
||||
end
|
||||
end
|
||||
|
||||
@@ -65,39 +59,25 @@ class RbMasterBacklogsController < RbApplicationController
|
||||
render "work_packages/split_view", layout: false
|
||||
else
|
||||
load_backlogs
|
||||
|
||||
if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
render :backlog
|
||||
else
|
||||
render :index
|
||||
end
|
||||
render :backlog
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def split_view_base_route
|
||||
if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
backlog_backlogs_project_backlogs_path(request.query_parameters)
|
||||
else
|
||||
backlogs_project_backlogs_path(request.query_parameters)
|
||||
end
|
||||
backlog_backlogs_project_backlogs_path(request.query_parameters)
|
||||
end
|
||||
|
||||
def load_backlogs
|
||||
@owner_backlogs = Backlog.owner_backlogs(@project)
|
||||
|
||||
if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
@sprints = Agile::Sprint.for_project(@project).not_completed.order_by_date
|
||||
@stories_by_sprint_id = WorkPackage
|
||||
.where(sprint: @sprints, project: @project)
|
||||
.includes(:type, :status)
|
||||
.order_by_position
|
||||
.group_by(&:sprint_id)
|
||||
@active_sprint_ids = @sprints.select(&:active?).map(&:id)
|
||||
@inbox_work_packages = Backlog.inbox_for(project: @project)
|
||||
else
|
||||
@sprint_backlogs = Backlog.sprint_backlogs(@project)
|
||||
end
|
||||
@sprints = Agile::Sprint.for_project(@project).not_completed.order_by_date
|
||||
@stories_by_sprint_id = WorkPackage
|
||||
.where(sprint: @sprints, project: @project)
|
||||
.includes(:type, :status)
|
||||
.order_by_position
|
||||
.group_by(&:sprint_id)
|
||||
@active_sprint_ids = @sprints.select(&:active?).map(&:id)
|
||||
@inbox_work_packages = Backlog.inbox_for(project: @project)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -216,7 +216,7 @@ class RbStoriesController < RbApplicationController
|
||||
|
||||
def load_story
|
||||
@allowed_stories =
|
||||
if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
if @sprint.is_a?(Agile::Sprint)
|
||||
WorkPackage.visible.where(sprint: @sprint, project: @project)
|
||||
else
|
||||
Story.visible.where(Story.condition(@project, @sprint))
|
||||
|
||||
@@ -34,7 +34,7 @@ class RbTaskboardsController < RbApplicationController
|
||||
helper :taskboards
|
||||
|
||||
def show
|
||||
if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
if @sprint.is_a?(Agile::Sprint)
|
||||
@board = @sprint.task_board_for(@project)
|
||||
|
||||
return redirect_to(project_work_package_board_path(@project, @board)) if @board
|
||||
@@ -56,10 +56,7 @@ class RbTaskboardsController < RbApplicationController
|
||||
|
||||
return unless (@sprint_id = params.delete(:sprint_id))
|
||||
|
||||
@sprint = if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
Agile::Sprint.for_project(@project).visible.find(@sprint_id)
|
||||
else
|
||||
Sprint.visible.apply_to(@project).find(@sprint_id)
|
||||
end
|
||||
@sprint = Agile::Sprint.for_project(@project).visible.find_by(id: @sprint_id) ||
|
||||
Sprint.visible.apply_to(@project).find(@sprint_id)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,12 +31,6 @@
|
||||
class My::BacklogsForm < ApplicationForm
|
||||
form do |f|
|
||||
f.fieldset_group(title: helpers.t("backlogs.user_preference.header_backlogs"), mt: 4) do |fg|
|
||||
unless OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
fg.text_field name: :backlogs_task_color,
|
||||
label: helpers.t("backlogs.task_color"),
|
||||
input_width: :xsmall
|
||||
end
|
||||
|
||||
fg.check_box name: :backlogs_versions_default_fold_state,
|
||||
value: DEFAULT_FOLD_STATE,
|
||||
unchecked_value: DEFAULT_EXPAND_STATE,
|
||||
|
||||
@@ -132,13 +132,8 @@ module RbCommonHelper
|
||||
item.remaining_hours.blank? || item.remaining_hours == 0 ? "" : item.remaining_hours
|
||||
end
|
||||
|
||||
def scrum_projects_enabled?
|
||||
OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
end
|
||||
|
||||
def allow_sprint_creation?(project)
|
||||
scrum_projects_enabled? &&
|
||||
current_user.allowed_in_project?(:create_sprints, project) &&
|
||||
current_user.allowed_in_project?(:create_sprints, project) &&
|
||||
!project.receive_shared_sprints?
|
||||
end
|
||||
|
||||
@@ -162,24 +157,11 @@ module RbCommonHelper
|
||||
end
|
||||
|
||||
def backlogs_types
|
||||
return [] if scrum_projects_enabled?
|
||||
|
||||
@backlogs_types ||= begin
|
||||
backlogs_ids = Setting.plugin_openproject_backlogs["story_types"]
|
||||
backlogs_ids << Setting.plugin_openproject_backlogs["task_type"]
|
||||
|
||||
Type.where(id: backlogs_ids).order(Arel.sql("position ASC"))
|
||||
end
|
||||
[]
|
||||
end
|
||||
|
||||
def story_types
|
||||
return [] if scrum_projects_enabled?
|
||||
|
||||
@story_types ||= begin
|
||||
backlogs_type_ids = Setting.plugin_openproject_backlogs["story_types"].map(&:to_i)
|
||||
|
||||
backlogs_types.select { |t| backlogs_type_ids.include?(t.id) }
|
||||
end
|
||||
[]
|
||||
end
|
||||
|
||||
def get_backlogs_preference(assignee, attr)
|
||||
@@ -187,6 +169,6 @@ module RbCommonHelper
|
||||
end
|
||||
|
||||
def sprint_board_label
|
||||
OpenProject::FeatureDecisions.scrum_projects_active? ? t("backlogs.label_sprint_board") : t(:label_task_board)
|
||||
t("backlogs.label_sprint_board")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -35,7 +35,7 @@ module Queries::WorkPackages::Filter
|
||||
end
|
||||
|
||||
def available?
|
||||
scrum_projects_active? && allowed?
|
||||
allowed?
|
||||
end
|
||||
|
||||
def type
|
||||
@@ -71,10 +71,6 @@ module Queries::WorkPackages::Filter
|
||||
end
|
||||
end
|
||||
|
||||
def scrum_projects_active?
|
||||
OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
end
|
||||
|
||||
def sprints
|
||||
@sprints ||= begin
|
||||
scope = Agile::Sprint.visible
|
||||
|
||||
@@ -47,7 +47,7 @@ class WorkPackages::RebuildPositionsService
|
||||
id,
|
||||
ROW_NUMBER() OVER (
|
||||
PARTITION BY project_id, sprint_id
|
||||
ORDER BY position, created_at
|
||||
ORDER BY position, created_at, id
|
||||
) AS new_position
|
||||
FROM work_packages
|
||||
) AS mapping
|
||||
|
||||
@@ -40,22 +40,8 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
%>
|
||||
|
||||
<%=
|
||||
if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
render(Primer::Beta::Blankslate.new(border: true, spacious: true)) do |blankslate|
|
||||
blankslate.with_heading(tag: :h2).with_content(t("backlogs.administration_blankslate.title"))
|
||||
blankslate.with_description_content(t("backlogs.administration_blankslate.text"))
|
||||
end
|
||||
else
|
||||
settings_primer_form_with(
|
||||
url: admin_backlogs_settings_path,
|
||||
method: :put,
|
||||
model: @settings,
|
||||
scope: :settings,
|
||||
data: {
|
||||
controller: "admin--backlogs-settings"
|
||||
}
|
||||
) do |f|
|
||||
render Admin::Settings::BacklogsSettingsForm.new(f)
|
||||
end
|
||||
render(Primer::Beta::Blankslate.new(border: true, spacious: true)) do |blankslate|
|
||||
blankslate.with_heading(tag: :h2).with_content(t("backlogs.administration_blankslate.title"))
|
||||
blankslate.with_description_content(t("backlogs.administration_blankslate.text"))
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -98,9 +98,7 @@ en:
|
||||
task_type: "Task type"
|
||||
|
||||
backlogs:
|
||||
any: "any"
|
||||
caption_sprints_default_fold_state: "Sprints will not be expanded by default when viewing the 'Backlog and sprints' page. Each one has to be manually expanded."
|
||||
column_width: "Column width"
|
||||
definition_of_done: "Definition of Done"
|
||||
definition_of_done_caption: "Work packages with these statuses are treated as completed in backlog views and reporting."
|
||||
done_status: "Done status"
|
||||
@@ -121,11 +119,9 @@ en:
|
||||
rebuild_positions: "Rebuild positions"
|
||||
remaining_hours: "Remaining work"
|
||||
show_burndown_chart: "Burndown chart"
|
||||
story: "Story"
|
||||
story_points:
|
||||
one: "%{count} story point"
|
||||
other: "%{count} story points"
|
||||
task: "Task"
|
||||
task_color: "Task color"
|
||||
unassigned: "Unassigned"
|
||||
|
||||
@@ -234,19 +230,13 @@ en:
|
||||
move_menu: "Move"
|
||||
|
||||
backlogs_points_burn_direction: "Points burn up/down"
|
||||
backlogs_product_backlog: "Product backlog"
|
||||
backlogs_story: "Story"
|
||||
backlogs_story_type: "Story types"
|
||||
backlogs_task: "Task"
|
||||
backlogs_task_type: "Task type"
|
||||
backlogs_wiki_template: "Template for sprint wiki page"
|
||||
backlogs_empty_title: "No versions are defined yet"
|
||||
backlogs_empty_action_text: "To start using backlogs, please create a version first"
|
||||
|
||||
backlogs_not_configured_title: "Backlogs not configured"
|
||||
backlogs_not_configured_description: "Story and task types need to be set before using this module."
|
||||
backlogs_not_configured_action_text: "Configure Backlogs"
|
||||
|
||||
burndown:
|
||||
story_points: "Story points"
|
||||
story_points_ideal: "Story points (ideal)"
|
||||
@@ -260,10 +250,8 @@ en:
|
||||
label_inbox: "Inbox"
|
||||
label_backlogs: "Backlogs"
|
||||
label_backlogs_unconfigured: "You have not configured Backlogs yet. Please go to %{administration} > %{plugins}, then click on the %{configure} link for this plugin. Once you have set the fields, come back to this page to start using the tool."
|
||||
label_blocks_ids: "IDs of blocked work packages"
|
||||
label_burndown_chart: "Burndown chart"
|
||||
label_column_in_backlog: "Column in backlog"
|
||||
label_used_as_backlog: "Used as backlog"
|
||||
label_sprint_board: "Sprint board"
|
||||
label_points_burn_down: "Down"
|
||||
label_points_burn_up: "Up"
|
||||
|
||||
@@ -29,48 +29,46 @@
|
||||
#++
|
||||
|
||||
Rails.application.routes.draw do
|
||||
constraints(Constraints::FeatureDecision.new(:scrum_projects)) do
|
||||
# Routes for the new Agile::Sprint
|
||||
# Scoped under projects for permissions:
|
||||
resources :projects, only: [] do
|
||||
resources :sprints, controller: :rb_sprints, only: %i[create] do
|
||||
collection do
|
||||
get :new_dialog
|
||||
get :refresh_form
|
||||
end
|
||||
|
||||
member do
|
||||
post :start
|
||||
post :finish
|
||||
get :edit_dialog
|
||||
put :update_agile_sprint
|
||||
end
|
||||
|
||||
resources :stories, controller: :rb_stories, only: [] do
|
||||
member do
|
||||
get :menu
|
||||
put :move
|
||||
end
|
||||
end
|
||||
# Routes for the new Agile::Sprint
|
||||
# Scoped under projects for permissions:
|
||||
resources :projects, only: [] do
|
||||
resources :sprints, controller: :rb_sprints, only: %i[create] do
|
||||
collection do
|
||||
get :new_dialog
|
||||
get :refresh_form
|
||||
end
|
||||
|
||||
resources :inbox, only: [] do
|
||||
member do
|
||||
post :start
|
||||
post :finish
|
||||
get :edit_dialog
|
||||
put :update_agile_sprint
|
||||
end
|
||||
|
||||
resources :stories, controller: :rb_stories, only: [] do
|
||||
member do
|
||||
get :menu
|
||||
put :move
|
||||
post :reorder
|
||||
get :move_to_sprint_dialog
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scope "projects/:project_id", as: "project", module: "projects" do
|
||||
namespace "settings" do
|
||||
resource :backlog_sharing, only: %i[show update]
|
||||
resources :inbox, only: [] do
|
||||
member do
|
||||
get :menu
|
||||
put :move
|
||||
post :reorder
|
||||
get :move_to_sprint_dialog
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scope "projects/:project_id", as: "project", module: "projects" do
|
||||
namespace "settings" do
|
||||
resource :backlog_sharing, only: %i[show update]
|
||||
end
|
||||
end
|
||||
|
||||
# Legacy routes
|
||||
scope "", as: "backlogs" do
|
||||
scope "projects/:project_id", as: "project" do
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
#-- 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.
|
||||
#++
|
||||
|
||||
module API
|
||||
module V3
|
||||
module BacklogsTypes
|
||||
class BacklogsTypeRepresenter < ::API::Decorators::Single
|
||||
self_link path: :backlogs_type,
|
||||
id_attribute: ->(*) { represented.last },
|
||||
title_getter: ->(*) { represented.first }
|
||||
|
||||
property :id,
|
||||
exec_context: :decorator
|
||||
|
||||
property :name,
|
||||
exec_context: :decorator
|
||||
|
||||
def _type
|
||||
"BacklogsType"
|
||||
end
|
||||
|
||||
def id
|
||||
represented.last
|
||||
end
|
||||
|
||||
def name
|
||||
represented.first
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,65 +0,0 @@
|
||||
#-- 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.
|
||||
#++
|
||||
|
||||
module API
|
||||
module V3
|
||||
module Queries
|
||||
module Schemas
|
||||
class BacklogsTypeDependencyRepresenter <
|
||||
FilterDependencyRepresenter
|
||||
schema_with_allowed_collection :values,
|
||||
type: ->(*) { type },
|
||||
writable: true,
|
||||
has_default: false,
|
||||
required: true,
|
||||
values_callback: ->(*) {
|
||||
represented.allowed_values
|
||||
},
|
||||
value_representer: BacklogsTypes::BacklogsTypeRepresenter,
|
||||
link_factory: ->(value) {
|
||||
{
|
||||
href: api_v3_paths.backlogs_type(value.last),
|
||||
title: value.first
|
||||
}
|
||||
},
|
||||
show_if: ->(*) {
|
||||
value_required?
|
||||
}
|
||||
|
||||
def href_callback; end
|
||||
|
||||
private
|
||||
|
||||
def type
|
||||
"[1]BacklogsType"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -33,10 +33,6 @@ module API
|
||||
module Sprints
|
||||
class SprintsAPI < ::API::OpenProjectAPI
|
||||
resources :sprints do
|
||||
before do
|
||||
guard_feature_flag(:scrum_projects)
|
||||
end
|
||||
|
||||
get &::API::V3::Utilities::Endpoints::Index
|
||||
.new(model: Agile::Sprint)
|
||||
.mount
|
||||
|
||||
@@ -34,8 +34,6 @@ module API
|
||||
class SprintsByProjectAPI < ::API::OpenProjectAPI
|
||||
resources :sprints do
|
||||
after_validation do
|
||||
guard_feature_flag(:scrum_projects)
|
||||
|
||||
authorize_in_project(:view_sprints, project: @project)
|
||||
end
|
||||
|
||||
|
||||
@@ -99,8 +99,7 @@ module OpenProject::Backlogs
|
||||
{ rb_sprints: %i[start finish] },
|
||||
permissible_on: :project,
|
||||
require: :member,
|
||||
dependencies: %i[view_sprints manage_board_views manage_sprint_items],
|
||||
visible: -> { OpenProject::FeatureDecisions.scrum_projects_active? }
|
||||
dependencies: %i[view_sprints manage_board_views manage_sprint_items]
|
||||
|
||||
permission :manage_sprint_items,
|
||||
{ rb_stories: %i[move move_legacy reorder],
|
||||
@@ -113,15 +112,13 @@ module OpenProject::Backlogs
|
||||
{ "projects/settings/backlog_sharings": %i[show update] },
|
||||
permissible_on: :project,
|
||||
require: :member,
|
||||
dependencies: :create_sprints,
|
||||
visible: -> { OpenProject::FeatureDecisions.scrum_projects_active? }
|
||||
dependencies: :create_sprints
|
||||
end
|
||||
|
||||
# Menu items that are there when feature flag is active
|
||||
menu :project_menu,
|
||||
:backlogs,
|
||||
{ controller: "/rb_master_backlogs", action: :backlog },
|
||||
if: Proc.new { |project| project.module_enabled?(:backlogs) && OpenProject::FeatureDecisions.scrum_projects_active? },
|
||||
if: Proc.new { |project| project.module_enabled?(:backlogs) },
|
||||
caption: :project_module_backlogs,
|
||||
after: :work_packages,
|
||||
icon: "op-backlogs"
|
||||
@@ -129,19 +126,10 @@ module OpenProject::Backlogs
|
||||
menu :project_menu,
|
||||
:backlog,
|
||||
{ controller: "/rb_master_backlogs", action: :backlog },
|
||||
if: Proc.new { |project| project.module_enabled?(:backlogs) && OpenProject::FeatureDecisions.scrum_projects_active? },
|
||||
if: Proc.new { |project| project.module_enabled?(:backlogs) },
|
||||
caption: :label_backlog_and_sprints,
|
||||
parent: :backlogs
|
||||
|
||||
# Menu items that are there when feature flag is inactive
|
||||
menu :project_menu,
|
||||
:backlogs_legacy,
|
||||
{ controller: "/rb_master_backlogs", action: :index },
|
||||
if: Proc.new { |project| project.module_enabled?(:backlogs) && !OpenProject::FeatureDecisions.scrum_projects_active? },
|
||||
caption: :project_module_backlogs,
|
||||
after: :work_packages,
|
||||
icon: "op-backlogs"
|
||||
|
||||
# Menu items that are always present
|
||||
menu :project_menu,
|
||||
:settings_backlogs,
|
||||
@@ -158,12 +146,10 @@ module OpenProject::Backlogs
|
||||
Type
|
||||
Project
|
||||
User
|
||||
VersionsController
|
||||
Version]
|
||||
|
||||
patch_with_namespace :BasicData, :SettingSeeder
|
||||
patch_with_namespace :DemoData, :ProjectSeeder
|
||||
patch_with_namespace :WorkPackages, :UpdateService
|
||||
patch_with_namespace :WorkPackages, :SetAttributesService
|
||||
patch_with_namespace :WorkPackages, :BaseContract
|
||||
patch_with_namespace :WorkPackages, :UpdateContract
|
||||
@@ -203,11 +189,6 @@ module OpenProject::Backlogs
|
||||
extend_api_response(:v3, :work_packages, :schema, :work_package_schema,
|
||||
&::OpenProject::Backlogs::Patches::API::WorkPackageSchemaRepresenter.extension)
|
||||
|
||||
add_api_path :backlogs_type do |id|
|
||||
# There is no api endpoint for this url
|
||||
"#{root}/backlogs_types/#{id}"
|
||||
end
|
||||
|
||||
add_api_path :sprint do |id|
|
||||
"#{root}/sprints/#{id}"
|
||||
end
|
||||
@@ -250,20 +231,16 @@ module OpenProject::Backlogs
|
||||
config.to_prepare do
|
||||
enabled_backlogs_story = ->(type, project: nil) do
|
||||
if project.present?
|
||||
project.backlogs_enabled? && (OpenProject::FeatureDecisions.scrum_projects_active? || type.story?)
|
||||
project.backlogs_enabled?
|
||||
else
|
||||
# Allow globally configuring the attribute if story
|
||||
OpenProject::FeatureDecisions.scrum_projects_active? || type.story?
|
||||
true
|
||||
end
|
||||
end
|
||||
|
||||
story_and_sprint_permission = ->(_type, project: nil) do
|
||||
return false unless OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
project.nil? || User.current.allowed_in_project?(:view_sprints, project)
|
||||
end
|
||||
|
||||
# TODO: upon removal of the scrum_projects feature flag, remove these constraints
|
||||
::Type.add_constraint :position, enabled_backlogs_story
|
||||
::Type.add_constraint :story_points, enabled_backlogs_story
|
||||
::Type.add_constraint :sprint, story_and_sprint_permission
|
||||
@@ -274,7 +251,6 @@ module OpenProject::Backlogs
|
||||
|
||||
::Queries::Register.register(::Query) do
|
||||
filter Queries::WorkPackages::Filter::SprintFilter
|
||||
filter OpenProject::Backlogs::WorkPackageFilter
|
||||
|
||||
select OpenProject::Backlogs::QueryBacklogsSelect
|
||||
end
|
||||
|
||||
@@ -32,64 +32,37 @@ module OpenProject::Backlogs::List
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# Once the scrum_projects_active feature flag is removed,
|
||||
# add
|
||||
# scope [:project_id, :sprint_id]
|
||||
acts_as_list touch_on_update: false
|
||||
|
||||
# acts as list adds a before destroy hook which messes
|
||||
# with the parent_id_was value
|
||||
skip_callback(:destroy, :before, :reload)
|
||||
|
||||
# Reorder list, if work_package is removed from sprint
|
||||
# To be removed once the scrum_projects_active feature flag is removed
|
||||
before_update :fix_other_work_package_positions
|
||||
# To be removed once the scrum_projects_active feature flag is removed
|
||||
before_update :fix_own_work_package_position
|
||||
|
||||
private
|
||||
|
||||
# Used by acts_list to limit the list to a certain subset within
|
||||
# the table.
|
||||
#
|
||||
# Also sanitize_sql seems to be unavailable in a sensible way. Therefore
|
||||
# we're using send to circumvent visibility work_packages.
|
||||
def scope_condition
|
||||
if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
{ project_id:, sprint_id: }
|
||||
else
|
||||
self.class.send(:sanitize_sql, ["project_id = ? AND version_id = ? AND type_id IN (?)",
|
||||
project_id, version_id, types])
|
||||
end
|
||||
{ project_id:, sprint_id: }
|
||||
end
|
||||
|
||||
# rubocop:disable Style/ArrayIntersect
|
||||
# rubocop:disable Performance/InefficientHashSearch
|
||||
# Copied from acts_as_list.
|
||||
# To be removed once the scrum_projects_active feature flag is removed
|
||||
# acts_as_list needs to know when a work package moved between backlog/sprint scopes
|
||||
# so it can reorder both the source and target lists correctly.
|
||||
def scope_changed?
|
||||
return false unless OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
(scope_condition.keys & changed.map(&:to_sym)).any?
|
||||
end
|
||||
|
||||
# Copied from acts_as_list
|
||||
# To be removed once the scrum_projects_active feature flag is removed
|
||||
# Copied from acts_as_list to support our custom hash-based scope condition.
|
||||
def destroyed_via_scope?
|
||||
return false unless OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
return false unless destroyed_by_association
|
||||
|
||||
foreign_key = destroyed_by_association.foreign_key
|
||||
if foreign_key.is_a?(Array)
|
||||
# Composite foreign key - check if any keys overlap with scope
|
||||
(scope_condition.keys & foreign_key.map(&:to_sym)).any?
|
||||
else
|
||||
# Single foreign key
|
||||
scope_condition.keys.include?(foreign_key.to_sym)
|
||||
end
|
||||
end
|
||||
# rubocop:enable Style/ArrayIntersect
|
||||
# rubocop:enable Performance/InefficientHashSearch
|
||||
|
||||
include InstanceMethods
|
||||
end
|
||||
@@ -129,78 +102,10 @@ module OpenProject::Backlogs::List
|
||||
update_columns(position: new_position)
|
||||
end
|
||||
|
||||
def fix_other_work_package_positions # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
||||
return if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
if changes.slice("project_id", "type_id", "version_id").present?
|
||||
if changes.slice("project_id", "version_id").blank? and
|
||||
Story.types.include?(type_id.to_i) and
|
||||
Story.types.include?(type_id_was.to_i)
|
||||
return
|
||||
end
|
||||
|
||||
if version_id_changed?
|
||||
restore_version_id = true
|
||||
new_version_id = version_id
|
||||
self.version_id = version_id_was
|
||||
end
|
||||
|
||||
if type_id_changed?
|
||||
restore_type_id = true
|
||||
new_type_id = type_id
|
||||
self.type_id = type_id_was
|
||||
end
|
||||
|
||||
if project_id_changed?
|
||||
restore_project_id = true
|
||||
# I've got no idea, why there's a difference between setting the
|
||||
# project via project= or via project_id=, but there is.
|
||||
new_project = project
|
||||
self.project = Project.find(project_id_was)
|
||||
end
|
||||
|
||||
remove_from_list if is_story?
|
||||
|
||||
if restore_project_id
|
||||
self.project = new_project
|
||||
end
|
||||
|
||||
if restore_type_id
|
||||
self.type_id = new_type_id
|
||||
end
|
||||
|
||||
if restore_version_id
|
||||
self.version_id = new_version_id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fix_own_work_package_position # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity
|
||||
return if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
if changes.slice("project_id", "type_id", "version_id").present?
|
||||
if changes.slice("project_id", "version_id").blank? and
|
||||
Story.types.include?(type_id.to_i) and
|
||||
Story.types.include?(type_id_was.to_i)
|
||||
return
|
||||
end
|
||||
|
||||
if is_story? and version.present?
|
||||
assume_bottom_position
|
||||
else
|
||||
remove_from_list
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def set_default_prev_positions_silently(prev)
|
||||
return if prev.nil?
|
||||
|
||||
if prev.is_task?
|
||||
prev.version.rebuild_task_positions(prev)
|
||||
else
|
||||
prev.version.rebuild_story_positions(prev.project)
|
||||
end
|
||||
WorkPackages::RebuildPositionsService.new(project: prev.project).call
|
||||
|
||||
prev.reload.position
|
||||
end
|
||||
|
||||
@@ -49,8 +49,7 @@ module OpenProject::Backlogs
|
||||
resource :sprint,
|
||||
link_cache_if: ->(*) {
|
||||
represented.project.present? &&
|
||||
current_user.allowed_in_project?(:view_sprints, represented.project) &&
|
||||
OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
current_user.allowed_in_project?(:view_sprints, represented.project)
|
||||
},
|
||||
link: ->(*) {
|
||||
if represented.sprint.present?
|
||||
@@ -68,8 +67,7 @@ module OpenProject::Backlogs
|
||||
if embed_links &&
|
||||
represented.project.present? &&
|
||||
represented.sprint.present? &&
|
||||
current_user.allowed_in_project?(:view_sprints, represented.project) &&
|
||||
OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
current_user.allowed_in_project?(:view_sprints, represented.project)
|
||||
::API::V3::Sprints::SprintRepresenter.create(represented.sprint, current_user:)
|
||||
end
|
||||
end,
|
||||
|
||||
+1
-2
@@ -54,8 +54,7 @@ module OpenProject::Backlogs
|
||||
required: false,
|
||||
show_if: ->(*) {
|
||||
current_user.allowed_in_project?(:view_sprints, represented.project) &&
|
||||
backlogs_constraint_passed?(:sprint) &&
|
||||
OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
backlogs_constraint_passed?(:sprint)
|
||||
},
|
||||
href_callback: ->(*) {
|
||||
api_v3_paths.project_sprints(represented.project_id)
|
||||
|
||||
@@ -37,64 +37,20 @@ module OpenProject::Backlogs::Patches::SetAttributesServicePatch
|
||||
def set_attributes(attributes)
|
||||
super
|
||||
|
||||
if OpenProject::FeatureDecisions.scrum_projects_active? && moved_to_project_that_has_no_access_to_sprint?
|
||||
if moved_to_project_that_has_no_access_to_sprint?
|
||||
model.change_by_system do
|
||||
model.sprint = nil
|
||||
end
|
||||
elsif should_inherit_version_from_parent?
|
||||
closest = closest_story_or_impediment(work_package.parent_id)
|
||||
work_package.version_id = closest.version_id if closest
|
||||
end
|
||||
end
|
||||
|
||||
def moved_to_project_that_has_no_access_to_sprint?
|
||||
work_package.project_id &&
|
||||
!work_package.new_record? &&
|
||||
work_package.project_id &&
|
||||
work_package.project_id_changed? &&
|
||||
work_package.sprint_id &&
|
||||
!work_package.sprint.visible_to?(work_package.project)
|
||||
end
|
||||
|
||||
def should_inherit_version_from_parent?
|
||||
work_package.parent_id_changed? &&
|
||||
work_package.parent_id &&
|
||||
!work_package.version_id_changed? &&
|
||||
work_package.in_backlogs_type?
|
||||
end
|
||||
|
||||
def closest_story_or_impediment(parent_id)
|
||||
return work_package if work_package.is_story? || work_package.is_impediment?
|
||||
|
||||
closest = nil
|
||||
ancestor_chain(parent_id).each do |i|
|
||||
# break if we found an element in our chain that is not relevant in backlogs
|
||||
break unless i.in_backlogs_type?
|
||||
|
||||
if i.is_story? || i.is_impediment?
|
||||
closest = i
|
||||
break
|
||||
end
|
||||
end
|
||||
closest
|
||||
end
|
||||
|
||||
# ancestors array similar to Module#ancestors
|
||||
# i.e. returns immediate ancestors first
|
||||
def ancestor_chain(parent_id)
|
||||
ancestors = []
|
||||
unless parent_id.nil?
|
||||
real_parent = WorkPackage.visible(user).find_by(id: parent_id)
|
||||
|
||||
# Sort immediate ancestors first
|
||||
ancestors = real_parent
|
||||
.ancestors
|
||||
.visible(user)
|
||||
.includes(project: :enabled_modules)
|
||||
.order_by_ancestors("desc")
|
||||
.select("work_packages.*, COALESCE(max_depth.depth, 0)")
|
||||
|
||||
ancestors = [real_parent] + ancestors
|
||||
end
|
||||
ancestors
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -39,15 +39,11 @@ module OpenProject::Backlogs::Patches::TypePatch
|
||||
|
||||
module InstanceMethods
|
||||
def story?
|
||||
return false if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
Story.types.include?(id)
|
||||
false
|
||||
end
|
||||
|
||||
def task?
|
||||
return false if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
Task.type.present? && id == Task.type
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
#-- 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.
|
||||
#++
|
||||
|
||||
module OpenProject::Backlogs::Patches::UpdateServicePatch
|
||||
def self.included(base)
|
||||
base.prepend InstanceMethods
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
def update_descendants(work_package)
|
||||
super_result = super
|
||||
|
||||
if work_package.in_backlogs_type? && work_package.saved_change_to_version_id?
|
||||
super_result += inherit_version_to_descendants(work_package)
|
||||
end
|
||||
|
||||
super_result
|
||||
end
|
||||
|
||||
def inherit_version_to_descendants(work_package)
|
||||
all_descendants = sorted_descendants(work_package)
|
||||
descendant_tasks = descendant_tasks_of(all_descendants)
|
||||
|
||||
attributes = { version_id: work_package.version_id }
|
||||
|
||||
descendant_tasks.map do |task|
|
||||
# Ensure the parent is already moved to new version so that validation errors are avoided.
|
||||
task.parent = ([work_package] + all_descendants).detect { |d| d.id == task.parent_id }
|
||||
set_descendant_attributes(attributes, task)
|
||||
end
|
||||
end
|
||||
|
||||
def sorted_descendants(work_package)
|
||||
work_package
|
||||
.descendants
|
||||
.includes(project: :enabled_modules)
|
||||
.order_by_ancestors("asc")
|
||||
.select("work_packages.*")
|
||||
end
|
||||
|
||||
def descendant_tasks_of(descendants)
|
||||
stop_descendants_ids = []
|
||||
|
||||
descendants.reject do |t|
|
||||
if stop_descendants_ids.include?(t.parent_id) || !t.is_task?
|
||||
stop_descendants_ids << t.id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,3 +1,5 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
@@ -34,7 +36,6 @@ module OpenProject::Backlogs::Patches::Versions::RowComponentPatch
|
||||
private
|
||||
|
||||
def backlogs_edit_link
|
||||
return if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
return if version.project == table.project || !table.project.module_enabled?("backlogs")
|
||||
|
||||
helpers.link_to_if_authorized "",
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#-- 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.
|
||||
#++
|
||||
|
||||
module OpenProject::Backlogs::Patches::VersionsControllerPatch
|
||||
def self.included(base) # rubocop:disable Metrics/AbcSize
|
||||
base.class_eval do
|
||||
before_action :override_project_from_id, only: %i[edit update]
|
||||
|
||||
append_before_action :add_project_to_version_settings_attributes, only: %i[update create]
|
||||
append_before_action :whitelist_update_params, only: :update
|
||||
|
||||
private
|
||||
|
||||
def override_project_from_id
|
||||
# @project is already set by the VersionsController's find_version before action to the version's project
|
||||
# here we want to add that we always set it to the project from params if present
|
||||
if params[:project_id].present?
|
||||
@project = Project.visible.find(params[:project_id])
|
||||
end
|
||||
end
|
||||
|
||||
def whitelist_update_params
|
||||
if @project != @version.project
|
||||
# Make sure only the version_settings_attributes
|
||||
# (column=left|right|none) can be stored when current project does not
|
||||
# equal the version project (which is valid in inherited versions)
|
||||
if permitted_params.version.present? && permitted_params.version[:version_settings_attributes].present?
|
||||
params["version"] = { version_settings_attributes: permitted_params.version[:version_settings_attributes] }
|
||||
else
|
||||
# This is an unfortunate hack giving how plugins work at the moment.
|
||||
# In this else branch we want the `version` to be an empty hash.
|
||||
permitted_params.define_singleton_method :version, lambda { {} }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# This forces the current project for the nested version settings in order
|
||||
# to prevent it from being set through firebug etc. #mass_assignment
|
||||
def add_project_to_version_settings_attributes
|
||||
if permitted_params.version["version_settings_attributes"].present?
|
||||
params["version"]["version_settings_attributes"].each do |attr_hash|
|
||||
attr_hash["project_id"] = @project.id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
VersionsController.include OpenProject::Backlogs::Patches::VersionsControllerPatch
|
||||
@@ -52,20 +52,6 @@ module OpenProject::Backlogs::Patches::WorkPackagePatch
|
||||
def order_by_position
|
||||
order(arel_table[:position].asc.nulls_last)
|
||||
end
|
||||
|
||||
def backlogs_types
|
||||
return [] if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
# Unfortunately, this is not cachable so the following line would be wrong
|
||||
# @backlogs_types ||= Story.types << Task.type
|
||||
# Caching like in the line above would prevent the types selected
|
||||
# for backlogs to be changed without restarting all app server.
|
||||
(Story.types << Task.type).compact
|
||||
end
|
||||
|
||||
def children_of(ids)
|
||||
where(parent_id: ids)
|
||||
end
|
||||
end
|
||||
|
||||
module InstanceMethods
|
||||
@@ -73,46 +59,10 @@ module OpenProject::Backlogs::Patches::WorkPackagePatch
|
||||
project.done_statuses.to_a.include?(status)
|
||||
end
|
||||
|
||||
def to_story
|
||||
Story.find(id) if is_story?
|
||||
end
|
||||
|
||||
def is_story?
|
||||
return false if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
backlogs_enabled? && Story.types.include?(type_id)
|
||||
end
|
||||
|
||||
def to_task
|
||||
Task.find(id) if is_task?
|
||||
end
|
||||
|
||||
def is_task?
|
||||
return false if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
backlogs_enabled? && parent_id && type_id == Task.type && Task.type.present?
|
||||
end
|
||||
|
||||
def is_impediment?
|
||||
return false if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
backlogs_enabled? && parent_id.nil? && type_id == Task.type && Task.type.present?
|
||||
end
|
||||
|
||||
def types
|
||||
if is_story?
|
||||
Story.types
|
||||
elsif is_task?
|
||||
Task.types
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def story
|
||||
if is_story?
|
||||
if Story.types.include?(type_id)
|
||||
Story.find(id)
|
||||
elsif is_task?
|
||||
elsif Task.type.present? && type_id == Task.type
|
||||
ancestors.where(type_id: Story.types).first
|
||||
end
|
||||
end
|
||||
@@ -127,12 +77,6 @@ module OpenProject::Backlogs::Patches::WorkPackagePatch
|
||||
def backlogs_enabled?
|
||||
project&.backlogs_enabled?
|
||||
end
|
||||
|
||||
def in_backlogs_type?
|
||||
return false if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
backlogs_enabled? && WorkPackage.backlogs_types.include?(type.try(:id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
# 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.
|
||||
#++
|
||||
|
||||
require "story"
|
||||
require "task"
|
||||
|
||||
module OpenProject::Backlogs
|
||||
class WorkPackageFilter < ::Queries::WorkPackages::Filter::WorkPackageFilter
|
||||
def allowed_values
|
||||
[[I18n.t("backlogs.story"), "story"],
|
||||
[I18n.t("backlogs.task"), "task"],
|
||||
[I18n.t("backlogs.impediment"), "impediment"],
|
||||
[I18n.t("backlogs.any"), "any"]]
|
||||
end
|
||||
|
||||
def available?
|
||||
backlogs_enabled? &&
|
||||
backlogs_configured?
|
||||
end
|
||||
|
||||
def self.key
|
||||
:backlogs_work_package_type
|
||||
end
|
||||
|
||||
def where
|
||||
sql_for_field(values)
|
||||
end
|
||||
|
||||
def type
|
||||
:list
|
||||
end
|
||||
|
||||
def human_name
|
||||
WorkPackage.human_attribute_name(:backlogs_work_package_type)
|
||||
end
|
||||
|
||||
def dependency_class
|
||||
"::API::V3::Queries::Schemas::BacklogsTypeDependencyRepresenter"
|
||||
end
|
||||
|
||||
def ar_object_filter?
|
||||
true
|
||||
end
|
||||
|
||||
def value_objects
|
||||
available_backlog_types = allowed_values.index_by(&:last)
|
||||
|
||||
values
|
||||
.filter_map { |backlog_type_id| available_backlog_types[backlog_type_id] }
|
||||
.map { |value| BacklogsType.new(*value) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def backlogs_configured?
|
||||
return false if OpenProject::FeatureDecisions.scrum_projects_active?
|
||||
|
||||
Story.types.present? && Task.type.present?
|
||||
end
|
||||
|
||||
def backlogs_enabled?
|
||||
project.nil? || project.module_enabled?(:backlogs)
|
||||
end
|
||||
|
||||
def sql_for_field(values)
|
||||
selected_values = if values.include?("any")
|
||||
["story", "task"]
|
||||
else
|
||||
values
|
||||
end
|
||||
|
||||
sql_parts = selected_values.map do |val|
|
||||
case val
|
||||
when "story"
|
||||
sql_for_story
|
||||
when "task"
|
||||
sql_for_task
|
||||
when "impediment"
|
||||
sql_for_impediment
|
||||
end
|
||||
end
|
||||
|
||||
case operator
|
||||
when "="
|
||||
sql_parts.join(" OR ")
|
||||
when "!"
|
||||
"NOT (" + sql_parts.join(" OR ") + ")"
|
||||
end
|
||||
end
|
||||
|
||||
def db_table
|
||||
WorkPackage.table_name
|
||||
end
|
||||
|
||||
def sql_for_story
|
||||
story_types = Story.types.map(&:to_s).join(",")
|
||||
|
||||
"(#{db_table}.type_id IN (#{story_types}))"
|
||||
end
|
||||
|
||||
def sql_for_task
|
||||
<<-SQL.squish
|
||||
(#{db_table}.type_id = #{Task.type} AND
|
||||
#{db_table}.parent_id IS NOT NULL)
|
||||
SQL
|
||||
end
|
||||
|
||||
def sql_for_impediment
|
||||
<<-SQL.squish
|
||||
(#{db_table}.type_id = #{Task.type} AND
|
||||
#{db_table}.id IN (#{blocks_backlogs_type_sql})
|
||||
AND #{db_table}.parent_id IS NULL)
|
||||
SQL
|
||||
end
|
||||
|
||||
def blocks_backlogs_type_sql
|
||||
all_types = (Story.types + [Task.type]).map(&:to_s)
|
||||
|
||||
Relation
|
||||
.blocks
|
||||
.joins(:to)
|
||||
.where(work_packages: { type_id: all_types })
|
||||
.select(:to_id)
|
||||
.to_sql
|
||||
end
|
||||
end
|
||||
|
||||
# Need to be conformant to the interface required
|
||||
# by api/v3/queries/filters/query_filter_instance_representer.rb
|
||||
class BacklogsType
|
||||
attr_accessor :id,
|
||||
:name
|
||||
|
||||
def initialize(name, id)
|
||||
self.id = id
|
||||
self.name = name
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe InboxController, with_flag: { scrum_projects_active: true } do
|
||||
RSpec.describe InboxController do
|
||||
current_user { user }
|
||||
|
||||
let(:user) { create(:admin) }
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Projects::Settings::BacklogSharingsController, with_flag: { scrum_projects: true } do
|
||||
RSpec.describe Projects::Settings::BacklogSharingsController do
|
||||
shared_let(:user) { create(:admin) }
|
||||
|
||||
current_user { user }
|
||||
|
||||
@@ -174,25 +174,23 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
|
||||
describe "GET #new_dialog" do
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
it "responds with success", :aggregate_failures do
|
||||
it "responds with success", :aggregate_failures do
|
||||
get :new_dialog, params: { project_id: project.id }, format: :turbo_stream
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response).to have_turbo_stream action: "dialog", target: "backlogs-new-sprint-dialog-component"
|
||||
expect(assigns(:project)).to eq(project)
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
get :new_dialog, params: { project_id: project.id }, format: :turbo_stream
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response).to have_turbo_stream action: "dialog", target: "backlogs-new-sprint-dialog-component"
|
||||
expect(assigns(:project)).to eq(project)
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
get :new_dialog, params: { project_id: project.id }, format: :turbo_stream
|
||||
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -200,60 +198,56 @@ RSpec.describe RbSprintsController do
|
||||
describe "GET #edit_dialog" do
|
||||
let!(:sprint) { create(:agile_sprint, project:) }
|
||||
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
it "responds with success", :aggregate_failures do
|
||||
it "responds with success", :aggregate_failures do
|
||||
get :edit_dialog, params: { project_id: project.id, id: sprint.id }, format: :turbo_stream
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response).to have_turbo_stream action: "dialog", target: "backlogs-new-sprint-dialog-component"
|
||||
expect(assigns(:project)).to eq(project)
|
||||
expect(assigns(:sprint)).to eq(sprint)
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
get :edit_dialog, params: { project_id: project.id, id: sprint.id }, format: :turbo_stream
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response).to have_turbo_stream action: "dialog", target: "backlogs-new-sprint-dialog-component"
|
||||
expect(assigns(:project)).to eq(project)
|
||||
expect(assigns(:sprint)).to eq(sprint)
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
get :edit_dialog, params: { project_id: project.id, id: sprint.id }, format: :turbo_stream
|
||||
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST #create" do
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
let(:params) do
|
||||
{
|
||||
project_id: project.id,
|
||||
sprint: { name: "My Sprint", start_date: "2025-10-05", finish_date: "2025-10-15" }
|
||||
}
|
||||
end
|
||||
let(:params) do
|
||||
{
|
||||
project_id: project.id,
|
||||
sprint: { name: "My Sprint", start_date: "2025-10-05", finish_date: "2025-10-15" }
|
||||
}
|
||||
end
|
||||
|
||||
it "responds with success, creates a sprint, and redirects to backlogs", :aggregate_failures do
|
||||
it "responds with success, creates a sprint, and redirects to backlogs", :aggregate_failures do
|
||||
post :create, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response.body).to include("turbo-stream")
|
||||
expect(response.body).to include("action=\"redirect_to\"")
|
||||
expect(response.body).to include(backlogs_project_backlogs_path(project))
|
||||
expect(project.reload.sprints.last.name).to eq("My Sprint")
|
||||
expect(flash[:notice]).to eq(I18n.t(:notice_successful_create))
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
post :create, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response.body).to include("turbo-stream")
|
||||
expect(response.body).to include("action=\"redirect_to\"")
|
||||
expect(response.body).to include(backlogs_project_backlogs_path(project))
|
||||
expect(project.reload.sprints.last.name).to eq("My Sprint")
|
||||
expect(flash[:notice]).to eq(I18n.t(:notice_successful_create))
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
post :create, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -261,36 +255,34 @@ RSpec.describe RbSprintsController do
|
||||
describe "PUT #update_agile_sprint" do
|
||||
let!(:sprint) { create(:agile_sprint, name: "Original sprint name", project:) }
|
||||
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
let(:params) do
|
||||
{
|
||||
id: sprint.id,
|
||||
project_id: project.id,
|
||||
sprint: { name: "Changed sprint name" }
|
||||
}
|
||||
end
|
||||
let(:params) do
|
||||
{
|
||||
id: sprint.id,
|
||||
project_id: project.id,
|
||||
sprint: { name: "Changed sprint name" }
|
||||
}
|
||||
end
|
||||
|
||||
it "responds with success", :aggregate_failures do
|
||||
it "responds with success", :aggregate_failures do
|
||||
put :update_agile_sprint, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response.body).to have_turbo_stream action: "flash"
|
||||
expect(response.body).to have_turbo_stream action: "update", target: "backlogs-sprint-header-component-#{sprint.id}"
|
||||
assert_select %(turbo-stream[action="update"][target="backlogs-sprint-header-component-#{sprint.id}"][method="morph"])
|
||||
expect(response.body).to include("Successful update.")
|
||||
expect(sprint.reload.name).to eq("Changed sprint name")
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
put :update_agile_sprint, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response.body).to have_turbo_stream action: "flash"
|
||||
expect(response.body).to have_turbo_stream action: "update", target: "backlogs-sprint-header-component-#{sprint.id}"
|
||||
assert_select %(turbo-stream[action="update"][target="backlogs-sprint-header-component-#{sprint.id}"][method="morph"])
|
||||
expect(response.body).to include("Successful update.")
|
||||
expect(sprint.reload.name).to eq("Changed sprint name")
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
put :update_agile_sprint, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -308,8 +300,7 @@ RSpec.describe RbSprintsController do
|
||||
.and_return(service)
|
||||
end
|
||||
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
context "when the sprint is rendered in a receiving project" do
|
||||
context "when the sprint is rendered in a receiving project" do
|
||||
let(:source_project) { create(:project, sprint_sharing: "share_all_projects") }
|
||||
let(:project) { create(:project, sprint_sharing: "receive_shared") }
|
||||
let!(:sprint) { create(:agile_sprint, project: source_project) }
|
||||
@@ -356,7 +347,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when a board already exists" do
|
||||
context "when a board already exists" do
|
||||
let!(:existing_board) do
|
||||
create(:board_grid_with_query,
|
||||
project:,
|
||||
@@ -372,7 +363,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when board creation succeeds" do
|
||||
context "when board creation succeeds" do
|
||||
let(:board) { create(:board_grid_with_query, project:, linked: sprint) }
|
||||
let(:service_result) do
|
||||
started_sprint = sprint.tap { it.status = "active" }
|
||||
@@ -393,7 +384,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when board creation fails" do
|
||||
context "when board creation fails" do
|
||||
let(:service_result) { ServiceResult.failure(message: "something went wrong") }
|
||||
|
||||
it "redirects back to the backlog and leaves the sprint in planning", :aggregate_failures do
|
||||
@@ -407,7 +398,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when sprint start fails without an explicit message" do
|
||||
context "when sprint start fails without an explicit message" do
|
||||
let(:service_result) { ServiceResult.failure }
|
||||
|
||||
it "redirects back with the default start failure message", :aggregate_failures do
|
||||
@@ -419,7 +410,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when another sprint is already active" do
|
||||
context "when another sprint is already active" do
|
||||
let!(:active_sprint) { create(:agile_sprint, project:, status: "active") }
|
||||
let(:service_result) do
|
||||
ServiceResult.failure(
|
||||
@@ -437,7 +428,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "without the 'start_complete_sprint' permission" do
|
||||
context "without the 'start_complete_sprint' permission" do
|
||||
let(:permissions) { all_permissions - [:start_complete_sprint] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
@@ -448,7 +439,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when the sprint is already active" do
|
||||
context "when the sprint is already active" do
|
||||
let!(:sprint) { create(:agile_sprint, project:, status: "active") }
|
||||
let(:service_result) { ServiceResult.failure }
|
||||
|
||||
@@ -460,7 +451,6 @@ RSpec.describe RbSprintsController do
|
||||
expect(service).to have_received(:call)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST #finish" do
|
||||
@@ -480,8 +470,7 @@ RSpec.describe RbSprintsController do
|
||||
.and_return(service)
|
||||
end
|
||||
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
context "when the sprint is rendered in a receiving project" do
|
||||
context "when the sprint is rendered in a receiving project" do
|
||||
let(:source_project) { create(:project, sprint_sharing: "share_all_projects") }
|
||||
let(:project) { create(:project, sprint_sharing: "receive_shared") }
|
||||
let!(:sprint) { create(:agile_sprint, project: source_project, status: "active") }
|
||||
@@ -517,7 +506,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
it "finishes the sprint and redirects to the backlog via turbo stream", :aggregate_failures do
|
||||
it "finishes the sprint and redirects to the backlog via turbo stream", :aggregate_failures do
|
||||
post :finish, format: :turbo_stream, params: request_params
|
||||
|
||||
expect(response).to be_successful
|
||||
@@ -527,7 +516,7 @@ RSpec.describe RbSprintsController do
|
||||
expect(service).to have_received(:call)
|
||||
end
|
||||
|
||||
context "when finishing fails" do
|
||||
context "when finishing fails" do
|
||||
let(:service_result) { ServiceResult.failure(message: "something went wrong") }
|
||||
|
||||
it "redirects back to the backlog", :aggregate_failures do
|
||||
@@ -541,7 +530,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when finishing fails without an explicit message" do
|
||||
context "when finishing fails without an explicit message" do
|
||||
let(:service_result) { ServiceResult.failure }
|
||||
|
||||
it "redirects back with the default finish failure message", :aggregate_failures do
|
||||
@@ -553,7 +542,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "without the 'start_complete_sprint' permission" do
|
||||
context "without the 'start_complete_sprint' permission" do
|
||||
let(:permissions) { all_permissions - [:start_complete_sprint] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
@@ -564,7 +553,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when the sprint is already completed" do
|
||||
context "when the sprint is already completed" do
|
||||
let!(:sprint) { create(:agile_sprint, project:, status: "completed") }
|
||||
let(:service_result) { ServiceResult.failure }
|
||||
|
||||
@@ -577,7 +566,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when moving to the top of the backlog" do
|
||||
context "when moving to the top of the backlog" do
|
||||
let(:request_params) { { project_id: project.id, id: sprint.id, unfinished_action: "move_to_top_of_backlog" } }
|
||||
|
||||
it "passes unfinished_action to the service and redirects via turbo stream", :aggregate_failures do
|
||||
@@ -590,7 +579,7 @@ RSpec.describe RbSprintsController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when moving to the bottom of the backlog" do
|
||||
context "when moving to the bottom of the backlog" do
|
||||
let(:request_params) { { project_id: project.id, id: sprint.id, unfinished_action: "move_to_bottom_of_backlog" } }
|
||||
|
||||
it "passes unfinished_action to the service and redirects via turbo stream", :aggregate_failures do
|
||||
@@ -602,15 +591,42 @@ RSpec.describe RbSprintsController do
|
||||
.with(hash_including(unfinished_action: "move_to_bottom_of_backlog"))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET #refresh_form" do
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
let(:params) do
|
||||
{
|
||||
project_id: project.id,
|
||||
sprint: { name: "My Sprint", start_date: "2025-10-05", finish_date: "2025-10-15" }
|
||||
}
|
||||
end
|
||||
|
||||
it "responds with success", :aggregate_failures do
|
||||
get :refresh_form, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response).to have_turbo_stream action: "update", target: "backlogs-new-sprint-form-component"
|
||||
expect(assigns(:sprint)).to be_nil
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
get :refresh_form, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
context "when refreshing the form in edit mode by passing a sprint id" do
|
||||
let!(:sprint) { create(:agile_sprint, project:) }
|
||||
let(:params) do
|
||||
{
|
||||
project_id: project.id,
|
||||
sprint: { name: "My Sprint", start_date: "2025-10-05", finish_date: "2025-10-15" }
|
||||
sprint: { id: sprint.id, name: "My Sprint", start_date: "2025-10-05", finish_date: "2025-10-15" }
|
||||
}
|
||||
end
|
||||
|
||||
@@ -620,36 +636,6 @@ RSpec.describe RbSprintsController do
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response).to have_turbo_stream action: "update", target: "backlogs-new-sprint-form-component"
|
||||
expect(assigns(:sprint)).to be_nil
|
||||
end
|
||||
|
||||
context "without the 'create_sprints' permission" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "responds with forbidden", :aggregate_failures do
|
||||
get :refresh_form, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).not_to be_successful
|
||||
expect(response).to have_http_status :forbidden
|
||||
end
|
||||
end
|
||||
|
||||
context "when refreshing the form in edit mode by passing a sprint id" do
|
||||
let!(:sprint) { create(:agile_sprint, project:) }
|
||||
let(:params) do
|
||||
{
|
||||
project_id: project.id,
|
||||
sprint: { id: sprint.id, name: "My Sprint", start_date: "2025-10-05", finish_date: "2025-10-15" }
|
||||
}
|
||||
end
|
||||
|
||||
it "responds with success", :aggregate_failures do
|
||||
get :refresh_form, format: :turbo_stream, params: params
|
||||
|
||||
expect(response).to be_successful
|
||||
expect(response).to have_http_status :ok
|
||||
expect(response).to have_turbo_stream action: "update", target: "backlogs-new-sprint-form-component"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -58,7 +58,7 @@ RSpec.describe RbStoriesController do
|
||||
format: :html
|
||||
end
|
||||
|
||||
context "when scrum_projects flag is inactive", with_flag: { scrum_projects: false } do
|
||||
context "when loading from a version sprint" do
|
||||
let(:load_story_id) { story.id }
|
||||
let(:requested_sprint) { version_sprint }
|
||||
|
||||
@@ -78,7 +78,7 @@ RSpec.describe RbStoriesController do
|
||||
end
|
||||
end
|
||||
|
||||
context "when scrum_projects flag is active", with_flag: { scrum_projects: true } do
|
||||
context "when loading from an agile sprint" do
|
||||
let(:agile_sprint) { create(:agile_sprint, name: "Agile Sprint load_story", project:) }
|
||||
let(:work_package_in_sprint) { create(:work_package, status:, sprint: agile_sprint, project:) }
|
||||
let(:load_story_id) { work_package_in_sprint.id }
|
||||
@@ -243,11 +243,11 @@ RSpec.describe RbStoriesController do
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT #move", with_flag: { scrum_projects: true } do
|
||||
describe "PUT #move" do
|
||||
let(:agile_sprint) { create(:agile_sprint, name: "Agile Sprint 1", project:) }
|
||||
let(:story_in_agile_sprint) { create(:work_package, status:, sprint: agile_sprint, project:) }
|
||||
|
||||
context "with another Agile::Sprint as target", with_flag: { scrum_projects: true } do
|
||||
context "with another Agile::Sprint as target" do
|
||||
let(:other_agile_sprint) { create(:agile_sprint, name: "Agile Sprint 2", project:) }
|
||||
|
||||
it "responds with success and moves story to another Agile::Sprint", :aggregate_failures do
|
||||
@@ -304,7 +304,7 @@ RSpec.describe RbStoriesController do
|
||||
end
|
||||
end
|
||||
|
||||
context "with a Sprint (Version) as target", with_flag: { scrum_projects: true } do
|
||||
context "with a Sprint (Version) as target" do
|
||||
it "responds with success and moves story to Sprint", :aggregate_failures do
|
||||
put :move, params: {
|
||||
project_id: project.id,
|
||||
|
||||
@@ -48,132 +48,89 @@ RSpec.describe RbTaskboardsController do
|
||||
end
|
||||
|
||||
describe "GET show" do
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
let(:sprint) { create(:agile_sprint, project:) }
|
||||
let(:sprint) { create(:agile_sprint, project:) }
|
||||
|
||||
context "when the board exists" do
|
||||
let!(:other_project) { create(:project) }
|
||||
let!(:other_board) { create(:board_grid_with_query, project: other_project, linked: sprint) }
|
||||
context "when the board exists" do
|
||||
let!(:other_project) { create(:project) }
|
||||
let!(:other_board) { create(:board_grid_with_query, project: other_project, linked: sprint) }
|
||||
|
||||
before do
|
||||
board.update!(linked: sprint)
|
||||
end
|
||||
|
||||
context "as a member with view_sprints permission" do
|
||||
let(:permissions) { %i[view_sprints view_work_packages] }
|
||||
|
||||
before do
|
||||
get :show, params: { project_id: project.identifier, sprint_id: sprint.id }
|
||||
end
|
||||
|
||||
it "redirects to the board" do
|
||||
expect(response).to redirect_to(project_work_package_board_path(project, board))
|
||||
end
|
||||
|
||||
it "uses the board for the current project" do
|
||||
expect(response).to redirect_to(project_work_package_board_path(project, board))
|
||||
expect(response).not_to redirect_to(project_work_package_board_path(other_project, other_board))
|
||||
end
|
||||
end
|
||||
before do
|
||||
board.update!(linked: sprint)
|
||||
end
|
||||
|
||||
context "when the board does not exist" do
|
||||
context "as a member with view_sprints permission" do
|
||||
let(:permissions) { %i[view_sprints view_work_packages] }
|
||||
|
||||
before do
|
||||
get :show, params: { project_id: project.identifier, sprint_id: sprint.id }
|
||||
end
|
||||
|
||||
it "returns not found" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the sprint is rendered in a receiving project" do
|
||||
let(:source_project) { create(:project, sprint_sharing: "share_all_projects") }
|
||||
let(:project) do
|
||||
create(:project,
|
||||
sprint_sharing: "receive_shared",
|
||||
member_with_permissions: { user => permissions })
|
||||
end
|
||||
let(:permissions) { %i[view_sprints view_work_packages] }
|
||||
let(:sprint) { create(:agile_sprint, project: source_project) }
|
||||
|
||||
before do
|
||||
create(:board_grid_with_query, project: source_project, linked: sprint)
|
||||
get :show, params: { project_id: project.identifier, sprint_id: sprint.id }
|
||||
it "redirects to the board" do
|
||||
expect(response).to redirect_to(project_work_package_board_path(project, board))
|
||||
end
|
||||
|
||||
it "returns not found when the receiving project has no task board" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context "as a member without view_sprints permission" do
|
||||
let(:permissions) { [:view_project] }
|
||||
|
||||
before do
|
||||
board.update!(linked: sprint)
|
||||
get :show, params: { project_id: project.identifier, sprint_id: sprint.id }
|
||||
end
|
||||
|
||||
it "denies access" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context "as a non-member" do
|
||||
current_user { create(:user) }
|
||||
|
||||
before do
|
||||
board.update!(linked: sprint)
|
||||
get :show, params: { project_id: project.identifier, sprint_id: sprint.id }
|
||||
end
|
||||
|
||||
it "denies access" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
it "uses the board for the current project" do
|
||||
expect(response).to redirect_to(project_work_package_board_path(project, board))
|
||||
expect(response).not_to redirect_to(project_work_package_board_path(other_project, other_board))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with the feature flag inactive", with_flag: { scrum_projects: false } do
|
||||
let(:sprint) { create(:sprint, project:) }
|
||||
context "when the board does not exist" do
|
||||
let(:permissions) { %i[view_sprints view_work_packages] }
|
||||
|
||||
before do
|
||||
get :show, params: { project_id: project.identifier, sprint_id: sprint.id }
|
||||
end
|
||||
|
||||
it "renders the legacy show template" do
|
||||
expect(response).to be_successful
|
||||
expect(response).to render_template :show
|
||||
it "returns not found" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context "when the sprint is rendered in a receiving project" do
|
||||
let(:source_project) { create(:project, sprint_sharing: "share_all_projects") }
|
||||
let(:project) do
|
||||
create(:project,
|
||||
sprint_sharing: "receive_shared",
|
||||
member_with_permissions: { user => permissions })
|
||||
end
|
||||
let(:permissions) { %i[view_sprints view_work_packages] }
|
||||
let(:sprint) { create(:agile_sprint, project: source_project) }
|
||||
|
||||
before do
|
||||
create(:board_grid_with_query, project: source_project, linked: sprint)
|
||||
get :show, params: { project_id: project.identifier, sprint_id: sprint.id }
|
||||
end
|
||||
|
||||
context "as a member with view_sprints permission" do
|
||||
let(:permissions) { %i[view_sprints view_work_packages] }
|
||||
it "returns not found when the receiving project has no task board" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
it "grants access" do
|
||||
expect(response).to be_successful
|
||||
expect(response).to render_template :show
|
||||
end
|
||||
context "as a member without view_sprints permission" do
|
||||
let(:permissions) { [:view_project] }
|
||||
|
||||
before do
|
||||
board.update!(linked: sprint)
|
||||
get :show, params: { project_id: project.identifier, sprint_id: sprint.id }
|
||||
end
|
||||
|
||||
context "as a member without view_sprints permission" do
|
||||
let(:permissions) { [:view_project] }
|
||||
it "denies access" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
it "denies access" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
context "as a non-member" do
|
||||
current_user { create(:user) }
|
||||
|
||||
before do
|
||||
board.update!(linked: sprint)
|
||||
get :show, params: { project_id: project.identifier, sprint_id: sprint.id }
|
||||
end
|
||||
|
||||
context "as a non-member" do
|
||||
let(:permissions) { [] }
|
||||
|
||||
current_user { create(:user) }
|
||||
|
||||
it "denies access" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
it "denies access" do
|
||||
expect(response).to have_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#-- 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.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe VersionsController, "Backlog patches" do
|
||||
let(:version) do
|
||||
create(:version,
|
||||
sharing: "system")
|
||||
end
|
||||
|
||||
let(:other_project) do
|
||||
create(:project).tap do |p|
|
||||
create(:member,
|
||||
user: current_user,
|
||||
roles: [create(:project_role, permissions: [:manage_versions])],
|
||||
project: p)
|
||||
end
|
||||
end
|
||||
|
||||
let(:current_user) do
|
||||
create(:user,
|
||||
member_with_permissions: { version.project => [:manage_versions] })
|
||||
end
|
||||
|
||||
before do
|
||||
# Create a version assigned to a project
|
||||
@oldVersionName = version.name
|
||||
@newVersionName = "NewVersionName"
|
||||
|
||||
# Create params to update version
|
||||
@params = {}
|
||||
@params[:id] = version.id
|
||||
@params[:version] = { name: @newVersionName }
|
||||
login_as current_user
|
||||
end
|
||||
|
||||
describe "update" do
|
||||
it "does not allow to update versions from different projects" do
|
||||
@params[:project_id] = other_project.id
|
||||
patch "update", params: @params
|
||||
version.reload
|
||||
|
||||
expect(response).to redirect_to project_settings_versions_path(other_project)
|
||||
expect(version.name).to eq(@oldVersionName)
|
||||
end
|
||||
|
||||
it "allows to update versions from the version project" do
|
||||
@params[:project_id] = version.project.id
|
||||
patch "update", params: @params
|
||||
version.reload
|
||||
|
||||
expect(response).to redirect_to project_settings_versions_path(version.project)
|
||||
expect(version.name).to eq(@newVersionName)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -47,85 +47,7 @@ RSpec.describe "Backlogs Admin Settings", :js do
|
||||
visit admin_backlogs_settings_path
|
||||
end
|
||||
|
||||
scenario "updating story types" do
|
||||
expect(page).to have_heading "Backlogs"
|
||||
|
||||
story_autocompleter.select_option "Feature", "Story"
|
||||
|
||||
click_on "Save"
|
||||
|
||||
expect_and_dismiss_flash type: :success, message: "Successful update."
|
||||
|
||||
story_autocompleter.expect_selected "Feature", "Story"
|
||||
end
|
||||
|
||||
scenario "updating task type" do
|
||||
expect(page).to have_heading "Backlogs"
|
||||
|
||||
task_autocompleter.select_option "Task"
|
||||
|
||||
click_on "Save"
|
||||
|
||||
expect_and_dismiss_flash type: :success, message: "Successful update."
|
||||
|
||||
task_autocompleter.expect_selected "Task"
|
||||
end
|
||||
|
||||
scenario "ensuring the same type is not selected as story and task type" do
|
||||
expect(page).to have_heading "Backlogs"
|
||||
|
||||
wait_for_network_idle
|
||||
|
||||
wait_for_autocompleter_options_to_be_loaded
|
||||
story_autocompleter.expect_blank
|
||||
task_autocompleter.expect_blank
|
||||
|
||||
# Select a value in the story autocompleter...
|
||||
story_autocompleter.select_option "Feature"
|
||||
story_autocompleter.expect_selected "Feature"
|
||||
story_autocompleter.expect_not_disabled "Story"
|
||||
story_autocompleter.close_autocompleter
|
||||
|
||||
# ... which is then disabled in the task autocompleter.
|
||||
task_autocompleter.open_options
|
||||
task_autocompleter.expect_disabled "Feature"
|
||||
|
||||
# Other way around: Select a value in the task automcompleter...
|
||||
task_autocompleter.select_option "Story"
|
||||
task_autocompleter.expect_selected "Story"
|
||||
task_autocompleter.close_autocompleter
|
||||
|
||||
# ... which will be disabled in the story autocompleter
|
||||
story_autocompleter.open_options
|
||||
story_autocompleter.expect_disabled "Story"
|
||||
story_autocompleter.expect_selected "Feature"
|
||||
end
|
||||
|
||||
scenario "updating points burn direction" do
|
||||
expect(page).to have_heading "Backlogs"
|
||||
|
||||
choose "Down", fieldset: "Points burn up/down"
|
||||
|
||||
click_on "Save"
|
||||
|
||||
expect_and_dismiss_flash type: :success, message: "Successful update."
|
||||
|
||||
expect(page).to have_checked_field "Down", fieldset: "Points burn up/down"
|
||||
end
|
||||
|
||||
scenario "updating template for wiki page" do
|
||||
expect(page).to have_heading "Backlogs"
|
||||
|
||||
fill_in "Template for sprint wiki page", with: "my_sprint_wiki_page"
|
||||
|
||||
click_on "Save"
|
||||
|
||||
expect_and_dismiss_flash type: :success, message: "Successful update."
|
||||
|
||||
expect(page).to have_field "Template for sprint wiki page", with: "my_sprint_wiki_page"
|
||||
end
|
||||
|
||||
it "hides configuration on scrum projects feature flag active", with_flag: { scrum_projects: true } do
|
||||
it "shows the sprint planning blankslate instead of legacy configuration" do
|
||||
expect(page).to have_no_field "Template for sprint wiki page"
|
||||
expect(page).to have_no_css "[data-test-selector='story_type_autocomplete']"
|
||||
expect(page).to have_no_css "[data-test-selector='task_type_autocomplete']"
|
||||
|
||||
@@ -47,166 +47,155 @@ RSpec.describe "Create", :js do
|
||||
|
||||
current_user { create(:user, member_with_permissions: { project => permissions }) }
|
||||
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
it "shows the correct breadcrumb menu" do
|
||||
it "shows the correct breadcrumb menu" do
|
||||
planning_page.visit!
|
||||
|
||||
within ".PageHeader-breadcrumbs" do
|
||||
expect(page).to have_link(href: project_path(project), text: project.name)
|
||||
expect(page).to have_link(href: backlog_backlogs_project_backlogs_path(project), text: "Backlogs")
|
||||
expect(page).to have_text("Backlog and sprints")
|
||||
end
|
||||
end
|
||||
|
||||
it "renders the menu" do
|
||||
planning_page.visit!
|
||||
|
||||
within "#main-menu" do
|
||||
expect(page).to have_css(".selected", text: "Backlog and sprints")
|
||||
end
|
||||
end
|
||||
|
||||
context "with the 'create_sprints' permissions" do
|
||||
let(:start_date) { Date.new(2025, 10, 5) }
|
||||
let(:start_date_fmt) { start_date.strftime("%Y-%m-%d") }
|
||||
let(:finish_date) { Date.new(2025, 10, 20) }
|
||||
let(:finish_date_fmt) { finish_date.strftime("%Y-%m-%d") }
|
||||
|
||||
it "allows creating a new sprint" do
|
||||
planning_page.visit!
|
||||
|
||||
within ".PageHeader-breadcrumbs" do
|
||||
expect(page).to have_link(href: project_path(project), text: project.name)
|
||||
expect(page).to have_link(href: backlog_backlogs_project_backlogs_path(project), text: "Backlogs")
|
||||
expect(page).to have_text("Backlog and sprints")
|
||||
planning_page.expect_sprint_names_in_order(initial_sprint.name)
|
||||
|
||||
planning_page.open_create_sprint_dialog
|
||||
|
||||
within_dialog "New sprint" do
|
||||
page.fill_in "Sprint name", with: "Created sprint"
|
||||
page.fill_in "Start date", with: start_date_fmt
|
||||
page.fill_in "Finish date", with: finish_date_fmt
|
||||
|
||||
click_on "Create"
|
||||
end
|
||||
|
||||
expect_and_dismiss_flash(exact_message: "Successful creation.")
|
||||
planning_page.expect_sprint_names_in_order(initial_sprint.name, "Created sprint")
|
||||
|
||||
sprint = project.reload.sprints.last
|
||||
expect(sprint).to be_present
|
||||
expect(sprint.name).to eq "Created sprint"
|
||||
expect(sprint.start_date).to eq start_date
|
||||
expect(sprint.finish_date).to eq finish_date
|
||||
end
|
||||
|
||||
it "previews the sprint duration when changing the dates" do
|
||||
planning_page.visit!
|
||||
|
||||
planning_page.open_create_sprint_dialog
|
||||
|
||||
within_dialog "New sprint" do
|
||||
expect(page).to have_field "Duration", with: "", readonly: true
|
||||
|
||||
page.fill_in "Start date", with: start_date_fmt
|
||||
page.fill_in "Finish date", with: finish_date_fmt
|
||||
|
||||
expect(page).to have_field "Duration", with: "16 days", readonly: true
|
||||
end
|
||||
end
|
||||
|
||||
it "renders the menu" do
|
||||
planning_page.visit!
|
||||
describe "validations" do
|
||||
let(:too_early_finish_date) { start_date - 1.day }
|
||||
|
||||
within "#main-menu" do
|
||||
expect(page).to have_css(".selected", text: "Backlog and sprints")
|
||||
end
|
||||
end
|
||||
|
||||
context "with the 'create_sprints' permissions" do
|
||||
let(:start_date) { Date.new(2025, 10, 5) }
|
||||
let(:start_date_fmt) { start_date.strftime("%Y-%m-%d") }
|
||||
let(:finish_date) { Date.new(2025, 10, 20) }
|
||||
let(:finish_date_fmt) { finish_date.strftime("%Y-%m-%d") }
|
||||
|
||||
it "allows creating a new sprint" do
|
||||
it "validates required fields are present" do
|
||||
planning_page.visit!
|
||||
|
||||
planning_page.expect_sprint_names_in_order(initial_sprint.name)
|
||||
|
||||
planning_page.open_create_sprint_dialog
|
||||
|
||||
within_dialog "New sprint" do
|
||||
page.fill_in "Sprint name", with: "Created sprint"
|
||||
page.fill_in "Start date", with: start_date_fmt
|
||||
page.fill_in "Finish date", with: finish_date_fmt
|
||||
page.fill_in "Sprint name", with: ""
|
||||
|
||||
click_on "Create"
|
||||
|
||||
expect(page).to have_field "Sprint name", validation_error: "can't be blank"
|
||||
expect(page).to have_field "Start date", validation_error: false
|
||||
expect(page).to have_field "Finish date", validation_error: false
|
||||
end
|
||||
|
||||
expect_and_dismiss_flash(exact_message: "Successful creation.")
|
||||
planning_page.expect_sprint_names_in_order(initial_sprint.name, "Created sprint")
|
||||
|
||||
sprint = project.reload.sprints.last
|
||||
expect(sprint).to be_present
|
||||
expect(sprint.name).to eq "Created sprint"
|
||||
expect(sprint.start_date).to eq start_date
|
||||
expect(sprint.finish_date).to eq finish_date
|
||||
end
|
||||
|
||||
it "previews the sprint duration when changing the dates" do
|
||||
it "validates finish date is not before start date" do
|
||||
planning_page.visit!
|
||||
|
||||
planning_page.open_create_sprint_dialog
|
||||
|
||||
within_dialog "New sprint" do
|
||||
expect(page).to have_field "Duration", with: "", readonly: true
|
||||
|
||||
page.fill_in "Start date", with: start_date_fmt
|
||||
page.fill_in "Finish date", with: finish_date_fmt
|
||||
page.fill_in "Finish date", with: too_early_finish_date.strftime("%Y-%m-%d")
|
||||
|
||||
expect(page).to have_field "Duration", with: "16 days", readonly: true
|
||||
# Shows duration as zero if finish date is before start date:
|
||||
expect(page).to have_field "Duration", with: "0 days", readonly: true
|
||||
|
||||
click_on "Create"
|
||||
|
||||
expect(page).to have_field("Finish date",
|
||||
validation_error: "must be greater than or equal to #{start_date_fmt}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "proposed sprint names" do
|
||||
before do
|
||||
Agile::Sprint.delete_all
|
||||
end
|
||||
|
||||
it "prefilled with 'Sprint 1' if there are no previous sprints" do
|
||||
planning_page.visit!
|
||||
|
||||
planning_page.open_create_sprint_dialog
|
||||
|
||||
within_dialog "New sprint" do
|
||||
expect(page).to have_field "Sprint name *", with: "Sprint 1", required: true, focused: true
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
let(:too_early_finish_date) { start_date - 1.day }
|
||||
|
||||
it "validates required fields are present" do
|
||||
planning_page.visit!
|
||||
|
||||
planning_page.open_create_sprint_dialog
|
||||
|
||||
within_dialog "New sprint" do
|
||||
page.fill_in "Sprint name", with: ""
|
||||
|
||||
click_on "Create"
|
||||
|
||||
expect(page).to have_field "Sprint name", validation_error: "can't be blank"
|
||||
expect(page).to have_field "Start date", validation_error: false
|
||||
expect(page).to have_field "Finish date", validation_error: false
|
||||
end
|
||||
end
|
||||
|
||||
it "validates finish date is not before start date" do
|
||||
planning_page.visit!
|
||||
|
||||
planning_page.open_create_sprint_dialog
|
||||
|
||||
within_dialog "New sprint" do
|
||||
page.fill_in "Start date", with: start_date_fmt
|
||||
page.fill_in "Finish date", with: too_early_finish_date.strftime("%Y-%m-%d")
|
||||
|
||||
# Shows duration as zero if finish date is before start date:
|
||||
expect(page).to have_field "Duration", with: "0 days", readonly: true
|
||||
|
||||
click_on "Create"
|
||||
|
||||
expect(page).to have_field("Finish date",
|
||||
validation_error: "must be greater than or equal to #{start_date_fmt}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "proposed sprint names" do
|
||||
context "with a previous sprint" do
|
||||
before do
|
||||
Agile::Sprint.delete_all
|
||||
end
|
||||
create(:agile_sprint, name: "Be ambitious 42", project:)
|
||||
|
||||
it "prefilled with 'Sprint 1' if there are no previous sprints" do
|
||||
planning_page.visit!
|
||||
|
||||
planning_page.open_create_sprint_dialog
|
||||
end
|
||||
|
||||
it "offers the next sprint name with a number increment" do
|
||||
within_dialog "New sprint" do
|
||||
expect(page).to have_field "Sprint name *", with: "Sprint 1", required: true, focused: true
|
||||
expect(page).to have_field "Sprint name *", with: "Be ambitious 43"
|
||||
end
|
||||
end
|
||||
|
||||
context "with a previous sprint" do
|
||||
before do
|
||||
create(:agile_sprint, name: "Be ambitious 42", project:)
|
||||
|
||||
planning_page.visit!
|
||||
planning_page.open_create_sprint_dialog
|
||||
end
|
||||
|
||||
it "offers the next sprint name with a number increment" do
|
||||
within_dialog "New sprint" do
|
||||
expect(page).to have_field "Sprint name *", with: "Be ambitious 43"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without the necessary permissions" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "is missing the 'new sprint' button" do
|
||||
planning_page.visit!
|
||||
|
||||
expect(page).to have_no_button "Create"
|
||||
expect(page).not_to have_test_selector("op-sprints--new-sprint-button")
|
||||
end
|
||||
end
|
||||
|
||||
context "with the project receiving sprints from another project" do
|
||||
let(:project) { create(:project, sprint_sharing: Projects::SprintSharing::RECEIVE_SHARED) }
|
||||
|
||||
it "is missing the 'new sprint' button" do
|
||||
planning_page.visit!
|
||||
|
||||
expect(page).to have_no_button "Create"
|
||||
expect(page).not_to have_test_selector("op-sprints--new-sprint-button")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "with the feature flag inactive" do
|
||||
context "without the necessary permissions" do
|
||||
let(:permissions) { all_permissions - [:create_sprints] }
|
||||
|
||||
it "is missing the 'new sprint' button" do
|
||||
planning_page.visit!
|
||||
|
||||
expect(page).to have_no_button "Create"
|
||||
expect(page).not_to have_test_selector("op-sprints--new-sprint-button")
|
||||
end
|
||||
end
|
||||
|
||||
context "with the project receiving sprints from another project" do
|
||||
let(:project) { create(:project, sprint_sharing: Projects::SprintSharing::RECEIVE_SHARED) }
|
||||
|
||||
it "is missing the 'new sprint' button" do
|
||||
planning_page.visit!
|
||||
|
||||
|
||||
@@ -80,108 +80,106 @@ RSpec.describe "Edit", :js do
|
||||
planning_page.visit!
|
||||
end
|
||||
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
it "lists all open sprints" do
|
||||
planning_page.expect_sprint_names_in_order(first_sprint.name, second_sprint.name)
|
||||
it "lists all open sprints" do
|
||||
planning_page.expect_sprint_names_in_order(first_sprint.name, second_sprint.name)
|
||||
|
||||
planning_page.expect_story_in_sprint(work_package, first_sprint)
|
||||
planning_page.expect_story_not_in_sprint(work_package, second_sprint)
|
||||
planning_page.expect_story_in_sprint(work_package, first_sprint)
|
||||
planning_page.expect_story_not_in_sprint(work_package, second_sprint)
|
||||
end
|
||||
|
||||
it "adds a work package to a sprint" do
|
||||
planning_page.click_in_sprint_menu(first_sprint, "Add work package")
|
||||
planning_page.expect_create_work_package_dialog
|
||||
|
||||
page.within("#create-work-package-dialog") do
|
||||
page.fill_in "Subject", with: "Story created in sprint"
|
||||
|
||||
click_on "Create"
|
||||
end
|
||||
|
||||
it "adds a work package to a sprint" do
|
||||
planning_page.click_in_sprint_menu(first_sprint, "Add work package")
|
||||
planning_page.expect_create_work_package_dialog
|
||||
wait_for_reload
|
||||
|
||||
page.within("#create-work-package-dialog") do
|
||||
page.fill_in "Subject", with: "Story created in sprint"
|
||||
expect_and_dismiss_flash type: :success, exact_message: "Successful creation."
|
||||
created_wp = first_sprint.reload.work_packages.last
|
||||
expect(created_wp.subject).to eq("Story created in sprint")
|
||||
planning_page.expect_story_in_sprint(created_wp, first_sprint)
|
||||
end
|
||||
|
||||
click_on "Create"
|
||||
end
|
||||
|
||||
wait_for_reload
|
||||
|
||||
expect_and_dismiss_flash type: :success, exact_message: "Successful creation."
|
||||
created_wp = first_sprint.reload.work_packages.last
|
||||
expect(created_wp.subject).to eq("Story created in sprint")
|
||||
planning_page.expect_story_in_sprint(created_wp, first_sprint)
|
||||
end
|
||||
|
||||
context "with the 'create_sprints' permissions" do
|
||||
context "when editing a sprint" do
|
||||
it "displays all menu entries" do
|
||||
planning_page.within_sprint_menu(first_sprint) do |menu|
|
||||
expect(menu).to have_selector :menuitem, count: 2
|
||||
expect(menu).to have_selector :menuitem, "Edit sprint"
|
||||
expect(menu).to have_selector :menuitem, "Add work package"
|
||||
end
|
||||
end
|
||||
|
||||
it "edits the sprint name" do
|
||||
planning_page.expect_sprint_names_in_order(first_sprint.name, second_sprint.name)
|
||||
|
||||
planning_page.click_in_sprint_menu(first_sprint, "Edit sprint")
|
||||
planning_page.expect_sprint_dialog
|
||||
|
||||
within_dialog "Edit sprint" do
|
||||
page.fill_in "Sprint name", with: "Changed name"
|
||||
page.click_button "Save"
|
||||
end
|
||||
|
||||
wait_for_reload
|
||||
planning_page.expect_sprint_names_in_order("Changed name", second_sprint.name)
|
||||
end
|
||||
|
||||
context "when lacking the 'manage_sprint_items' permission" do
|
||||
let(:permissions) { all_permissions - %i[manage_sprint_items] }
|
||||
|
||||
it "has no menu entry for creating a new story" do
|
||||
planning_page.within_sprint_menu(first_sprint) do |menu|
|
||||
expect(menu).to have_selector :menuitem, count: 1
|
||||
expect(menu).to have_selector :menuitem, "Edit sprint"
|
||||
|
||||
expect(menu).to have_no_selector :menuitem, "Add work package"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
context "when sprint status is active" do
|
||||
before { first_sprint.update!(status: "active") }
|
||||
|
||||
it "validates required fields are present" do
|
||||
planning_page.click_in_sprint_menu(first_sprint, "Edit sprint")
|
||||
planning_page.expect_sprint_dialog
|
||||
|
||||
within_dialog "Edit sprint" do
|
||||
page.fill_in "Sprint name", with: ""
|
||||
page.fill_in "Start date", with: ""
|
||||
page.fill_in "Finish date", with: ""
|
||||
|
||||
page.click_button "Save"
|
||||
|
||||
expect(page).to have_field "Sprint name", validation_error: "can't be blank"
|
||||
expect(page).to have_field "Start date", validation_error: "can't be blank"
|
||||
expect(page).to have_field "Finish date", validation_error: "can't be blank"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without the necessary permissions" do
|
||||
let(:permissions) { all_permissions - %i[create_sprints start_complete_sprint] }
|
||||
|
||||
it "is missing the 'new sprint' button" do
|
||||
expect(page).to have_no_button "Create"
|
||||
expect(page).not_to have_test_selector("op-sprints--new-sprint-button")
|
||||
end
|
||||
|
||||
it "has no menu entry for editing a sprint" do
|
||||
context "with the 'create_sprints' permissions" do
|
||||
context "when editing a sprint" do
|
||||
it "displays all menu entries" do
|
||||
planning_page.within_sprint_menu(first_sprint) do |menu|
|
||||
expect(menu).to have_no_selector :menuitem, "Edit sprint"
|
||||
expect(menu).to have_selector :menuitem, count: 2
|
||||
expect(menu).to have_selector :menuitem, "Edit sprint"
|
||||
expect(menu).to have_selector :menuitem, "Add work package"
|
||||
end
|
||||
end
|
||||
|
||||
it "edits the sprint name" do
|
||||
planning_page.expect_sprint_names_in_order(first_sprint.name, second_sprint.name)
|
||||
|
||||
planning_page.click_in_sprint_menu(first_sprint, "Edit sprint")
|
||||
planning_page.expect_sprint_dialog
|
||||
|
||||
within_dialog "Edit sprint" do
|
||||
page.fill_in "Sprint name", with: "Changed name"
|
||||
page.click_button "Save"
|
||||
end
|
||||
|
||||
wait_for_reload
|
||||
planning_page.expect_sprint_names_in_order("Changed name", second_sprint.name)
|
||||
end
|
||||
|
||||
context "when lacking the 'manage_sprint_items' permission" do
|
||||
let(:permissions) { all_permissions - %i[manage_sprint_items] }
|
||||
|
||||
it "has no menu entry for creating a new story" do
|
||||
planning_page.within_sprint_menu(first_sprint) do |menu|
|
||||
expect(menu).to have_selector :menuitem, count: 1
|
||||
expect(menu).to have_selector :menuitem, "Edit sprint"
|
||||
|
||||
expect(menu).to have_no_selector :menuitem, "Add work package"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "validations" do
|
||||
context "when sprint status is active" do
|
||||
before { first_sprint.update!(status: "active") }
|
||||
|
||||
it "validates required fields are present" do
|
||||
planning_page.click_in_sprint_menu(first_sprint, "Edit sprint")
|
||||
planning_page.expect_sprint_dialog
|
||||
|
||||
within_dialog "Edit sprint" do
|
||||
page.fill_in "Sprint name", with: ""
|
||||
page.fill_in "Start date", with: ""
|
||||
page.fill_in "Finish date", with: ""
|
||||
|
||||
page.click_button "Save"
|
||||
|
||||
expect(page).to have_field "Sprint name", validation_error: "can't be blank"
|
||||
expect(page).to have_field "Start date", validation_error: "can't be blank"
|
||||
expect(page).to have_field "Finish date", validation_error: "can't be blank"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without the necessary permissions" do
|
||||
let(:permissions) { all_permissions - %i[create_sprints start_complete_sprint] }
|
||||
|
||||
it "is missing the 'new sprint' button" do
|
||||
expect(page).to have_no_button "Create"
|
||||
expect(page).not_to have_test_selector("op-sprints--new-sprint-button")
|
||||
end
|
||||
|
||||
it "has no menu entry for editing a sprint" do
|
||||
planning_page.within_sprint_menu(first_sprint) do |menu|
|
||||
expect(menu).to have_no_selector :menuitem, "Edit sprint"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
require "spec_helper"
|
||||
require_relative "../../support/pages/backlog"
|
||||
|
||||
RSpec.describe "Sprint list", :js, with_flag: { scrum_projects: true } do
|
||||
RSpec.describe "Sprint list", :js do
|
||||
shared_let(:project) { create(:project) }
|
||||
shared_let(:other_project) { create(:project) }
|
||||
shared_let(:user) { create(:user, member_with_permissions: { project => %i[view_sprints view_work_packages] }) }
|
||||
|
||||
@@ -32,10 +32,7 @@ require "spec_helper"
|
||||
require_relative "../../support/pages/backlog"
|
||||
require_relative "../../../../boards/spec/features/support/board_page"
|
||||
|
||||
RSpec.describe "Start and finish sprints",
|
||||
:js,
|
||||
with_ee: %i[board_view],
|
||||
with_flag: { scrum_projects: true } do
|
||||
RSpec.describe "Start and finish sprints", :js do
|
||||
shared_let(:project) do
|
||||
create(:project, enabled_module_names: %i[backlogs work_package_tracking board_view])
|
||||
end
|
||||
@@ -50,14 +47,13 @@ RSpec.describe "Start and finish sprints",
|
||||
create(:user, member_with_permissions: { project => permissions })
|
||||
end
|
||||
let(:planning_page) { Pages::Backlog.new(project) }
|
||||
let(:task_statuses) { Type.find(Task.type).statuses }
|
||||
let(:story_type) { create(:type_feature) }
|
||||
let(:task_type) do
|
||||
type = create(:type_task)
|
||||
project.types << type
|
||||
|
||||
type
|
||||
end
|
||||
let(:task_statuses) { task_type.statuses }
|
||||
let!(:first_sprint) do
|
||||
create(:agile_sprint,
|
||||
project:,
|
||||
@@ -81,10 +77,6 @@ RSpec.describe "Start and finish sprints",
|
||||
before do
|
||||
login_as(user)
|
||||
|
||||
allow(Setting)
|
||||
.to receive(:plugin_openproject_backlogs)
|
||||
.and_return("story_types" => [story_type.id.to_s], "task_type" => task_type.id.to_s)
|
||||
|
||||
create(:workflow, type: task_type, old_status: default_status, new_status: default_status, role: create(:project_role))
|
||||
|
||||
planning_page.visit!
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
require "spec_helper"
|
||||
require_relative "../../support/pages/backlog"
|
||||
|
||||
RSpec.describe "Show burndown chart", :js, with_flag: { scrum_projects: true } do
|
||||
RSpec.describe "Show burndown chart", :js do
|
||||
include Redmine::I18n
|
||||
|
||||
shared_let(:project) { create(:project, enabled_module_names: %w(backlogs)) }
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
require "spec_helper"
|
||||
require_relative "../support/pages/backlog"
|
||||
|
||||
RSpec.describe "Inbox column in sprint planning view", :js, with_flag: { scrum_projects: true } do
|
||||
RSpec.describe "Inbox column in sprint planning view", :js do
|
||||
let(:sprint_sharing) { nil }
|
||||
let!(:project) do
|
||||
create(:project,
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe "Backlogs project settings sprint sharing", :js, with_flag: { scrum_projects: true } do
|
||||
RSpec.describe "Backlogs project settings sprint sharing", :js do
|
||||
let(:project) { create(:project) }
|
||||
let(:permissions) { %i[create_sprints share_sprint select_done_statuses] }
|
||||
|
||||
@@ -141,13 +141,4 @@ RSpec.describe "Backlogs project settings sprint sharing", :js, with_flag: { scr
|
||||
expect(page).to have_text(I18n.t(:notice_not_authorized))
|
||||
end
|
||||
end
|
||||
|
||||
context "when scrum_projects feature flag is inactive", with_flag: { scrum_projects: false } do
|
||||
it "does not show the sharing tab" do
|
||||
visit project_settings_backlogs_path(project)
|
||||
|
||||
expect(page).to have_heading(I18n.t(:label_backlogs))
|
||||
expect(page).to have_no_link(I18n.t("backlogs.sharing"))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
require "spec_helper"
|
||||
require_relative "../../support/pages/backlog"
|
||||
|
||||
RSpec.describe "Create work package in sprint", :js, with_flag: { scrum_projects: true } do
|
||||
RSpec.describe "Create work package in sprint", :js do
|
||||
let!(:project) do
|
||||
create(:project,
|
||||
types: [type, type2],
|
||||
|
||||
@@ -32,7 +32,7 @@ require "spec_helper"
|
||||
require_relative "../../support/pages/backlog"
|
||||
|
||||
RSpec.describe "Dragging work packages in the inbox",
|
||||
:js, with_flag: { scrum_projects: true } do
|
||||
:js do
|
||||
create_shared_association_defaults_for_work_package_factory
|
||||
|
||||
shared_let(:project) { create(:project) }
|
||||
@@ -49,7 +49,6 @@ RSpec.describe "Dragging work packages in the inbox",
|
||||
view_work_packages
|
||||
edit_work_packages))
|
||||
end
|
||||
# The explicit positioning can be removed once the scrum_projects flag is removed
|
||||
shared_let(:inbox_wp1) { create(:work_package, sprint: nil, project:, position: 1) }
|
||||
shared_let(:inbox_wp2) { create(:work_package, sprint: nil, project:, position: 2) }
|
||||
shared_let(:inbox_wp3) { create(:work_package, sprint: nil, project:, position: 3) }
|
||||
|
||||
@@ -32,7 +32,7 @@ require "spec_helper"
|
||||
require_relative "../../support/pages/backlog"
|
||||
|
||||
RSpec.describe "Dragging work packages in and between sprints",
|
||||
:js, :settings_reset, with_flag: { scrum_projects: true } do
|
||||
:js, :settings_reset do
|
||||
let!(:project) do
|
||||
create(:project,
|
||||
types: [type],
|
||||
|
||||
@@ -113,7 +113,7 @@ RSpec.describe "Filter work packages by backlog filters", :js do
|
||||
end
|
||||
end
|
||||
|
||||
context "on the sprint", with_flag: { scrum_projects: true } do
|
||||
context "on the sprint" do
|
||||
shared_examples_for "filtering on sprints" do
|
||||
it "allows filtering by sprint" do
|
||||
filters.open
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "Sprint displayed and selectable on work package view", :js, with_flag: { scrum_projects: true } do
|
||||
RSpec.describe "Sprint displayed and selectable on work package view", :js do
|
||||
shared_let(:project) { create(:project) }
|
||||
shared_let(:sprint) { create(:agile_sprint, project:) }
|
||||
shared_let(:other_sprint) { create(:agile_sprint, project:) }
|
||||
@@ -55,14 +55,6 @@ RSpec.describe "Sprint displayed and selectable on work package view", :js, with
|
||||
wp_page.expect_attributes sprint: other_sprint.name
|
||||
end
|
||||
|
||||
context "with the feature flag disabled", with_flag: { scrum_projects: false } do
|
||||
it "does not show a sprints property" do
|
||||
wp_page.visit!
|
||||
|
||||
wp_page.expect_no_attribute "Sprint"
|
||||
end
|
||||
end
|
||||
|
||||
context "when lacking the permission to see sprints" do
|
||||
let(:permissions) { %i(view_work_packages) }
|
||||
|
||||
|
||||
+4
-40
@@ -30,7 +30,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter, with_flag: { scrum_projects: true } do
|
||||
RSpec.describe API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
|
||||
include API::V3::Utilities::PathHelper
|
||||
|
||||
let(:custom_field) { build(:custom_field) }
|
||||
@@ -77,17 +77,7 @@ RSpec.describe API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter, with
|
||||
end
|
||||
end
|
||||
|
||||
context "when not a story with the feature flag inactive", with_flag: { scrum_projects: false } do
|
||||
before do
|
||||
allow(schema.type).to receive(:story?).and_return(false)
|
||||
end
|
||||
|
||||
it "does not show story points" do
|
||||
expect(subject).not_to have_json_path("storyPoints")
|
||||
end
|
||||
end
|
||||
|
||||
context "when not a story with the feature flag active" do
|
||||
context "when not a story" do
|
||||
before do
|
||||
allow(schema.type).to receive(:story?).and_return(false)
|
||||
end
|
||||
@@ -121,17 +111,7 @@ RSpec.describe API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter, with
|
||||
end
|
||||
end
|
||||
|
||||
context "when not a story with the feature flag inactive", with_flag: { scrum_projects: false } do
|
||||
before do
|
||||
allow(schema.type).to receive(:story?).and_return(false)
|
||||
end
|
||||
|
||||
it "does not show position" do
|
||||
expect(subject).not_to have_json_path("position")
|
||||
end
|
||||
end
|
||||
|
||||
context "when not a story with the feature flag active" do
|
||||
context "when not a story" do
|
||||
before do
|
||||
allow(schema.type).to receive(:story?).and_return(false)
|
||||
end
|
||||
@@ -181,23 +161,7 @@ RSpec.describe API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter, with
|
||||
end
|
||||
end
|
||||
|
||||
context "when the feature flag is disabled", with_flag: { scrum_projects: false } do
|
||||
it "has no reference to the sprint" do
|
||||
expect(subject).not_to have_json_path(path)
|
||||
end
|
||||
end
|
||||
|
||||
context "when not a story with the feature flag inactive", with_flag: { scrum_projects: false } do
|
||||
before do
|
||||
allow(schema.type).to receive(:story?).and_return(false)
|
||||
end
|
||||
|
||||
it "does not show sprint" do
|
||||
expect(subject).not_to have_json_path("sprint")
|
||||
end
|
||||
end
|
||||
|
||||
context "when not a story with the feature flag active" do
|
||||
context "when not a story" do
|
||||
before do
|
||||
allow(schema.type).to receive(:story?).and_return(false)
|
||||
end
|
||||
|
||||
+9
-91
@@ -43,9 +43,7 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter, "rendering" do
|
||||
position:,
|
||||
sprint:)
|
||||
end
|
||||
let(:type) { story_type }
|
||||
let(:story_type) { build_stubbed(:type) }
|
||||
let(:task_type) { build_stubbed(:type) }
|
||||
let(:type) { build_stubbed(:type) }
|
||||
let(:enabled_module_names) { %w[backlogs] }
|
||||
let(:project) do
|
||||
build_stubbed(:project, enabled_module_names:)
|
||||
@@ -81,30 +79,8 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter, "rendering" do
|
||||
|
||||
describe "properties" do
|
||||
describe "storyPoints" do
|
||||
context "when it is a story (without the feature flag on)", with_flag: { scrum_projects: false } do
|
||||
it_behaves_like "property", :storyPoints do
|
||||
let(:value) { story_points }
|
||||
end
|
||||
end
|
||||
|
||||
context "when it is a story (with the feature flag on)", with_flag: { scrum_projects: true } do
|
||||
it_behaves_like "property", :storyPoints do
|
||||
let(:value) { story_points }
|
||||
end
|
||||
end
|
||||
|
||||
context "when it is a task (without the feature flag on)", with_flag: { scrum_projects: false } do
|
||||
let(:type) { task_type }
|
||||
|
||||
it_behaves_like "no property", :storyPoints
|
||||
end
|
||||
|
||||
context "when it is a task (with the feature flag on)", with_flag: { scrum_projects: true } do
|
||||
let(:type) { task_type }
|
||||
|
||||
it_behaves_like "property", :storyPoints do
|
||||
let(:value) { story_points }
|
||||
end
|
||||
it_behaves_like "property", :storyPoints do
|
||||
let(:value) { story_points }
|
||||
end
|
||||
|
||||
context "when backlogs is disabled" do
|
||||
@@ -115,30 +91,8 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter, "rendering" do
|
||||
end
|
||||
|
||||
describe "position" do
|
||||
context "when it is a story (without the feature flag on)", with_flag: { scrum_projects: false } do
|
||||
it_behaves_like "property", :position do
|
||||
let(:value) { position }
|
||||
end
|
||||
end
|
||||
|
||||
context "when it is a story (with the feature flag on)", with_flag: { scrum_projects: true } do
|
||||
it_behaves_like "property", :position do
|
||||
let(:value) { position }
|
||||
end
|
||||
end
|
||||
|
||||
context "when it is a task (with the feature flag on)", with_flag: { scrum_projects: true } do
|
||||
let(:type) { task_type }
|
||||
|
||||
it_behaves_like "property", :position do
|
||||
let(:value) { position }
|
||||
end
|
||||
end
|
||||
|
||||
context "when it is a task (without the feature flag on)", with_flag: { scrum_projects: false } do
|
||||
let(:type) { task_type }
|
||||
|
||||
it_behaves_like "no property", :position
|
||||
it_behaves_like "property", :position do
|
||||
let(:value) { position }
|
||||
end
|
||||
|
||||
context "when backlogs is disabled" do
|
||||
@@ -150,14 +104,12 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter, "rendering" do
|
||||
end
|
||||
|
||||
describe "links" do
|
||||
describe "sprint", with_flag: { scrum_projects: true } do
|
||||
describe "sprint" do
|
||||
let(:link) { "sprint" }
|
||||
let(:href) { api_v3_paths.sprint(sprint.id) }
|
||||
let(:title) { sprint.name }
|
||||
|
||||
context "when it is a story" do
|
||||
it_behaves_like "has a titled link"
|
||||
end
|
||||
it_behaves_like "has a titled link"
|
||||
|
||||
context "when lacking the permission" do
|
||||
let(:permissions) { [] }
|
||||
@@ -165,22 +117,6 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter, "rendering" do
|
||||
it_behaves_like "has no link"
|
||||
end
|
||||
|
||||
context "when the feature flag is inactive", with_flag: { scrum_projects: false } do
|
||||
it_behaves_like "has no link"
|
||||
end
|
||||
|
||||
context "when it is a task with the feature flag off", with_flag: { scrum_projects: false } do
|
||||
let(:type) { task_type }
|
||||
|
||||
it_behaves_like "has no link"
|
||||
end
|
||||
|
||||
context "when it is a task with the feature flag on" do
|
||||
let(:type) { task_type }
|
||||
|
||||
it_behaves_like "has a titled link"
|
||||
end
|
||||
|
||||
context "when the project is empty (because the work package is not persisted yet)" do
|
||||
let(:project) { nil }
|
||||
|
||||
@@ -206,36 +142,18 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter, "rendering" do
|
||||
end
|
||||
|
||||
describe "embedded" do
|
||||
describe "sprint", with_flag: { scrum_projects: true } do
|
||||
describe "sprint" do
|
||||
let(:embedded_path) { "_embedded/sprint" }
|
||||
let(:embedded_resource) { sprint }
|
||||
let(:embedded_resource_type) { "Sprint" }
|
||||
|
||||
context "when it is a story" do
|
||||
it_behaves_like "has the resource embedded"
|
||||
end
|
||||
it_behaves_like "has the resource embedded"
|
||||
|
||||
context "when lacking the permission" do
|
||||
let(:permissions) { [] }
|
||||
|
||||
it_behaves_like "has the resource not embedded"
|
||||
end
|
||||
|
||||
context "when the feature flag is inactive", with_flag: { scrum_projects: false } do
|
||||
it_behaves_like "has the resource not embedded"
|
||||
end
|
||||
|
||||
context "when it is a type with the feature flag off", with_flag: { scrum_projects: false } do
|
||||
let(:type) { task_type }
|
||||
|
||||
it_behaves_like "has the resource not embedded"
|
||||
end
|
||||
|
||||
context "when it is a type with the feature flag on" do
|
||||
let(:type) { task_type }
|
||||
|
||||
it_behaves_like "has the resource embedded"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,13 +70,7 @@ RSpec.describe OpenProject::AccessControl, "Backlogs module permissions" do # ru
|
||||
expect(subject.controller_actions).to include("rb_sprints/start", "rb_sprints/finish")
|
||||
end
|
||||
|
||||
context "when scrum_projects feature flag is active", with_flag: { scrum_projects: true } do
|
||||
it { is_expected.to be_visible }
|
||||
end
|
||||
|
||||
context "when scrum_projects feature flag is inactive", with_flag: { scrum_projects: false } do
|
||||
it { is_expected.to be_hidden }
|
||||
end
|
||||
it { is_expected.to be_visible }
|
||||
end
|
||||
|
||||
describe "share_sprint" do
|
||||
@@ -86,12 +80,6 @@ RSpec.describe OpenProject::AccessControl, "Backlogs module permissions" do # ru
|
||||
expect(subject.dependencies).to contain_exactly(:create_sprints)
|
||||
end
|
||||
|
||||
context "when scrum_projects feature flag is active", with_flag: { scrum_projects: true } do
|
||||
it { is_expected.to be_visible }
|
||||
end
|
||||
|
||||
context "when scrum_projects feature flag is inactive", with_flag: { scrum_projects: false } do
|
||||
it { is_expected.to be_hidden }
|
||||
end
|
||||
it { is_expected.to be_visible }
|
||||
end
|
||||
end
|
||||
|
||||
@@ -55,195 +55,123 @@ RSpec.describe Burndown do
|
||||
let(:issue_open) { create(:status, name: "status 1", is_default: true) }
|
||||
let(:issue_closed) { create(:status, name: "status 2", is_closed: true) }
|
||||
let(:issue_resolved) { create(:status, name: "status 3", is_closed: false) }
|
||||
let(:sprint) { create(:agile_sprint, project:) }
|
||||
|
||||
current_user { create(:user, member_with_roles: { project => role }) }
|
||||
|
||||
subject(:burndown) { described_class.new(sprint, project) }
|
||||
|
||||
describe "for a version sprint" do
|
||||
let(:version) { create(:version, project:) }
|
||||
let(:sprint) { Sprint.find(version.id) }
|
||||
|
||||
before do
|
||||
allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ "points_burn_direction" => "down",
|
||||
"wiki_template" => "",
|
||||
"story_types" => [type_feature.id.to_s],
|
||||
"task_type" => type_task.id.to_s })
|
||||
describe "WITH the today date fixed to April 4th, 2011 and having a 10 (working days) sprint" do
|
||||
around do |example|
|
||||
travel_to(Time.utc(2011, "apr", 4, 20, 15, 1)) { example.run }
|
||||
end
|
||||
|
||||
describe "WITH the today date fixed to April 4th, 2011 and having a 10 (working days) sprint" do
|
||||
around do |example|
|
||||
travel_to(Time.utc(2011, "apr", 4, 20, 15, 1)) { example.run }
|
||||
describe "WITH having a sprint in the future" do
|
||||
before do
|
||||
sprint.start_date = Time.zone.today + 1.day
|
||||
sprint.finish_date = Time.zone.today + 6.days
|
||||
sprint.save!
|
||||
end
|
||||
|
||||
describe "WITH having a version in the future" do
|
||||
before do
|
||||
version.start_date = Time.zone.today + 1.day
|
||||
version.effective_date = Time.zone.today + 6.days
|
||||
version.save!
|
||||
end
|
||||
|
||||
it "generates an empty burndown" do
|
||||
expect(burndown.series[:story_points]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH having a 10 (working days) sprint and being 5 (working) days into it" do
|
||||
before do
|
||||
version.start_date = Time.zone.today - 7.days
|
||||
version.effective_date = Time.zone.today + 6.days
|
||||
version.save!
|
||||
end
|
||||
|
||||
describe "WITH 1 story assigned to the sprint" do
|
||||
let(:story) do
|
||||
build(:story, subject: "Story 1",
|
||||
project:,
|
||||
version:,
|
||||
type: type_feature,
|
||||
status: issue_open,
|
||||
priority: issue_priority,
|
||||
created_at: Time.zone.today - 20.days,
|
||||
updated_at: Time.zone.today - 20.days)
|
||||
end
|
||||
|
||||
describe "WITH the story having story_point defined on creation" do
|
||||
before do
|
||||
story.story_points = 9
|
||||
story.save!
|
||||
story.last_journal.update_columns(created_at: story.created_at, updated_at: story.created_at)
|
||||
end
|
||||
|
||||
describe "WITH the story being closed and opened again within the sprint duration" do
|
||||
before do
|
||||
set_attribute_journalized story, :status_id=, issue_closed.id, 6.days.ago
|
||||
set_attribute_journalized story, :status_id=, issue_open.id, 3.days.ago
|
||||
end
|
||||
|
||||
it { expect(burndown.story_points).to eql [9.0, 0.0, 0.0, 0.0, 9.0, 9.0] }
|
||||
it { expect(burndown.story_points.unit).to be :points }
|
||||
it { expect(burndown.days).to eql(sprint.days) }
|
||||
it { expect(burndown.max[:hours]).to be 0.0 }
|
||||
it { expect(burndown.max[:points]).to be 9.0 }
|
||||
it { expect(burndown.story_points_ideal).to eql [9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0] }
|
||||
end
|
||||
|
||||
describe "WITH the story marked as resolved and consequently 'done'" do
|
||||
before do
|
||||
set_attribute_journalized story, :status_id=, issue_resolved.id, 6.days.ago
|
||||
set_attribute_journalized story, :status_id=, issue_open.id, 3.days.ago
|
||||
project.done_statuses << issue_resolved
|
||||
end
|
||||
|
||||
it { expect(story.done?).to be false }
|
||||
it { expect(burndown.story_points).to eql [9.0, 0.0, 0.0, 0.0, 9.0, 9.0] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH 10 stories assigned to the sprint" do
|
||||
let!(:stories) do
|
||||
stories = []
|
||||
|
||||
10.times do |i|
|
||||
stories[i] = create(:story, subject: "Story #{i}",
|
||||
project:,
|
||||
version:,
|
||||
type: type_feature,
|
||||
status: issue_open,
|
||||
priority: issue_priority,
|
||||
created_at: Time.zone.today - (20 - i).days,
|
||||
updated_at: Time.zone.today - (20 - i).days)
|
||||
stories[i].last_journal.update_columns(created_at: stories[i].created_at,
|
||||
updated_at: stories[i].created_at,
|
||||
validity_period: stories[i].created_at..Float::INFINITY)
|
||||
end
|
||||
|
||||
stories
|
||||
end
|
||||
|
||||
describe "WITH each story having story points defined at start" do
|
||||
before do
|
||||
stories.each_with_index do |s, _i|
|
||||
set_attribute_journalized s, :story_points=, 10, version.start_date - 3.days
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH 5 stories having been reduced to 0 story points, one story per day" do
|
||||
before do
|
||||
5.times do |i|
|
||||
set_attribute_journalized stories[i], :story_points=, nil, version.start_date + i.days + 1.hour
|
||||
end
|
||||
end
|
||||
|
||||
describe "THEN" do
|
||||
it { expect(burndown.story_points).to eql [90.0, 80.0, 70.0, 60.0, 50.0, 50.0] }
|
||||
it { expect(burndown.story_points.unit).to be :points }
|
||||
it { expect(burndown.days).to eql(sprint.days) }
|
||||
it { expect(burndown.max[:hours]).to be 0.0 }
|
||||
it { expect(burndown.max[:points]).to be 90.0 }
|
||||
it { expect(burndown.story_points_ideal).to eql [90.0, 80.0, 70.0, 60.0, 50.0, 40.0, 30.0, 20.0, 10.0, 0.0] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
it "generates an empty burndown" do
|
||||
expect(burndown.series[:story_points]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "for an agile sprint" do
|
||||
let(:sprint) { create(:agile_sprint, project:) }
|
||||
|
||||
describe "WITH the today date fixed to April 4th, 2011 and having a 10 (working days) sprint" do
|
||||
around do |example|
|
||||
travel_to(Time.utc(2011, "apr", 4, 20, 15, 1)) { example.run }
|
||||
describe "WITH having a 10 (working days) sprint and being 5 (working) days into it" do
|
||||
before do
|
||||
sprint.start_date = Time.zone.today - 7.days
|
||||
sprint.finish_date = Time.zone.today + 6.days
|
||||
sprint.save!
|
||||
end
|
||||
|
||||
describe "WITH having a sprint in the future" do
|
||||
before do
|
||||
sprint.start_date = Time.zone.today + 1.day
|
||||
sprint.finish_date = Time.zone.today + 6.days
|
||||
sprint.save!
|
||||
describe "WITH 1 story assigned to the sprint" do
|
||||
let(:story) do
|
||||
build(:story, subject: "Story 1",
|
||||
project:,
|
||||
sprint:,
|
||||
type: type_feature,
|
||||
status: issue_open,
|
||||
priority: issue_priority,
|
||||
created_at: Time.zone.today - 20.days,
|
||||
updated_at: Time.zone.today - 20.days)
|
||||
end
|
||||
|
||||
it "generates an empty burndown" do
|
||||
expect(burndown.series[:story_points]).to be_empty
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH having a 10 (working days) sprint and being 5 (working) days into it" do
|
||||
before do
|
||||
sprint.start_date = Time.zone.today - 7.days
|
||||
sprint.finish_date = Time.zone.today + 6.days
|
||||
sprint.save!
|
||||
end
|
||||
|
||||
describe "WITH 1 story assigned to the sprint" do
|
||||
let(:story) do
|
||||
build(:story, subject: "Story 1",
|
||||
project:,
|
||||
sprint:,
|
||||
type: type_feature,
|
||||
status: issue_open,
|
||||
priority: issue_priority,
|
||||
created_at: Time.zone.today - 20.days,
|
||||
updated_at: Time.zone.today - 20.days)
|
||||
describe "WITH the story having story_point defined on creation" do
|
||||
before do
|
||||
story.story_points = 9
|
||||
story.save!
|
||||
story.last_journal.update_columns(created_at: story.created_at, updated_at: story.created_at)
|
||||
end
|
||||
|
||||
describe "WITH the story having story_point defined on creation" do
|
||||
describe "WITH the story being closed and opened again within the sprint duration" do
|
||||
before do
|
||||
story.story_points = 9
|
||||
story.save!
|
||||
story.last_journal.update_columns(created_at: story.created_at, updated_at: story.created_at)
|
||||
set_attribute_journalized story, :status_id=, issue_closed.id, 6.days.ago
|
||||
set_attribute_journalized story, :status_id=, issue_open.id, 3.days.ago
|
||||
end
|
||||
|
||||
describe "WITH the story being closed and opened again within the sprint duration" do
|
||||
before do
|
||||
set_attribute_journalized story, :status_id=, issue_closed.id, 6.days.ago
|
||||
set_attribute_journalized story, :status_id=, issue_open.id, 3.days.ago
|
||||
end
|
||||
it { expect(burndown.story_points).to eql [9.0, 0.0, 0.0, 0.0, 9.0, 9.0] }
|
||||
it { expect(burndown.story_points.unit).to be :points }
|
||||
|
||||
it { expect(burndown.story_points).to eql [9.0, 0.0, 0.0, 0.0, 9.0, 9.0] }
|
||||
it {
|
||||
expect(burndown.days).to eql(Day.working.from_range(from: sprint.start_date,
|
||||
to: sprint.finish_date).map(&:date))
|
||||
}
|
||||
|
||||
it { expect(burndown.max[:points]).to be 9.0 }
|
||||
it { expect(burndown.story_points_ideal).to eql [9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0] }
|
||||
end
|
||||
|
||||
describe "WITH the story marked as resolved and consequently 'done'" do
|
||||
before do
|
||||
set_attribute_journalized story, :status_id=, issue_resolved.id, 6.days.ago
|
||||
set_attribute_journalized story, :status_id=, issue_open.id, 3.days.ago
|
||||
project.done_statuses << issue_resolved
|
||||
end
|
||||
|
||||
it { expect(story.done?).to be false }
|
||||
it { expect(burndown.story_points).to eql [9.0, 0.0, 0.0, 0.0, 9.0, 9.0] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH 10 stories assigned to the sprint" do
|
||||
let!(:stories) do
|
||||
stories = []
|
||||
|
||||
10.times do |i|
|
||||
stories[i] = create(:story, subject: "Story #{i}",
|
||||
project:,
|
||||
sprint:,
|
||||
type: type_feature,
|
||||
status: issue_open,
|
||||
priority: issue_priority,
|
||||
created_at: Time.zone.today - (20 - i).days,
|
||||
updated_at: Time.zone.today - (20 - i).days)
|
||||
stories[i].last_journal.update_columns(created_at: stories[i].created_at,
|
||||
updated_at: stories[i].created_at,
|
||||
validity_period: stories[i].created_at..Float::INFINITY)
|
||||
end
|
||||
|
||||
stories
|
||||
end
|
||||
|
||||
describe "WITH each story having story points defined at start" do
|
||||
before do
|
||||
stories.each do |s|
|
||||
set_attribute_journalized s, :story_points=, 10, sprint.start_date - 3.days
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH 5 stories having been reduced to 0 story points, one story per day" do
|
||||
before do
|
||||
5.times do |i|
|
||||
set_attribute_journalized stories[i], :story_points=, nil, sprint.start_date + i.days + 1.hour
|
||||
end
|
||||
end
|
||||
|
||||
describe "THEN" do
|
||||
it { expect(burndown.story_points).to eql [90.0, 80.0, 70.0, 60.0, 50.0, 50.0] }
|
||||
it { expect(burndown.story_points.unit).to be :points }
|
||||
|
||||
it {
|
||||
@@ -251,94 +179,32 @@ RSpec.describe Burndown do
|
||||
to: sprint.finish_date).map(&:date))
|
||||
}
|
||||
|
||||
it { expect(burndown.max[:points]).to be 9.0 }
|
||||
it { expect(burndown.story_points_ideal).to eql [9.0, 8.0, 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0, 0.0] }
|
||||
end
|
||||
|
||||
describe "WITH the story marked as resolved and consequently 'done'" do
|
||||
before do
|
||||
set_attribute_journalized story, :status_id=, issue_resolved.id, 6.days.ago
|
||||
set_attribute_journalized story, :status_id=, issue_open.id, 3.days.ago
|
||||
project.done_statuses << issue_resolved
|
||||
end
|
||||
|
||||
it { expect(story.done?).to be false }
|
||||
it { expect(burndown.story_points).to eql [9.0, 0.0, 0.0, 0.0, 9.0, 9.0] }
|
||||
it { expect(burndown.max[:points]).to be 90.0 }
|
||||
it { expect(burndown.story_points_ideal).to eql [90.0, 80.0, 70.0, 60.0, 50.0, 40.0, 30.0, 20.0, 10.0, 0.0] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH 10 stories assigned to the sprint" do
|
||||
let!(:stories) do
|
||||
stories = []
|
||||
|
||||
10.times do |i|
|
||||
stories[i] = create(:story, subject: "Story #{i}",
|
||||
project:,
|
||||
sprint:,
|
||||
type: type_feature,
|
||||
status: issue_open,
|
||||
priority: issue_priority,
|
||||
created_at: Time.zone.today - (20 - i).days,
|
||||
updated_at: Time.zone.today - (20 - i).days)
|
||||
stories[i].last_journal.update_columns(created_at: stories[i].created_at,
|
||||
updated_at: stories[i].created_at,
|
||||
validity_period: stories[i].created_at..Float::INFINITY)
|
||||
end
|
||||
|
||||
stories
|
||||
end
|
||||
|
||||
describe "WITH each story having story points defined at start" do
|
||||
before do
|
||||
stories.each do |s|
|
||||
set_attribute_journalized s, :story_points=, 10, sprint.start_date - 3.days
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH 5 stories having been reduced to 0 story points, one story per day" do
|
||||
before do
|
||||
5.times do |i|
|
||||
set_attribute_journalized stories[i], :story_points=, nil, sprint.start_date + i.days + 1.hour
|
||||
end
|
||||
end
|
||||
|
||||
describe "THEN" do
|
||||
it { expect(burndown.story_points).to eql [90.0, 80.0, 70.0, 60.0, 50.0, 50.0] }
|
||||
it { expect(burndown.story_points.unit).to be :points }
|
||||
|
||||
it {
|
||||
expect(burndown.days).to eql(Day.working.from_range(from: sprint.start_date,
|
||||
to: sprint.finish_date).map(&:date))
|
||||
}
|
||||
|
||||
it { expect(burndown.max[:points]).to be 90.0 }
|
||||
it { expect(burndown.story_points_ideal).to eql [90.0, 80.0, 70.0, 60.0, 50.0, 40.0, 30.0, 20.0, 10.0, 0.0] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without dates on the sprint" do
|
||||
let(:sprint) { create(:agile_sprint, project:, start_date: nil, finish_date: nil) }
|
||||
let(:story) do
|
||||
build(:story,
|
||||
:created_in_past,
|
||||
subject: "Story 1",
|
||||
project:,
|
||||
sprint:,
|
||||
type: type_feature,
|
||||
status: issue_open,
|
||||
priority: issue_priority,
|
||||
created_at: Time.zone.today - 20.days,
|
||||
updated_at: Time.zone.today - 20.days)
|
||||
end
|
||||
|
||||
it "generates an empty burndown" do
|
||||
expect(burndown.series[:story_points]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "without dates on the sprint" do
|
||||
let(:sprint) { create(:agile_sprint, project:, start_date: nil, finish_date: nil) }
|
||||
let(:story) do
|
||||
build(:story,
|
||||
:created_in_past,
|
||||
subject: "Story 1",
|
||||
project:,
|
||||
sprint:,
|
||||
type: type_feature,
|
||||
status: issue_open,
|
||||
priority: issue_priority,
|
||||
created_at: Time.zone.today - 20.days,
|
||||
updated_at: Time.zone.today - 20.days)
|
||||
end
|
||||
|
||||
it "generates an empty burndown" do
|
||||
expect(burndown.series[:story_points]).to be_empty
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,424 +0,0 @@
|
||||
#-- 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.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe WorkPackage do
|
||||
describe "Story positions" do
|
||||
def build_work_package(options)
|
||||
build(:work_package, options.reverse_merge(version_id: sprint_1.id,
|
||||
priority_id: priority.id,
|
||||
project_id: project.id,
|
||||
status_id: status.id,
|
||||
type_id: story_type.id))
|
||||
end
|
||||
|
||||
def create_work_package(options)
|
||||
build_work_package(options).tap(&:save!)
|
||||
end
|
||||
|
||||
let(:status) { create(:status) }
|
||||
let(:priority) { create(:priority_normal) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
let(:story_type) { create(:type, name: "Story") }
|
||||
let(:epic_type) { create(:type, name: "Epic") }
|
||||
let(:task_type) { create(:type, name: "Task") }
|
||||
let(:other_type) { create(:type, name: "Feedback") }
|
||||
|
||||
let(:sprint_1) { create(:version, project_id: project.id, name: "Sprint 1") }
|
||||
let(:sprint_2) { create(:version, project_id: project.id, name: "Sprint 2") }
|
||||
|
||||
let(:work_package_1) { create_work_package(subject: "WorkPackage 1", version_id: sprint_1.id) }
|
||||
let(:work_package_2) { create_work_package(subject: "WorkPackage 2", version_id: sprint_1.id) }
|
||||
let(:work_package_3) { create_work_package(subject: "WorkPackage 3", version_id: sprint_1.id) }
|
||||
let(:work_package_4) { create_work_package(subject: "WorkPackage 4", version_id: sprint_1.id) }
|
||||
let(:work_package_5) { create_work_package(subject: "WorkPackage 5", version_id: sprint_1.id) }
|
||||
|
||||
let(:work_package_a) { create_work_package(subject: "WorkPackage a", version_id: sprint_2.id) }
|
||||
let(:work_package_b) { create_work_package(subject: "WorkPackage b", version_id: sprint_2.id) }
|
||||
let(:work_package_c) { create_work_package(subject: "WorkPackage c", version_id: sprint_2.id) }
|
||||
|
||||
let(:feedback_1) do
|
||||
create_work_package(subject: "Feedback 1", version_id: sprint_1.id,
|
||||
type_id: other_type.id)
|
||||
end
|
||||
|
||||
let(:task_1) do
|
||||
create_work_package(subject: "Task 1", version_id: sprint_1.id,
|
||||
type_id: task_type.id)
|
||||
end
|
||||
|
||||
before do
|
||||
# We had problems while writing these specs, that some elements kept
|
||||
# creaping around between tests. This should be fast enough to not harm
|
||||
# anybody while adding an additional safety net to make sure, that
|
||||
# everything runs in isolation.
|
||||
WorkPackage.delete_all
|
||||
IssuePriority.delete_all
|
||||
Status.delete_all
|
||||
Project.delete_all
|
||||
Type.delete_all
|
||||
Version.delete_all
|
||||
|
||||
# Enable and configure backlogs
|
||||
project.enabled_module_names = project.enabled_module_names + ["backlogs"]
|
||||
allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ "story_types" => [story_type.id, epic_type.id],
|
||||
"task_type" => task_type.id })
|
||||
|
||||
# Otherwise the type id's from the previous test are still active
|
||||
WorkPackage.instance_variable_set(:@backlogs_types, nil)
|
||||
|
||||
project.types = [story_type, epic_type, task_type, other_type]
|
||||
sprint_1
|
||||
sprint_2
|
||||
|
||||
# Create and order work_packages
|
||||
work_package_1.move_to_bottom
|
||||
work_package_2.move_to_bottom
|
||||
work_package_3.move_to_bottom
|
||||
work_package_4.move_to_bottom
|
||||
work_package_5.move_to_bottom
|
||||
|
||||
work_package_a.move_to_bottom
|
||||
work_package_b.move_to_bottom
|
||||
work_package_c.move_to_bottom
|
||||
end
|
||||
|
||||
describe "- Creating a work_package in a sprint" do
|
||||
it "adds it to the bottom of the list" do
|
||||
new_work_package = create_work_package(subject: "Newest WorkPackage", version_id: sprint_1.id)
|
||||
|
||||
expect(new_work_package).not_to be_new_record
|
||||
expect(new_work_package).to be_last
|
||||
end
|
||||
|
||||
it "does not reorder the existing work_packages" do
|
||||
new_work_package = create_work_package(subject: "Newest WorkPackage", version_id: sprint_1.id)
|
||||
|
||||
expect([work_package_1, work_package_2, work_package_3, work_package_4,
|
||||
work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4, 5])
|
||||
end
|
||||
end
|
||||
|
||||
describe "- Removing a work_package from the sprint" do
|
||||
it "reorders the remaining work_packages" do
|
||||
work_package_2.version = sprint_2
|
||||
work_package_2.save!
|
||||
|
||||
expect(sprint_1.work_packages.order(Arel.sql("id"))).to eq([work_package_1, work_package_3, work_package_4,
|
||||
work_package_5])
|
||||
expect(sprint_1.work_packages.order(Arel.sql("id")).each(&:reload).map(&:position)).to eq([1, 2, 3, 4])
|
||||
end
|
||||
end
|
||||
|
||||
describe "- Adding a work_package to a sprint" do
|
||||
it "adds it to the bottom of the list" do
|
||||
work_package_a.version = sprint_1
|
||||
work_package_a.save!
|
||||
|
||||
expect(work_package_a).to be_last
|
||||
end
|
||||
|
||||
it "does not reorder the existing work_packages" do
|
||||
work_package_a.version = sprint_1
|
||||
work_package_a.save!
|
||||
|
||||
expect([work_package_1, work_package_2, work_package_3, work_package_4,
|
||||
work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4, 5])
|
||||
end
|
||||
end
|
||||
|
||||
describe "- Deleting a work_package in a sprint" do
|
||||
it "reorders the existing work_packages" do
|
||||
work_package_3.destroy
|
||||
|
||||
expect([work_package_1, work_package_2, work_package_4,
|
||||
work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4])
|
||||
end
|
||||
end
|
||||
|
||||
describe "- Changing the type" do
|
||||
describe "by moving a story to another story type" do
|
||||
it "keeps all positions in the sprint in tact" do
|
||||
work_package_3.type = epic_type
|
||||
work_package_3.save!
|
||||
|
||||
expect([work_package_1, work_package_2, work_package_3, work_package_4,
|
||||
work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4, 5])
|
||||
end
|
||||
end
|
||||
|
||||
describe "by moving a story to a non-backlogs type" do
|
||||
it "removes it from any list" do
|
||||
work_package_3.type = other_type
|
||||
work_package_3.save!
|
||||
|
||||
expect(work_package_3).not_to be_in_list
|
||||
end
|
||||
|
||||
it "reorders the remaining stories" do
|
||||
work_package_3.type = other_type
|
||||
work_package_3.save!
|
||||
|
||||
expect([work_package_1, work_package_2, work_package_4,
|
||||
work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4])
|
||||
end
|
||||
end
|
||||
|
||||
describe "by moving a story to the task type" do
|
||||
it "removes it from any list" do
|
||||
work_package_3.type = task_type
|
||||
work_package_3.save!
|
||||
|
||||
expect(work_package_3).not_to be_in_list
|
||||
end
|
||||
|
||||
it "reorders the remaining stories" do
|
||||
work_package_3.type = task_type
|
||||
work_package_3.save!
|
||||
|
||||
expect([work_package_1, work_package_2, work_package_4,
|
||||
work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4])
|
||||
end
|
||||
end
|
||||
|
||||
describe "by moving a task to the story type" do
|
||||
it "adds it to the bottom of the list" do
|
||||
task_1.type = story_type
|
||||
task_1.save!
|
||||
|
||||
expect(task_1).to be_last
|
||||
end
|
||||
|
||||
it "does not reorder the existing stories" do
|
||||
task_1.type = story_type
|
||||
task_1.save!
|
||||
|
||||
expect([work_package_1, work_package_2, work_package_3, work_package_4, work_package_5,
|
||||
task_1].each(&:reload).map(&:position)).to eq([1, 2, 3, 4, 5, 6])
|
||||
end
|
||||
end
|
||||
|
||||
describe "by moving a non-backlogs work_package to a story type" do
|
||||
it "adds it to the bottom of the list" do
|
||||
feedback_1.type = story_type
|
||||
feedback_1.save!
|
||||
|
||||
expect(feedback_1).to be_last
|
||||
end
|
||||
|
||||
it "does not reorder the existing stories" do
|
||||
feedback_1.type = story_type
|
||||
feedback_1.save!
|
||||
|
||||
expect([work_package_1, work_package_2, work_package_3, work_package_4, work_package_5,
|
||||
feedback_1].each(&:reload).map(&:position)).to eq([1, 2, 3, 4, 5, 6])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "- Moving work_packages between projects" do
|
||||
# N.B.: You cannot move a ticket to another project and change the
|
||||
# 'version' at the same time. On the other hand, OpenProject tries
|
||||
# to keep the 'version' if possible (e.g. within project
|
||||
# hierarchies with shared versions)
|
||||
|
||||
let(:project_wo_backlogs) { create(:project) }
|
||||
let(:sub_project_wo_backlogs) { create(:project) }
|
||||
|
||||
let(:shared_sprint) do
|
||||
create(:version,
|
||||
project_id: project.id,
|
||||
name: "Shared Sprint",
|
||||
sharing: "descendants")
|
||||
end
|
||||
|
||||
let(:version_go_live) do
|
||||
create(:version,
|
||||
project_id: project_wo_backlogs.id,
|
||||
name: "Go-Live")
|
||||
end
|
||||
|
||||
shared_let(:admin) { create(:admin) }
|
||||
|
||||
def move_to_project(work_package, project)
|
||||
WorkPackages::UpdateService
|
||||
.new(model: work_package, user: admin)
|
||||
.call(project:)
|
||||
end
|
||||
|
||||
before do
|
||||
project_wo_backlogs.enabled_module_names = project_wo_backlogs.enabled_module_names - ["backlogs"]
|
||||
sub_project_wo_backlogs.enabled_module_names = sub_project_wo_backlogs.enabled_module_names - ["backlogs"]
|
||||
|
||||
project_wo_backlogs.types = [story_type, task_type, other_type]
|
||||
sub_project_wo_backlogs.types = [story_type, task_type, other_type]
|
||||
|
||||
sub_project_wo_backlogs.move_to_child_of(project)
|
||||
|
||||
shared_sprint
|
||||
version_go_live
|
||||
end
|
||||
|
||||
describe "- Moving an work_package from a project without backlogs to a backlogs_enabled project" do
|
||||
describe "if the version may not be kept" do
|
||||
let(:work_package_i) do
|
||||
create_work_package(subject: "WorkPackage I",
|
||||
version_id: version_go_live.id,
|
||||
project_id: project_wo_backlogs.id)
|
||||
end
|
||||
|
||||
before do
|
||||
work_package_i
|
||||
end
|
||||
|
||||
it "sets the version_id to nil" do
|
||||
result = move_to_project(work_package_i, project)
|
||||
|
||||
expect(result).to be_truthy
|
||||
|
||||
expect(work_package_i.version).to be_nil
|
||||
end
|
||||
|
||||
it "removes it from any list" do
|
||||
result = move_to_project(work_package_i, project)
|
||||
|
||||
expect(result).to be_truthy
|
||||
|
||||
expect(work_package_i).not_to be_in_list
|
||||
end
|
||||
end
|
||||
|
||||
describe "if the version may be kept" do
|
||||
let(:work_package_i) do
|
||||
create_work_package(subject: "WorkPackage I",
|
||||
version_id: shared_sprint.id,
|
||||
project_id: sub_project_wo_backlogs.id)
|
||||
end
|
||||
|
||||
before do
|
||||
work_package_i
|
||||
end
|
||||
|
||||
it "keeps the version_id" do
|
||||
result = move_to_project(work_package_i, project)
|
||||
|
||||
expect(result).to be_truthy
|
||||
|
||||
expect(work_package_i.version).to eq(shared_sprint)
|
||||
end
|
||||
|
||||
it "adds it to the bottom of the list" do
|
||||
result = move_to_project(work_package_i, project)
|
||||
|
||||
expect(result).to be_truthy
|
||||
|
||||
expect(work_package_i).to be_first
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "- Moving an work_package away from backlogs_enabled project to a project without backlogs" do
|
||||
describe "if the version may not be kept" do
|
||||
it "sets the version_id to nil" do
|
||||
result = move_to_project(work_package_3, project_wo_backlogs)
|
||||
|
||||
expect(result).to be_truthy
|
||||
|
||||
expect(work_package_3.version).to be_nil
|
||||
end
|
||||
|
||||
it "removes it from any list" do
|
||||
result = move_to_project(work_package_3, sub_project_wo_backlogs)
|
||||
|
||||
expect(result).to be_truthy
|
||||
|
||||
expect(work_package_3).not_to be_in_list
|
||||
end
|
||||
|
||||
it "reorders the remaining work_packages" do
|
||||
result = move_to_project(work_package_3, sub_project_wo_backlogs)
|
||||
|
||||
expect(result).to be_truthy
|
||||
|
||||
expect([work_package_1, work_package_2, work_package_4,
|
||||
work_package_5].each(&:reload).map(&:position)).to eq([1, 2, 3, 4])
|
||||
end
|
||||
end
|
||||
|
||||
describe "if the version may be kept" do
|
||||
let(:work_package_i) do
|
||||
create_work_package(subject: "WorkPackage I",
|
||||
version_id: shared_sprint.id)
|
||||
end
|
||||
let(:work_package_ii) do
|
||||
create_work_package(subject: "WorkPackage II",
|
||||
version_id: shared_sprint.id)
|
||||
end
|
||||
let(:work_package_iii) do
|
||||
create_work_package(subject: "WorkPackage III",
|
||||
version_id: shared_sprint.id)
|
||||
end
|
||||
|
||||
before do
|
||||
work_package_i.move_to_bottom
|
||||
work_package_ii.move_to_bottom
|
||||
work_package_iii.move_to_bottom
|
||||
|
||||
expect([work_package_i, work_package_ii, work_package_iii].map(&:position)).to eq([1, 2, 3])
|
||||
end
|
||||
|
||||
it "keeps the version_id" do
|
||||
result = move_to_project(work_package_ii, sub_project_wo_backlogs)
|
||||
|
||||
expect(result).to be_truthy
|
||||
|
||||
expect(work_package_ii.version).to eq(shared_sprint)
|
||||
end
|
||||
|
||||
it "removes it from any list" do
|
||||
result = move_to_project(work_package_ii, sub_project_wo_backlogs)
|
||||
|
||||
expect(result).to be_truthy
|
||||
|
||||
expect(work_package_ii).not_to be_in_list
|
||||
end
|
||||
|
||||
it "reorders the remaining work_packages" do
|
||||
result = move_to_project(work_package_ii, sub_project_wo_backlogs)
|
||||
|
||||
expect(result).to be_truthy
|
||||
|
||||
expect([work_package_i, work_package_iii].each(&:reload).map(&:position)).to eq([1, 2])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe Queries::WorkPackages::Filter::SprintFilter, with_flag: { scrum_projects: true } do
|
||||
RSpec.describe Queries::WorkPackages::Filter::SprintFilter do
|
||||
let(:scope_class) do
|
||||
Class.new do
|
||||
def for_project(_project); end
|
||||
@@ -78,19 +78,13 @@ RSpec.describe Queries::WorkPackages::Filter::SprintFilter, with_flag: { scrum_p
|
||||
end
|
||||
|
||||
describe "#available?" do
|
||||
context "when in a project, scrum projects is active and user has the permission" do
|
||||
context "when in a project and the user has the permission" do
|
||||
it "is true" do
|
||||
expect(instance).to be_available
|
||||
end
|
||||
end
|
||||
|
||||
context "when in a project, scrum projects is inactive and user has the permission", with_flag: { scrum_projects: false } do
|
||||
it "is false" do
|
||||
expect(instance).not_to be_available
|
||||
end
|
||||
end
|
||||
|
||||
context "when in a project, scrum projects is active and user lacks the permission" do
|
||||
context "when in a project and the user lacks the permission" do
|
||||
let(:project_permissions) { [] }
|
||||
|
||||
it "is false" do
|
||||
@@ -98,7 +92,7 @@ RSpec.describe Queries::WorkPackages::Filter::SprintFilter, with_flag: { scrum_p
|
||||
end
|
||||
end
|
||||
|
||||
context "when outside a project, scrum projects is active and user has the permission" do
|
||||
context "when outside a project and the user has the permission" do
|
||||
let(:project) { nil }
|
||||
|
||||
it "is true" do
|
||||
@@ -106,16 +100,7 @@ RSpec.describe Queries::WorkPackages::Filter::SprintFilter, with_flag: { scrum_p
|
||||
end
|
||||
end
|
||||
|
||||
context "when outside a project, scrum projects is inactive and user has the permission",
|
||||
with_flag: { scrum_projects: false } do
|
||||
let(:project) { nil }
|
||||
|
||||
it "is false" do
|
||||
expect(instance).not_to be_available
|
||||
end
|
||||
end
|
||||
|
||||
context "when outside a project, scrum projects is active and user lacks the permission" do
|
||||
context "when outside a project and the user lacks the permission" do
|
||||
let(:project) { nil }
|
||||
let(:project_permissions) { [] }
|
||||
|
||||
|
||||
@@ -45,70 +45,4 @@ RSpec.describe WorkPackage do
|
||||
expect(ordered_positions).to eq([1, 2, nil])
|
||||
end
|
||||
end
|
||||
|
||||
describe "#backlogs_types" do
|
||||
it "returns all the ids of types that are configures to be considered backlogs types" do
|
||||
allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ "story_types" => [1], "task_type" => 2 })
|
||||
|
||||
expect(described_class.backlogs_types).to contain_exactly(1, 2)
|
||||
end
|
||||
|
||||
it "returns an empty array if nothing is defined" do
|
||||
allow(Setting).to receive(:plugin_openproject_backlogs).and_return({})
|
||||
|
||||
expect(described_class.backlogs_types).to eq([])
|
||||
end
|
||||
|
||||
it "reflects changes to the configuration" do
|
||||
allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ "story_types" => [1], "task_type" => 2 })
|
||||
expect(described_class.backlogs_types).to contain_exactly(1, 2)
|
||||
|
||||
allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ "story_types" => [3], "task_type" => 4 })
|
||||
expect(described_class.backlogs_types).to contain_exactly(3, 4)
|
||||
end
|
||||
end
|
||||
|
||||
describe "#story" do
|
||||
shared_let(:project) { create(:project) }
|
||||
shared_let(:status) { create(:status) }
|
||||
shared_let(:story_type) { create(:type, name: "Story") }
|
||||
shared_let(:task_type) { create(:type, name: "Task") }
|
||||
|
||||
before do
|
||||
allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ "story_types" => [story_type.id],
|
||||
"task_type" => task_type.id })
|
||||
end
|
||||
|
||||
context "for a WorkPackage" do
|
||||
let(:work_package) { build_stubbed(:work_package) }
|
||||
|
||||
it "returns nil" do
|
||||
expect(work_package.story).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context "for a Story" do
|
||||
let(:story) { create(:story, project:, status:, type: story_type) }
|
||||
|
||||
it "returns self" do
|
||||
expect(story.story).to eq(story)
|
||||
end
|
||||
end
|
||||
|
||||
context "for a Task" do
|
||||
let(:parent_parent_story) { create(:story, project:, status:, type: story_type) }
|
||||
let(:parent_story) { create(:story, parent: parent_parent_story, project:, status:, type: story_type) }
|
||||
let(:task) { create(:task, parent: parent_story, project:, status:, type: task_type) }
|
||||
|
||||
it "returns the closest WorkPackage ancestor being a Story" do
|
||||
expect(task.story).to eq(described_class.find(parent_story.id))
|
||||
|
||||
# transform the parent_story into a task
|
||||
parent_story.update(type: task_type)
|
||||
|
||||
# the returned story is now the grand parent
|
||||
expect(task.story).to eq(described_class.find(parent_parent_story.id))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe WorkPackage, "positions", with_flag: { scrum_projects: true } do # rubocop:disable RSpec/SpecFilePathFormat
|
||||
RSpec.describe WorkPackage, "positions" do # rubocop:disable RSpec/SpecFilePathFormat
|
||||
def create_work_package(options)
|
||||
create(:work_package, options.reverse_merge(project:, type_id: type.id))
|
||||
end
|
||||
@@ -40,7 +40,6 @@ RSpec.describe WorkPackage, "positions", with_flag: { scrum_projects: true } do
|
||||
shared_let(:sprint1) { create(:agile_sprint, project:, name: "Sprint 1") }
|
||||
shared_let(:sprint2) { create(:agile_sprint, project:, name: "Sprint 2") }
|
||||
|
||||
# Once the feature flag is removed, those can be changed into shared_let
|
||||
let!(:sprint1_wp1) { create_work_package(subject: "Sprint 1 WorkPackage 1", sprint: sprint1) }
|
||||
let!(:sprint1_wp2) { create_work_package(subject: "Sprint 1 WorkPackage 2", sprint: sprint1) }
|
||||
let!(:sprint1_wp3) { create_work_package(subject: "Sprint 1 WorkPackage 3", sprint: sprint1) }
|
||||
|
||||
@@ -52,7 +52,7 @@ RSpec.describe "API v3 Sprint resource", content_type: :json do
|
||||
})
|
||||
end
|
||||
|
||||
describe "GET /api/v3/sprints", with_flag: :scrum_projects do
|
||||
describe "GET /api/v3/sprints" do
|
||||
let(:get_path) { api_v3_paths.path_for(:sprints, filters:, page_size:, offset:) }
|
||||
let(:filters) { [] }
|
||||
let(:page_size) { nil }
|
||||
@@ -80,10 +80,6 @@ RSpec.describe "API v3 Sprint resource", content_type: :json do
|
||||
it_behaves_like "unauthenticated access"
|
||||
end
|
||||
|
||||
context "when the feature flag is turned off", with_flag: { scrum_projects: false } do
|
||||
it_behaves_like "not found"
|
||||
end
|
||||
|
||||
context "with a page_size parameter and offset parameter" do
|
||||
let(:page_size) { 1 }
|
||||
let(:offset) { 2 }
|
||||
|
||||
@@ -52,7 +52,7 @@ RSpec.describe "API v3 Sprint resource on project", content_type: :json do
|
||||
})
|
||||
end
|
||||
|
||||
describe "GET /api/v3/projects/:id/sprints", with_flag: :scrum_projects do
|
||||
describe "GET /api/v3/projects/:id/sprints" do
|
||||
let(:get_path) { api_v3_paths.project_sprints(project.id) }
|
||||
|
||||
before do
|
||||
@@ -71,8 +71,5 @@ RSpec.describe "API v3 Sprint resource on project", content_type: :json do
|
||||
it_behaves_like "unauthorized access"
|
||||
end
|
||||
|
||||
context "when the feature flag is turned off", with_flag: { scrum_projects: false } do
|
||||
it_behaves_like "not found"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -44,7 +44,7 @@ RSpec.describe "API v3 Sprint resource", content_type: :json do
|
||||
create(:user, member_with_permissions: { project => permissions })
|
||||
end
|
||||
|
||||
describe "GET /api/v3/sprints/:id", with_flag: :scrum_projects do
|
||||
describe "GET /api/v3/sprints/:id" do
|
||||
let(:get_path) { api_v3_paths.sprint(sprint.id) }
|
||||
|
||||
before do
|
||||
@@ -73,8 +73,5 @@ RSpec.describe "API v3 Sprint resource", content_type: :json do
|
||||
it_behaves_like "not found"
|
||||
end
|
||||
|
||||
context "when the feature flag is turned off", with_flag: { scrum_projects: false } do
|
||||
it_behaves_like "not found"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,93 +54,76 @@ RSpec.describe "RbMasterBacklogs", :skip_csrf, type: :rails_request do
|
||||
get "/projects/#{project.identifier}/backlogs"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to render_template(:index)
|
||||
expect(response).to render_template(:backlog)
|
||||
|
||||
expect(response).to have_turbo_frame "backlogs_container", src: "/projects/#{project.identifier}/backlogs"
|
||||
expect(response).to have_turbo_frame "backlogs_container",
|
||||
src: "/projects/#{project.identifier}/backlogs/backlog?all=false"
|
||||
expect(response).to have_turbo_frame "content-bodyRight"
|
||||
end
|
||||
|
||||
context "with a Turbo Frame request" do
|
||||
it "renders the list partial" do
|
||||
it "renders the backlog list partial" do
|
||||
get "/projects/#{project.identifier}/backlogs", headers: { "Turbo-Frame" => "backlogs_container" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to render_template("rb_master_backlogs/_list")
|
||||
expect(response).to render_template("rb_master_backlogs/_backlog_list")
|
||||
|
||||
expect(response).to have_turbo_frame "backlogs_container"
|
||||
expect(response).to have_no_turbo_frame "content-bodyRight"
|
||||
end
|
||||
end
|
||||
|
||||
context "with the scrum project feature flag active", with_flag: { scrum_projects: true } do
|
||||
it "redirects to backlog" do
|
||||
get "/projects/#{project.identifier}/backlogs"
|
||||
|
||||
expect(response).to redirect_to("/projects/#{project.identifier}/backlogs/backlog")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET #backlog" do
|
||||
context "without the scrum project feature flag" do
|
||||
it "is not successful" do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog"
|
||||
it "is successful" do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog"
|
||||
|
||||
expect(response).to have_http_status(:forbidden)
|
||||
end
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to render_template(:backlog)
|
||||
expect(response).to have_turbo_frame "backlogs_container",
|
||||
src: "/projects/#{project.identifier}/backlogs/backlog?all=false"
|
||||
expect(response).to have_turbo_frame "content-bodyRight"
|
||||
end
|
||||
|
||||
context "with the scrum project feature flag active", with_flag: { scrum_projects: true } do
|
||||
it "is successful" do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog"
|
||||
it "passes all=true on the backlog turbo frame when requested" do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog", params: { all: "1" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to have_turbo_frame "backlogs_container",
|
||||
src: "/projects/#{project.identifier}/backlogs/backlog?all=true"
|
||||
end
|
||||
|
||||
context "with a Turbo Frame request" do
|
||||
it "renders the sprint planning list partial" do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog", headers: { "Turbo-Frame" => "backlogs_container" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to render_template(:backlog)
|
||||
expect(response).to have_turbo_frame "backlogs_container",
|
||||
src: "/projects/#{project.identifier}/backlogs/backlog?all=false"
|
||||
expect(response).to have_turbo_frame "content-bodyRight"
|
||||
expect(response).to render_template("rb_master_backlogs/_backlog_list")
|
||||
|
||||
expect(response).to have_turbo_frame "backlogs_container"
|
||||
expect(response).to have_no_turbo_frame "content-bodyRight"
|
||||
end
|
||||
|
||||
it "passes all=true on the backlog turbo frame when requested" do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog", params: { all: "1" }
|
||||
context "with no sprints available" do
|
||||
before do
|
||||
allow(Backlog)
|
||||
.to receive(:owner_backlogs)
|
||||
.with(project)
|
||||
.and_return([])
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to have_turbo_frame "backlogs_container",
|
||||
src: "/projects/#{project.identifier}/backlogs/backlog?all=true"
|
||||
end
|
||||
|
||||
context "with a Turbo Frame request" do
|
||||
it "renders the sprint planning list partial" do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog", headers: { "Turbo-Frame" => "backlogs_container" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to render_template("rb_master_backlogs/_backlog_list")
|
||||
|
||||
expect(response).to have_turbo_frame "backlogs_container"
|
||||
expect(response).to have_no_turbo_frame "content-bodyRight"
|
||||
allow(Agile::Sprint)
|
||||
.to receive(:for_project)
|
||||
.with(project)
|
||||
.and_return(Agile::Sprint.none)
|
||||
end
|
||||
|
||||
context "with no sprints available" do
|
||||
before do
|
||||
allow(Backlog)
|
||||
.to receive(:owner_backlogs)
|
||||
.with(project)
|
||||
.and_return([])
|
||||
it "still renders the sprint planning container for turbo-frame requests" do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog",
|
||||
headers: { "Turbo-Frame" => "backlogs_container" }
|
||||
|
||||
allow(Agile::Sprint)
|
||||
.to receive(:for_project)
|
||||
.with(project)
|
||||
.and_return(Agile::Sprint.none)
|
||||
end
|
||||
|
||||
it "still renders the sprint planning container for turbo-frame requests" do
|
||||
get "/projects/#{project.identifier}/backlogs/backlog",
|
||||
headers: { "Turbo-Frame" => "backlogs_container" }
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('id="owner_backlogs_container"')
|
||||
expect(response.body).to include('id="sprint_backlogs_container"')
|
||||
end
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.body).to include('id="owner_backlogs_container"')
|
||||
expect(response.body).to include('id="sprint_backlogs_container"')
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -151,21 +134,13 @@ RSpec.describe "RbMasterBacklogs", :skip_csrf, type: :rails_request do
|
||||
get "/projects/#{project.identifier}/backlogs/details/#{story.id}"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to render_template(:index)
|
||||
expect(response).to render_template(:backlog)
|
||||
|
||||
expect(response).to have_turbo_frame "backlogs_container", src: "/projects/#{project.identifier}/backlogs"
|
||||
expect(response).to have_turbo_frame "backlogs_container",
|
||||
src: "/projects/#{project.identifier}/backlogs/backlog?all=false"
|
||||
expect(response).to have_turbo_frame "content-bodyRight"
|
||||
end
|
||||
|
||||
context "with the scrum project feature flag active", with_flag: { scrum_projects: true } do
|
||||
it "is successful and renders backlog" do
|
||||
get "/projects/#{project.identifier}/backlogs/details/#{story.id}"
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response).to render_template(:backlog)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a Turbo Frame request" do
|
||||
it "renders the split view" do
|
||||
get "/projects/#{project.identifier}/backlogs/details/#{story.id}",
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe InboxController, with_flag: { scrum_projects_active: true } do
|
||||
RSpec.describe InboxController do
|
||||
describe "routing" do
|
||||
it {
|
||||
expect(put("/projects/project_42/inbox/85/move")).to route_to(
|
||||
|
||||
@@ -4,27 +4,20 @@ require "spec_helper"
|
||||
|
||||
RSpec.describe Projects::Settings::BacklogSharingsController do
|
||||
describe "routing" do
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
it {
|
||||
expect(get("/projects/project_42/settings/backlog_sharing")).to route_to(
|
||||
controller: "projects/settings/backlog_sharings",
|
||||
action: "show",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
it {
|
||||
expect(get("/projects/project_42/settings/backlog_sharing")).to route_to(
|
||||
controller: "projects/settings/backlog_sharings",
|
||||
action: "show",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(patch("/projects/project_42/settings/backlog_sharing")).to route_to(
|
||||
controller: "projects/settings/backlog_sharings",
|
||||
action: "update",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
context "with the feature flag inactive", with_flag: { scrum_projects: false } do
|
||||
it { expect(get("/projects/project_42/settings/backlog_sharing")).not_to be_routable }
|
||||
it { expect(patch("/projects/project_42/settings/backlog_sharing")).not_to be_routable }
|
||||
end
|
||||
it {
|
||||
expect(patch("/projects/project_42/settings/backlog_sharing")).to route_to(
|
||||
controller: "projects/settings/backlog_sharings",
|
||||
action: "update",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -57,76 +57,64 @@ RSpec.describe RbSprintsController do
|
||||
id: "21")
|
||||
}
|
||||
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/new_dialog")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "new_dialog",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/new_dialog")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "new_dialog",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/refresh_form")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "refresh_form",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/refresh_form")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "refresh_form",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "create",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "create",
|
||||
project_id: "project_42"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/21/edit_dialog")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "edit_dialog",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
)
|
||||
}
|
||||
it {
|
||||
expect(get("/projects/project_42/sprints/21/edit_dialog")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "edit_dialog",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(put("/projects/project_42/sprints/21/update_agile_sprint")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "update_agile_sprint",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
)
|
||||
}
|
||||
it {
|
||||
expect(put("/projects/project_42/sprints/21/update_agile_sprint")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "update_agile_sprint",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints/21/start")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "start",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
)
|
||||
}
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints/21/start")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "start",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints/21/finish")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "finish",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
context "with the feature flag inactive", with_flag: { scrum_projects: false } do
|
||||
it { expect(get("/projects/project_42/sprints/new_dialog")).not_to be_routable }
|
||||
it { expect(get("/projects/project_42/sprints/refresh_form")).not_to be_routable }
|
||||
it { expect(post("/projects/project_42/sprints")).not_to be_routable }
|
||||
it { expect(get("/projects/project_42/sprints/21/edit_dialog")).not_to be_routable }
|
||||
it { expect(put("/projects/project_42/sprints/21/update_agile_sprint")).not_to be_routable }
|
||||
it { expect(post("/projects/project_42/sprints/21/start")).not_to be_routable }
|
||||
it { expect(post("/projects/project_42/sprints/21/finish")).not_to be_routable }
|
||||
end
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints/21/finish")).to route_to(
|
||||
controller: "rb_sprints",
|
||||
action: "finish",
|
||||
project_id: "project_42",
|
||||
id: "21"
|
||||
)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -42,21 +42,15 @@ RSpec.describe RbStoriesController do
|
||||
)
|
||||
}
|
||||
|
||||
context "with the feature flag active", with_flag: { scrum_projects: true } do
|
||||
it {
|
||||
expect(put("/projects/project_42/sprints/21/stories/85/move")).to route_to(
|
||||
controller: "rb_stories",
|
||||
action: "move",
|
||||
project_id: "project_42",
|
||||
sprint_id: "21",
|
||||
id: "85"
|
||||
)
|
||||
}
|
||||
end
|
||||
|
||||
context "with the feature flag inactive", with_flag: { scrum_projects: false } do
|
||||
it { expect(put("/projects/project_42/sprints/21/stories/85/move")).not_to be_routable }
|
||||
end
|
||||
it {
|
||||
expect(put("/projects/project_42/sprints/21/stories/85/move")).to route_to(
|
||||
controller: "rb_stories",
|
||||
action: "move",
|
||||
project_id: "project_42",
|
||||
sprint_id: "21",
|
||||
id: "85"
|
||||
)
|
||||
}
|
||||
|
||||
it {
|
||||
expect(post("/projects/project_42/sprints/21/stories/85/reorder")).to route_to(
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
RSpec.describe Sprints::FinishService, with_flag: { scrum_projects: true } do
|
||||
RSpec.describe Sprints::FinishService do
|
||||
create_shared_association_defaults_for_work_package_factory
|
||||
|
||||
shared_let(:project) { create(:project, enabled_module_names: %w[backlogs work_package_tracking]) }
|
||||
|
||||
+11
-3
@@ -47,7 +47,13 @@ RSpec.describe WorkPackages::RebuildPositionsService, "integration", type: :mode
|
||||
shared_let(:sprint1_wp1) { create_work_package(subject: "Sprint 1 WorkPackage 1", sprint: sprint1, position: nil) }
|
||||
shared_let(:sprint1_wp2) { create_work_package(subject: "Sprint 1 WorkPackage 2", sprint: sprint1, position: 1) }
|
||||
shared_let(:sprint1_wp3) { create_work_package(subject: "Sprint 1 WorkPackage 3", sprint: sprint1, position: 2) }
|
||||
shared_let(:sprint1_wp4) { create_work_package(subject: "Sprint 1 WorkPackage 4", sprint: sprint1, position: 2) }
|
||||
shared_let(:sprint1_wp4) do
|
||||
create_work_package(subject: "Sprint 1 WorkPackage 4", sprint: sprint1, position: 2).tap do
|
||||
# Force wp3 back to position 2 so that wp3 and wp4 are genuinely
|
||||
# duplicated — the service must break the tie via created_at.
|
||||
sprint1_wp3.update_column(:position, 2)
|
||||
end
|
||||
end
|
||||
shared_let(:sprint1_wp5) { create_work_package(subject: "Sprint 1 WorkPackage 5", sprint: sprint1, position: nil) }
|
||||
|
||||
shared_let(:sprint2_wp1) { create_work_package(subject: "Sprint 2 WorkPackage 1", sprint: sprint2, position: 3) }
|
||||
@@ -112,6 +118,8 @@ RSpec.describe WorkPackages::RebuildPositionsService, "integration", type: :mode
|
||||
end
|
||||
|
||||
it "fixes only the work packages in the other project" do # rubocop:disable Rspec/ExampleLength
|
||||
# sprint1 and sprint2 belong to project1, so their positions are
|
||||
# unchanged by rebuilding project2.
|
||||
expect(WorkPackage.where(sprint: sprint1).to_h { [it.subject, it.position] })
|
||||
.to eql(
|
||||
sprint1_wp1.subject => nil,
|
||||
@@ -124,8 +132,8 @@ RSpec.describe WorkPackages::RebuildPositionsService, "integration", type: :mode
|
||||
expect(WorkPackage.where(sprint: sprint2).to_h { [it.subject, it.position] })
|
||||
.to eql(
|
||||
sprint2_wp3.subject => 1,
|
||||
sprint2_wp2.subject => 2,
|
||||
sprint2_wp1.subject => 3
|
||||
sprint2_wp2.subject => 3,
|
||||
sprint2_wp1.subject => 5
|
||||
)
|
||||
|
||||
expect(WorkPackage.where(sprint: nil).to_h { [it.subject, it.position] })
|
||||
|
||||
+49
-51
@@ -77,21 +77,34 @@ RSpec.describe WorkPackages::UpdateService, "sprint preservation on project chan
|
||||
current_user { user }
|
||||
|
||||
describe "when changing the project" do
|
||||
context "when scrum_projects feature flag is active", with_flag: { scrum_projects: true } do
|
||||
context "when the work package has a sprint" do
|
||||
context "when moving to a project that does NOT have access to the sprint" do
|
||||
it "nullifies the sprint_id" do
|
||||
result = instance.call(project: target_project)
|
||||
context "when the work package has a sprint" do
|
||||
context "when moving to a project that does NOT have access to the sprint" do
|
||||
it "nullifies the sprint_id" do
|
||||
result = instance.call(project: target_project)
|
||||
|
||||
expect(result).to be_success
|
||||
expect(work_package.reload.sprint_id).to be_nil
|
||||
expect(work_package.project).to eq(target_project)
|
||||
end
|
||||
expect(result).to be_success
|
||||
expect(work_package.reload.sprint_id).to be_nil
|
||||
expect(work_package.project).to eq(target_project)
|
||||
end
|
||||
end
|
||||
|
||||
context "when moving to a project that HAS access to the sprint" do
|
||||
let(:source_sharing) { "share_all_projects" }
|
||||
|
||||
it "preserves the sprint_id" do
|
||||
result = instance.call(project: target_project)
|
||||
|
||||
expect(result).to be_success
|
||||
expect(work_package.reload.sprint_id).to eq(sprint_in_source_project.id)
|
||||
expect(work_package.project).to eq(target_project)
|
||||
end
|
||||
|
||||
context "when moving to a project that HAS access to the sprint" do
|
||||
let(:source_sharing) { "share_all_projects" }
|
||||
context "with the manage_sprint_items permission missing" do
|
||||
let(:source_project_permissions) { project_permissions - %i[manage_sprint_items] }
|
||||
let(:target_project_permissions) { project_permissions - %i[manage_sprint_items] }
|
||||
|
||||
# Usually this should not work without the permission, but since the change is
|
||||
# performed via `change_by_system`, this is bypassed.
|
||||
it "preserves the sprint_id" do
|
||||
result = instance.call(project: target_project)
|
||||
|
||||
@@ -99,56 +112,41 @@ RSpec.describe WorkPackages::UpdateService, "sprint preservation on project chan
|
||||
expect(work_package.reload.sprint_id).to eq(sprint_in_source_project.id)
|
||||
expect(work_package.project).to eq(target_project)
|
||||
end
|
||||
|
||||
context "with the manage_sprint_items permission missing" do
|
||||
let(:source_project_permissions) { project_permissions - %i[manage_sprint_items] }
|
||||
let(:target_project_permissions) { project_permissions - %i[manage_sprint_items] }
|
||||
|
||||
# Usually this should not work without the permission, but since the change is
|
||||
# performed via `change_by_system`, this is bypassed.
|
||||
it "preserves the sprint_id" do
|
||||
result = instance.call(project: target_project)
|
||||
|
||||
expect(result).to be_success
|
||||
expect(work_package.reload.sprint_id).to eq(sprint_in_source_project.id)
|
||||
expect(work_package.project).to eq(target_project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the work package project is NOT changing" do
|
||||
it "preserves the sprint_id" do
|
||||
original_sprint_id = work_package.sprint_id
|
||||
result = instance.call(subject: "Updated Subject")
|
||||
|
||||
expect(result).to be_success
|
||||
expect(work_package.reload.sprint_id).to eq(original_sprint_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the work package does NOT have a sprint" do
|
||||
let(:work_package_without_sprint) do
|
||||
create(:work_package,
|
||||
subject: "Work Package Without Sprint",
|
||||
project: source_project,
|
||||
sprint: nil)
|
||||
end
|
||||
|
||||
let(:instance) { described_class.new(user:, model: work_package_without_sprint) }
|
||||
|
||||
it "keeps sprint_id nil when moving to another project" do
|
||||
result = instance.call(project: target_project)
|
||||
context "when the work package project is NOT changing" do
|
||||
it "preserves the sprint_id" do
|
||||
original_sprint_id = work_package.sprint_id
|
||||
result = instance.call(subject: "Updated Subject")
|
||||
|
||||
expect(result).to be_success
|
||||
expect(work_package_without_sprint.reload.sprint_id).to be_nil
|
||||
expect(work_package_without_sprint.project).to eq(target_project)
|
||||
expect(work_package.reload.sprint_id).to eq(original_sprint_id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context "when the work package does NOT have a sprint" do
|
||||
let(:work_package_without_sprint) do
|
||||
create(:work_package,
|
||||
subject: "Work Package Without Sprint",
|
||||
project: source_project,
|
||||
sprint: nil)
|
||||
end
|
||||
|
||||
let(:instance) { described_class.new(user:, model: work_package_without_sprint) }
|
||||
|
||||
it "keeps sprint_id nil when moving to another project" do
|
||||
result = instance.call(project: target_project)
|
||||
|
||||
expect(result).to be_success
|
||||
expect(work_package_without_sprint.reload.sprint_id).to be_nil
|
||||
expect(work_package_without_sprint.project).to eq(target_project)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "Integration with sprint visibility logic", with_flag: { scrum_projects: true } do
|
||||
describe "Integration with sprint visibility logic" do
|
||||
context "when sprint is owned by the target project" do
|
||||
let(:sprint_in_target_project) do
|
||||
create(:agile_sprint,
|
||||
|
||||
-583
@@ -1,583 +0,0 @@
|
||||
#-- 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.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe WorkPackages::UpdateService, "version inheritance", type: :model do
|
||||
let(:type_feature) { build(:type_feature) }
|
||||
let(:type_task) { build(:type_task) }
|
||||
let(:type_bug) { build(:type_bug) }
|
||||
let(:version1) { project.versions.first }
|
||||
let(:version2) { project.versions.last }
|
||||
let(:role) { build(:project_role) }
|
||||
let(:user) { build(:admin) }
|
||||
let(:issue_priority) { build(:priority) }
|
||||
let(:status) { build(:status, name: "status 1", is_default: true) }
|
||||
|
||||
let(:project) do
|
||||
p = build(:project,
|
||||
members: [build(:member,
|
||||
principal: user,
|
||||
roles: [role])],
|
||||
types: [type_feature, type_task, type_bug])
|
||||
|
||||
p.versions << build(:version, name: "Version1", project: p)
|
||||
p.versions << build(:version, name: "Version2", project: p)
|
||||
|
||||
p
|
||||
end
|
||||
|
||||
let(:story) do
|
||||
story = build(:work_package,
|
||||
subject: "Story",
|
||||
project:,
|
||||
type: type_feature,
|
||||
version: version1,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
story
|
||||
end
|
||||
|
||||
let(:story2) do
|
||||
story = build(:work_package,
|
||||
subject: "Story2",
|
||||
project:,
|
||||
type: type_feature,
|
||||
version: version1,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
story
|
||||
end
|
||||
|
||||
let(:story3) do
|
||||
story = build(:work_package,
|
||||
subject: "Story3",
|
||||
project:,
|
||||
type: type_feature,
|
||||
version: version1,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
story
|
||||
end
|
||||
|
||||
let(:task) do
|
||||
build(:work_package,
|
||||
subject: "Task",
|
||||
type: type_task,
|
||||
version: version1,
|
||||
project:,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
end
|
||||
|
||||
let(:task2) do
|
||||
build(:work_package,
|
||||
subject: "Task2",
|
||||
type: type_task,
|
||||
version: version1,
|
||||
project:,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
end
|
||||
|
||||
let(:task3) do
|
||||
build(:work_package,
|
||||
subject: "Task3",
|
||||
type: type_task,
|
||||
version: version1,
|
||||
project:,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
end
|
||||
|
||||
let(:task4) do
|
||||
build(:work_package,
|
||||
subject: "Task4",
|
||||
type: type_task,
|
||||
version: version1,
|
||||
project:,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
end
|
||||
|
||||
let(:task5) do
|
||||
build(:work_package,
|
||||
subject: "Task5",
|
||||
type: type_task,
|
||||
version: version1,
|
||||
project:,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
end
|
||||
|
||||
let(:task6) do
|
||||
build(:work_package,
|
||||
subject: "Task6",
|
||||
type: type_task,
|
||||
version: version1,
|
||||
project:,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
end
|
||||
|
||||
let(:bug) do
|
||||
build(:work_package,
|
||||
subject: "Bug",
|
||||
type: type_bug,
|
||||
version: version1,
|
||||
project:,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
end
|
||||
|
||||
let(:bug2) do
|
||||
build(:work_package,
|
||||
subject: "Bug2",
|
||||
type: type_bug,
|
||||
version: version1,
|
||||
project:,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
end
|
||||
|
||||
let(:bug3) do
|
||||
build(:work_package,
|
||||
subject: "Bug3",
|
||||
type: type_bug,
|
||||
version: version1,
|
||||
project:,
|
||||
status:,
|
||||
author: user,
|
||||
priority: issue_priority)
|
||||
end
|
||||
|
||||
before do
|
||||
project.save!
|
||||
|
||||
allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ "points_burn_direction" => "down",
|
||||
"wiki_template" => "",
|
||||
"story_types" => [type_feature.id],
|
||||
"task_type" => type_task.id.to_s })
|
||||
end
|
||||
|
||||
def standard_child_layout
|
||||
# Layout is
|
||||
# child
|
||||
# -> task3
|
||||
# -> task4
|
||||
# -> bug3
|
||||
# -> task5
|
||||
# -> story3
|
||||
# -> task6
|
||||
task3.parent_id = child.id
|
||||
task3.save!
|
||||
task4.parent_id = child.id
|
||||
task4.save!
|
||||
bug3.parent_id = child.id
|
||||
bug3.save!
|
||||
story3.parent_id = child.id
|
||||
story3.save!
|
||||
|
||||
task5.parent_id = bug3.id
|
||||
task5.save!
|
||||
task6.parent_id = story3.id
|
||||
task6.save!
|
||||
|
||||
child.reload
|
||||
end
|
||||
|
||||
describe "WHEN changing version" do
|
||||
let(:instance) { described_class.new(user:, model: parent) }
|
||||
|
||||
shared_examples_for "changing parent's version changes child's version" do
|
||||
it "changes the child's version to the parent's version" do
|
||||
parent.save!
|
||||
child.parent_id = parent.id
|
||||
child.save!
|
||||
|
||||
standard_child_layout
|
||||
|
||||
parent.reload
|
||||
|
||||
call = instance.call(version: version2)
|
||||
|
||||
expect(call).to be_success
|
||||
|
||||
# Because of performance, these assertions are all in one it statement
|
||||
expect(child.reload.version).to eql version2
|
||||
expect(task3.reload.version).to eql version2
|
||||
expect(task4.reload.version).to eql version2
|
||||
expect(bug3.reload.version).to eql version1
|
||||
expect(story3.reload.version).to eql version1
|
||||
expect(task5.reload.version).to eql version1
|
||||
expect(task6.reload.version).to eql version1
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for "changing parent's version does not change child's version" do
|
||||
it "keeps the child's version" do
|
||||
parent.save!
|
||||
child.parent_id = parent.id
|
||||
child.save!
|
||||
|
||||
standard_child_layout
|
||||
|
||||
parent.reload
|
||||
|
||||
instance.call(version: version2)
|
||||
|
||||
# Because of performance, these assertions are all in one it statement
|
||||
expect(child.reload.version).to eql version1
|
||||
expect(task3.reload.version).to eql version1
|
||||
expect(task4.reload.version).to eql version1
|
||||
expect(bug3.reload.version).to eql version1
|
||||
expect(story3.reload.version).to eql version1
|
||||
expect(task5.reload.version).to eql version1
|
||||
expect(task6.reload.version).to eql version1
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH backlogs enabled" do
|
||||
before do
|
||||
project.enabled_module_names += ["backlogs"]
|
||||
end
|
||||
|
||||
describe "WITH a story" do
|
||||
let(:parent) { story }
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task2 }
|
||||
|
||||
it_behaves_like "changing parent's version changes child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non backlogs work_package as child" do
|
||||
let(:child) { bug2 }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
|
||||
describe "WITH a story as a child" do
|
||||
let(:child) { story2 }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH a task (impediment) without a parent" do
|
||||
let(:parent) { task }
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task2 }
|
||||
|
||||
it_behaves_like "changing parent's version changes child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non backlogs work_package as child" do
|
||||
let(:child) { bug }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH a non backlogs work_package" do
|
||||
let(:parent) { bug }
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non backlogs work_package as child" do
|
||||
let(:child) { bug2 }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
|
||||
describe "WITH a story as a child" do
|
||||
let(:child) { story }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH backlogs disabled" do
|
||||
before do
|
||||
project.enabled_module_names = project.enabled_module_names.find_all { |n| n != "backlogs" }
|
||||
end
|
||||
|
||||
describe "WITH a story" do
|
||||
let(:parent) { story }
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task2 }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non backlogs work_package as child" do
|
||||
let(:child) { bug2 }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
|
||||
describe "WITH a story as a child" do
|
||||
let(:child) { story2 }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH a task" do
|
||||
before do
|
||||
bug2.save!
|
||||
task.parent_id = bug2.id # so that it is considered a task
|
||||
task.save!
|
||||
end
|
||||
|
||||
let(:parent) { task }
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task2 }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non backlogs work_package as child" do
|
||||
let(:child) { bug }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH a task (impediment) without a parent" do
|
||||
let(:parent) { task }
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task2 }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non backlogs work_package as child" do
|
||||
let(:child) { bug }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH a non backlogs work_package" do
|
||||
let(:parent) { bug }
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non backlogs work_package as child" do
|
||||
let(:child) { bug2 }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
|
||||
describe "WITH a story as a child" do
|
||||
let(:child) { story }
|
||||
|
||||
it_behaves_like "changing parent's version does not change child's version"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "WHEN changing the parent_id" do
|
||||
let(:instance) { described_class.new(user:, model: child) }
|
||||
|
||||
shared_examples_for "changing the child's parent_issue to the parent changes child's version" do
|
||||
it "changes the child's version to the parent's version" do
|
||||
child.save!
|
||||
standard_child_layout
|
||||
|
||||
parent.version = version2
|
||||
parent.save!
|
||||
|
||||
instance.call(parent_id: parent.id)
|
||||
|
||||
# Because of performance, these assertions are all in one it statement
|
||||
expect(child.reload.version).to eql version2
|
||||
expect(task3.reload.version).to eql version2
|
||||
expect(task4.reload.version).to eql version2
|
||||
expect(bug3.reload.version).to eql version1
|
||||
expect(story3.reload.version).to eql version1
|
||||
expect(task5.reload.version).to eql version1
|
||||
expect(task6.reload.version).to eql version1
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for "changing the child's parent to the parent leaves child's version" do
|
||||
it "keeps the child's version" do
|
||||
child.save!
|
||||
standard_child_layout
|
||||
|
||||
parent.version = version2
|
||||
parent.save!
|
||||
|
||||
instance.call(parent_id: parent.id)
|
||||
|
||||
# Because of performance, these assertions are all in one it statement
|
||||
expect(child.reload.version).to eql version1
|
||||
expect(task3.reload.version).to eql version1
|
||||
expect(task4.reload.version).to eql version1
|
||||
expect(bug3.reload.version).to eql version1
|
||||
expect(story3.reload.version).to eql version1
|
||||
expect(task5.reload.version).to eql version1
|
||||
expect(task6.reload.version).to eql version1
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH backogs enabled" do
|
||||
before do
|
||||
story.project.enabled_module_names += ["backlogs"]
|
||||
end
|
||||
|
||||
describe "WITH a story as parent" do
|
||||
let(:parent) { story }
|
||||
|
||||
describe "WITH a story as child" do
|
||||
let(:child) { story2 }
|
||||
|
||||
it_behaves_like "changing the child's parent to the parent leaves child's version"
|
||||
end
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task2 }
|
||||
|
||||
it_behaves_like "changing the child's parent_issue to the parent changes child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non-backlogs work_package as child" do
|
||||
let(:child) { bug2 }
|
||||
|
||||
it_behaves_like "changing the child's parent to the parent leaves child's version"
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH a story as parent " \
|
||||
"WITH the story having a non backlogs work_package as parent " \
|
||||
"WITH a task as child" do
|
||||
before do
|
||||
bug2.save!
|
||||
story.parent_id = bug2.id
|
||||
story.save!
|
||||
end
|
||||
|
||||
let(:parent) { story }
|
||||
let(:child) { task2 }
|
||||
|
||||
it_behaves_like "changing the child's parent_issue to the parent changes child's version"
|
||||
end
|
||||
|
||||
describe "WITH a task as parent" do
|
||||
before do
|
||||
story.save!
|
||||
task.parent_id = story.id
|
||||
task.save!
|
||||
story.reload
|
||||
task.reload
|
||||
end
|
||||
|
||||
# Needs to be the story because it is not possible to change a task's
|
||||
# 'version_id'
|
||||
let(:parent) { story }
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task2 }
|
||||
|
||||
it_behaves_like "changing the child's parent_issue to the parent changes child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non-backlogs work_package as child" do
|
||||
let(:child) { bug2 }
|
||||
|
||||
it_behaves_like "changing the child's parent to the parent leaves child's version"
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH an impediment (task) as parent" do
|
||||
let(:parent) { task }
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task2 }
|
||||
|
||||
it_behaves_like "changing the child's parent_issue to the parent changes child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non-backlogs work_package as child" do
|
||||
let(:child) { bug2 }
|
||||
|
||||
it_behaves_like "changing the child's parent to the parent leaves child's version"
|
||||
end
|
||||
end
|
||||
|
||||
describe "WITH a non-backlogs work_package as parent" do
|
||||
let(:parent) { bug }
|
||||
|
||||
describe "WITH a story as child" do
|
||||
let(:child) { story2 }
|
||||
|
||||
it_behaves_like "changing the child's parent to the parent leaves child's version"
|
||||
end
|
||||
|
||||
describe "WITH a task as child" do
|
||||
let(:child) { task2 }
|
||||
|
||||
it_behaves_like "changing the child's parent to the parent leaves child's version"
|
||||
end
|
||||
|
||||
describe "WITH a non-backlogs work_package as child" do
|
||||
let(:child) { bug2 }
|
||||
|
||||
it_behaves_like "changing the child's parent to the parent leaves child's version"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -97,7 +97,7 @@ RSpec.describe "rb_burndown_charts/show" do
|
||||
end
|
||||
|
||||
describe "burndown chart" do
|
||||
it "renders a version with dates" do
|
||||
it "renders a sprint with dates" do
|
||||
assign(:sprint, sprint)
|
||||
assign(:project, project)
|
||||
assign(:burndown, sprint.burndown(project))
|
||||
|
||||
@@ -57,51 +57,6 @@ RSpec.describe "version edit", :js do
|
||||
.to have_content new_version_name
|
||||
end
|
||||
|
||||
context "when editing a shared version from a subproject with backlogs enabled", :js do
|
||||
let(:child_project) do
|
||||
create(:project, parent: project, enabled_module_names: %w[backlogs work_package_tracking])
|
||||
end
|
||||
let(:permissions) do
|
||||
{ project => %i[manage_versions view_work_packages],
|
||||
child_project => %i[manage_versions view_work_packages] }
|
||||
end
|
||||
|
||||
it "persists the version setting scoped to the subproject, not the parent project (Regression#73187)" do
|
||||
visit edit_version_path(version, project_id: child_project.id)
|
||||
|
||||
select I18n.t(:version_settings_display_option_right),
|
||||
from: I18n.t(:label_column_in_backlog)
|
||||
|
||||
click_button I18n.t(:button_save)
|
||||
|
||||
expect(page).to have_text(I18n.t(:notice_successful_update))
|
||||
|
||||
# it creates a version setting for the child project
|
||||
setting = VersionSetting.find_by(version:, project: child_project)
|
||||
expect(setting).not_to be_nil
|
||||
expect(setting).to be_display_right
|
||||
|
||||
# it does not create a version setting for the parent project
|
||||
expect(VersionSetting.exists?(version:, project:)).to be false
|
||||
|
||||
# visiting the parent project shows the default version setting
|
||||
visit edit_version_path(version, project_id: project.id)
|
||||
|
||||
expect(page).to have_select(
|
||||
I18n.t(:label_column_in_backlog),
|
||||
selected: I18n.t(:version_settings_display_option_left)
|
||||
)
|
||||
|
||||
# revisiting the settings page shows the correct version setting
|
||||
visit edit_version_path(version, project_id: child_project.id)
|
||||
|
||||
expect(page).to have_select(
|
||||
I18n.t(:label_column_in_backlog),
|
||||
selected: I18n.t(:version_settings_display_option_right)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context "with a custom field" do
|
||||
let!(:custom_field) do
|
||||
create(:version_custom_field, :string,
|
||||
|
||||
@@ -245,13 +245,16 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do
|
||||
"Remaining work", "9h",
|
||||
"% Complete", "25%",
|
||||
"Spent time", "0h",
|
||||
"Story Points", "1",
|
||||
"Details",
|
||||
"Priority", "Normal",
|
||||
*(work_package.sprint.present? ? ["Sprint", work_package.sprint] : ["Sprint"]),
|
||||
"Version", work_package.version,
|
||||
"Category", work_package.category,
|
||||
"Project phase",
|
||||
"Date", "05/30/2024 - 03/13/2025",
|
||||
"Other",
|
||||
"Position", "1",
|
||||
"Work Package Custom Field Long Text", "foo faa",
|
||||
"Empty Work Package Custom Field Long Text",
|
||||
"Work Package Custom Field Boolean", "Yes",
|
||||
@@ -627,7 +630,7 @@ RSpec.describe WorkPackage::PDFExport::WorkPackageToPdf do
|
||||
end
|
||||
end
|
||||
|
||||
context "with the backlogs module enabled and the feature flag active", with_flag: { scrum_projects: true } do
|
||||
context "with the backlogs module enabled" do
|
||||
let(:enabled_module_names) { %i[backlogs] }
|
||||
let(:sprint) { create(:agile_sprint, name: "Sprint name for export", project:) }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user