[#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:
Alexander Brandon Coles
2026-04-13 13:56:10 +01:00
parent 8d72592230
commit 44b434e328
76 changed files with 817 additions and 3363 deletions
+1 -1
View File
@@ -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
-59
View File
@@ -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
+1 -5
View File
@@ -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
-5
View File
@@ -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."
@@ -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
%>
-12
View File
@@ -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"
+27 -29
View File
@@ -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,
@@ -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) }
@@ -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) }
@@ -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
@@ -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
+117 -251
View File
@@ -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]) }
@@ -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] })
@@ -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,
@@ -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))
-45
View File
@@ -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:) }