From 44b434e3284af3e54a4439202ca2abcb69d697f9 Mon Sep 17 00:00:00 2001 From: Alexander Brandon Coles Date: Mon, 13 Apr 2026 13:56:10 +0100 Subject: [PATCH] [#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 --- app/components/versions/row_component.rb | 2 +- app/forms/versions/form.rb | 59 -- app/models/permitted_params.rb | 6 +- config/initializers/feature_decisions.rb | 5 - .../settings_header_component.html.erb | 2 +- .../app/controllers/inbox_controller.rb | 1 - .../projects/settings/backlogs_controller.rb | 6 +- .../controllers/rb_application_controller.rb | 23 +- .../rb_burndown_charts_controller.rb | 2 +- .../rb_master_backlogs_controller.rb | 52 +- .../app/controllers/rb_stories_controller.rb | 2 +- .../controllers/rb_taskboards_controller.rb | 9 +- .../backlogs/app/forms/my/backlogs_form.rb | 6 - .../backlogs/app/helpers/rb_common_helper.rb | 26 +- .../work_packages/filter/sprint_filter.rb | 6 +- .../rebuild_positions_service.rb | 2 +- .../app/views/backlogs_settings/show.html.erb | 20 +- modules/backlogs/config/locales/en.yml | 12 - modules/backlogs/config/routes.rb | 56 +- .../backlogs_type_representer.rb | 57 -- .../backlogs_type_dependency_representer.rb | 65 -- .../lib/api/v3/sprints/sprints_api.rb | 4 - .../api/v3/sprints/sprints_by_project_api.rb | 2 - .../lib/open_project/backlogs/engine.rb | 36 +- .../lib/open_project/backlogs/list.rb | 105 +--- .../patches/api/work_package_representer.rb | 6 +- .../api/work_package_schema_representer.rb | 3 +- .../patches/set_attributes_service_patch.rb | 50 +- .../backlogs/patches/type_patch.rb | 8 +- .../backlogs/patches/update_service_patch.rb | 76 --- .../patches/versions/row_component_patch.rb | 3 +- .../patches/versions_controller_patch.rb | 75 --- .../backlogs/patches/work_package_patch.rb | 60 +- .../backlogs/work_package_filter.rb | 166 ----- .../spec/controllers/inbox_controller_spec.rb | 2 +- .../backlog_sharings_controller_spec.rb | 2 +- .../controllers/rb_sprints_controller_spec.rb | 266 ++++---- .../controllers/rb_stories_controller_spec.rb | 10 +- .../rb_taskboards_controller_spec.rb | 149 ++--- .../controllers/versions_controller_spec.rb | 82 --- .../features/admin/backlogs_settings_spec.rb | 80 +-- .../spec/features/backlogs/create_spec.rb | 233 ++++--- .../spec/features/backlogs/edit_spec.rb | 186 +++--- .../features/backlogs/sprint_list_spec.rb | 2 +- .../features/backlogs/start_finish_spec.rb | 12 +- .../spec/features/burndown/show_spec.rb | 2 +- .../spec/features/inbox_column_spec.rb | 2 +- .../settings/backlog_sharing_settings_spec.rb | 11 +- .../work_packages/create_work_package_spec.rb | 2 +- .../work_packages/drag_in_inbox_spec.rb | 3 +- .../work_packages/drag_in_sprint_spec.rb | 2 +- .../features/work_packages/filter_spec.rb | 2 +- .../work_packages/sprints_on_wp_view_spec.rb | 10 +- .../work_package_schema_representer_spec.rb | 44 +- ...work_package_representer_rendering_spec.rb | 100 +-- .../open_project/backlogs/permissions_spec.rb | 16 +- modules/backlogs/spec/models/burndown_spec.rb | 368 ++++------- .../spec/models/issue_position_spec.rb | 424 ------------- .../filter/sprint_filter_spec.rb | 25 +- .../backlogs/spec/models/work_package_spec.rb | 66 -- .../models/work_packages/position_spec.rb | 3 +- .../api/v3/sprints/index_resource_spec.rb | 6 +- .../v3/sprints/project_index_resource_spec.rb | 5 +- .../api/v3/sprints/show_resource_spec.rb | 5 +- .../spec/requests/rb_master_backlogs_spec.rb | 117 ++-- .../spec/routing/inbox_routing_spec.rb | 2 +- .../settings/backlog_sharings_routing_spec.rb | 35 +- .../spec/routing/rb_sprints_routing_spec.rb | 118 ++-- .../spec/routing/rb_stories_routing_spec.rb | 24 +- .../services/sprints/finish_service_spec.rb | 2 +- ...uild_positions_service_integration_spec.rb | 14 +- ...update_service_sprint_preservation_spec.rb | 100 ++- ...update_service_version_inheritance_spec.rb | 583 ------------------ .../views/rb_burndown_charts/show_spec.rb | 2 +- spec/features/versions/edit_spec.rb | 45 -- .../pdf_export/work_package_to_pdf_spec.rb | 5 +- 76 files changed, 817 insertions(+), 3363 deletions(-) delete mode 100644 modules/backlogs/lib/api/v3/backlogs_types/backlogs_type_representer.rb delete mode 100644 modules/backlogs/lib/api/v3/queries/schemas/backlogs_type_dependency_representer.rb delete mode 100644 modules/backlogs/lib/open_project/backlogs/patches/update_service_patch.rb delete mode 100644 modules/backlogs/lib/open_project/backlogs/patches/versions_controller_patch.rb delete mode 100644 modules/backlogs/lib/open_project/backlogs/work_package_filter.rb delete mode 100644 modules/backlogs/spec/controllers/versions_controller_spec.rb delete mode 100644 modules/backlogs/spec/models/issue_position_spec.rb delete mode 100644 modules/backlogs/spec/services/work_packages/update_service_version_inheritance_spec.rb diff --git a/app/components/versions/row_component.rb b/app/components/versions/row_component.rb index 1a6187596f3..73dd9cb06db 100644 --- a/app/components/versions/row_component.rb +++ b/app/components/versions/row_component.rb @@ -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 diff --git a/app/forms/versions/form.rb b/app/forms/versions/form.rb index 35f8b6b0533..778650eda8a 100644 --- a/app/forms/versions/form.rb +++ b/app/forms/versions/form.rb @@ -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 diff --git a/app/models/permitted_params.rb b/app/models/permitted_params.rb index 3470efc1006..d0a44db3c3a 100644 --- a/app/models/permitted_params.rb +++ b/app/models/permitted_params.rb @@ -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 diff --git a/config/initializers/feature_decisions.rb b/config/initializers/feature_decisions.rb index e8101395c4a..022d5863785 100644 --- a/config/initializers/feature_decisions.rb +++ b/config/initializers/feature_decisions.rb @@ -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." diff --git a/modules/backlogs/app/components/projects/settings/backlogs/settings_header_component.html.erb b/modules/backlogs/app/components/projects/settings/backlogs/settings_header_component.html.erb index 985b7d9a585..40344ff43f1 100644 --- a/modules/backlogs/app/components/projects/settings/backlogs/settings_header_component.html.erb +++ b/modules/backlogs/app/components/projects/settings/backlogs/settings_header_component.html.erb @@ -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) diff --git a/modules/backlogs/app/controllers/inbox_controller.rb b/modules/backlogs/app/controllers/inbox_controller.rb index 8b44cfe30f4..3334fba19b7 100644 --- a/modules/backlogs/app/controllers/inbox_controller.rb +++ b/modules/backlogs/app/controllers/inbox_controller.rb @@ -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). diff --git a/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb b/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb index 695adaf7980..1d182802c09 100644 --- a/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb +++ b/modules/backlogs/app/controllers/projects/settings/backlogs_controller.rb @@ -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 diff --git a/modules/backlogs/app/controllers/rb_application_controller.rb b/modules/backlogs/app/controllers/rb_application_controller.rb index 1b37ec0444b..d76147b2eba 100644 --- a/modules/backlogs/app/controllers/rb_application_controller.rb +++ b/modules/backlogs/app/controllers/rb_application_controller.rb @@ -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 diff --git a/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb b/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb index d5001d452a7..06ae933f3eb 100644 --- a/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb +++ b/modules/backlogs/app/controllers/rb_burndown_charts_controller.rb @@ -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) diff --git a/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb b/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb index 6fd86af7837..0810fb1f96a 100644 --- a/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb +++ b/modules/backlogs/app/controllers/rb_master_backlogs_controller.rb @@ -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 diff --git a/modules/backlogs/app/controllers/rb_stories_controller.rb b/modules/backlogs/app/controllers/rb_stories_controller.rb index 4696e703515..6b6e49d8c39 100644 --- a/modules/backlogs/app/controllers/rb_stories_controller.rb +++ b/modules/backlogs/app/controllers/rb_stories_controller.rb @@ -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)) diff --git a/modules/backlogs/app/controllers/rb_taskboards_controller.rb b/modules/backlogs/app/controllers/rb_taskboards_controller.rb index ceeeceeb68f..1a412742028 100644 --- a/modules/backlogs/app/controllers/rb_taskboards_controller.rb +++ b/modules/backlogs/app/controllers/rb_taskboards_controller.rb @@ -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 diff --git a/modules/backlogs/app/forms/my/backlogs_form.rb b/modules/backlogs/app/forms/my/backlogs_form.rb index 9f3b4ad8d94..9e8cacb4533 100644 --- a/modules/backlogs/app/forms/my/backlogs_form.rb +++ b/modules/backlogs/app/forms/my/backlogs_form.rb @@ -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, diff --git a/modules/backlogs/app/helpers/rb_common_helper.rb b/modules/backlogs/app/helpers/rb_common_helper.rb index babf00ca127..cf849e44dd3 100644 --- a/modules/backlogs/app/helpers/rb_common_helper.rb +++ b/modules/backlogs/app/helpers/rb_common_helper.rb @@ -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 diff --git a/modules/backlogs/app/models/queries/work_packages/filter/sprint_filter.rb b/modules/backlogs/app/models/queries/work_packages/filter/sprint_filter.rb index 3b7ff4ae824..3ba25a9ca78 100644 --- a/modules/backlogs/app/models/queries/work_packages/filter/sprint_filter.rb +++ b/modules/backlogs/app/models/queries/work_packages/filter/sprint_filter.rb @@ -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 diff --git a/modules/backlogs/app/services/work_packages/rebuild_positions_service.rb b/modules/backlogs/app/services/work_packages/rebuild_positions_service.rb index 0cf950f6968..3e3c2b93d7c 100644 --- a/modules/backlogs/app/services/work_packages/rebuild_positions_service.rb +++ b/modules/backlogs/app/services/work_packages/rebuild_positions_service.rb @@ -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 diff --git a/modules/backlogs/app/views/backlogs_settings/show.html.erb b/modules/backlogs/app/views/backlogs_settings/show.html.erb index eea296fdfb9..2cd0672eed2 100644 --- a/modules/backlogs/app/views/backlogs_settings/show.html.erb +++ b/modules/backlogs/app/views/backlogs_settings/show.html.erb @@ -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 %> diff --git a/modules/backlogs/config/locales/en.yml b/modules/backlogs/config/locales/en.yml index 999c419c142..db97608a617 100644 --- a/modules/backlogs/config/locales/en.yml +++ b/modules/backlogs/config/locales/en.yml @@ -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" diff --git a/modules/backlogs/config/routes.rb b/modules/backlogs/config/routes.rb index f11657e7291..aa2344ae591 100644 --- a/modules/backlogs/config/routes.rb +++ b/modules/backlogs/config/routes.rb @@ -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 diff --git a/modules/backlogs/lib/api/v3/backlogs_types/backlogs_type_representer.rb b/modules/backlogs/lib/api/v3/backlogs_types/backlogs_type_representer.rb deleted file mode 100644 index e63aacc6ea1..00000000000 --- a/modules/backlogs/lib/api/v3/backlogs_types/backlogs_type_representer.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/lib/api/v3/queries/schemas/backlogs_type_dependency_representer.rb b/modules/backlogs/lib/api/v3/queries/schemas/backlogs_type_dependency_representer.rb deleted file mode 100644 index b8bf4d7631d..00000000000 --- a/modules/backlogs/lib/api/v3/queries/schemas/backlogs_type_dependency_representer.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/lib/api/v3/sprints/sprints_api.rb b/modules/backlogs/lib/api/v3/sprints/sprints_api.rb index 81c2cd047a5..cac5ab77180 100644 --- a/modules/backlogs/lib/api/v3/sprints/sprints_api.rb +++ b/modules/backlogs/lib/api/v3/sprints/sprints_api.rb @@ -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 diff --git a/modules/backlogs/lib/api/v3/sprints/sprints_by_project_api.rb b/modules/backlogs/lib/api/v3/sprints/sprints_by_project_api.rb index 1d2d651acef..f5acfb5bfd5 100644 --- a/modules/backlogs/lib/api/v3/sprints/sprints_by_project_api.rb +++ b/modules/backlogs/lib/api/v3/sprints/sprints_by_project_api.rb @@ -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 diff --git a/modules/backlogs/lib/open_project/backlogs/engine.rb b/modules/backlogs/lib/open_project/backlogs/engine.rb index 004587ec6c8..c194579d974 100644 --- a/modules/backlogs/lib/open_project/backlogs/engine.rb +++ b/modules/backlogs/lib/open_project/backlogs/engine.rb @@ -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 diff --git a/modules/backlogs/lib/open_project/backlogs/list.rb b/modules/backlogs/lib/open_project/backlogs/list.rb index 3d7029dd634..b7a22fb3ae9 100644 --- a/modules/backlogs/lib/open_project/backlogs/list.rb +++ b/modules/backlogs/lib/open_project/backlogs/list.rb @@ -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 diff --git a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb index 0a2af73ba77..6b9783ce1f3 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_representer.rb @@ -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, diff --git a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb index 66f70df1d63..92f5a58f949 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/api/work_package_schema_representer.rb @@ -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) diff --git a/modules/backlogs/lib/open_project/backlogs/patches/set_attributes_service_patch.rb b/modules/backlogs/lib/open_project/backlogs/patches/set_attributes_service_patch.rb index f173c4a78a3..caab9d2df53 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/set_attributes_service_patch.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/set_attributes_service_patch.rb @@ -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 diff --git a/modules/backlogs/lib/open_project/backlogs/patches/type_patch.rb b/modules/backlogs/lib/open_project/backlogs/patches/type_patch.rb index faf5f964ff5..e3dbfaa34d1 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/type_patch.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/type_patch.rb @@ -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 diff --git a/modules/backlogs/lib/open_project/backlogs/patches/update_service_patch.rb b/modules/backlogs/lib/open_project/backlogs/patches/update_service_patch.rb deleted file mode 100644 index 20fd7d173f5..00000000000 --- a/modules/backlogs/lib/open_project/backlogs/patches/update_service_patch.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/lib/open_project/backlogs/patches/versions/row_component_patch.rb b/modules/backlogs/lib/open_project/backlogs/patches/versions/row_component_patch.rb index 90f0a2fd93a..34e1e92b4a6 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/versions/row_component_patch.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/versions/row_component_patch.rb @@ -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 "", diff --git a/modules/backlogs/lib/open_project/backlogs/patches/versions_controller_patch.rb b/modules/backlogs/lib/open_project/backlogs/patches/versions_controller_patch.rb deleted file mode 100644 index 53e29ab9403..00000000000 --- a/modules/backlogs/lib/open_project/backlogs/patches/versions_controller_patch.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/lib/open_project/backlogs/patches/work_package_patch.rb b/modules/backlogs/lib/open_project/backlogs/patches/work_package_patch.rb index aa910015263..028b67a4a64 100644 --- a/modules/backlogs/lib/open_project/backlogs/patches/work_package_patch.rb +++ b/modules/backlogs/lib/open_project/backlogs/patches/work_package_patch.rb @@ -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 diff --git a/modules/backlogs/lib/open_project/backlogs/work_package_filter.rb b/modules/backlogs/lib/open_project/backlogs/work_package_filter.rb deleted file mode 100644 index c08d55e753d..00000000000 --- a/modules/backlogs/lib/open_project/backlogs/work_package_filter.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/spec/controllers/inbox_controller_spec.rb b/modules/backlogs/spec/controllers/inbox_controller_spec.rb index 29c7f8cec2e..26b4ef0ba91 100644 --- a/modules/backlogs/spec/controllers/inbox_controller_spec.rb +++ b/modules/backlogs/spec/controllers/inbox_controller_spec.rb @@ -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) } diff --git a/modules/backlogs/spec/controllers/projects/settings/backlog_sharings_controller_spec.rb b/modules/backlogs/spec/controllers/projects/settings/backlog_sharings_controller_spec.rb index fe43d163b2a..5f1f9eaddae 100644 --- a/modules/backlogs/spec/controllers/projects/settings/backlog_sharings_controller_spec.rb +++ b/modules/backlogs/spec/controllers/projects/settings/backlog_sharings_controller_spec.rb @@ -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 } diff --git a/modules/backlogs/spec/controllers/rb_sprints_controller_spec.rb b/modules/backlogs/spec/controllers/rb_sprints_controller_spec.rb index 452c70ad378..6eeee3d8f9d 100644 --- a/modules/backlogs/spec/controllers/rb_sprints_controller_spec.rb +++ b/modules/backlogs/spec/controllers/rb_sprints_controller_spec.rb @@ -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 diff --git a/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb b/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb index 215608223ed..6b872b941e2 100644 --- a/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb +++ b/modules/backlogs/spec/controllers/rb_stories_controller_spec.rb @@ -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, diff --git a/modules/backlogs/spec/controllers/rb_taskboards_controller_spec.rb b/modules/backlogs/spec/controllers/rb_taskboards_controller_spec.rb index 33d226de6de..55bc8425ba1 100644 --- a/modules/backlogs/spec/controllers/rb_taskboards_controller_spec.rb +++ b/modules/backlogs/spec/controllers/rb_taskboards_controller_spec.rb @@ -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 diff --git a/modules/backlogs/spec/controllers/versions_controller_spec.rb b/modules/backlogs/spec/controllers/versions_controller_spec.rb deleted file mode 100644 index 197ac886638..00000000000 --- a/modules/backlogs/spec/controllers/versions_controller_spec.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/spec/features/admin/backlogs_settings_spec.rb b/modules/backlogs/spec/features/admin/backlogs_settings_spec.rb index 8c486888c03..fa02e504783 100644 --- a/modules/backlogs/spec/features/admin/backlogs_settings_spec.rb +++ b/modules/backlogs/spec/features/admin/backlogs_settings_spec.rb @@ -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']" diff --git a/modules/backlogs/spec/features/backlogs/create_spec.rb b/modules/backlogs/spec/features/backlogs/create_spec.rb index 1958971dc0d..789e235717e 100644 --- a/modules/backlogs/spec/features/backlogs/create_spec.rb +++ b/modules/backlogs/spec/features/backlogs/create_spec.rb @@ -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! diff --git a/modules/backlogs/spec/features/backlogs/edit_spec.rb b/modules/backlogs/spec/features/backlogs/edit_spec.rb index 38963c51ba2..285c1155b50 100644 --- a/modules/backlogs/spec/features/backlogs/edit_spec.rb +++ b/modules/backlogs/spec/features/backlogs/edit_spec.rb @@ -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 diff --git a/modules/backlogs/spec/features/backlogs/sprint_list_spec.rb b/modules/backlogs/spec/features/backlogs/sprint_list_spec.rb index fb4b1a23384..1fffdb7962c 100644 --- a/modules/backlogs/spec/features/backlogs/sprint_list_spec.rb +++ b/modules/backlogs/spec/features/backlogs/sprint_list_spec.rb @@ -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] }) } diff --git a/modules/backlogs/spec/features/backlogs/start_finish_spec.rb b/modules/backlogs/spec/features/backlogs/start_finish_spec.rb index de692042e09..36aeb964e26 100644 --- a/modules/backlogs/spec/features/backlogs/start_finish_spec.rb +++ b/modules/backlogs/spec/features/backlogs/start_finish_spec.rb @@ -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! diff --git a/modules/backlogs/spec/features/burndown/show_spec.rb b/modules/backlogs/spec/features/burndown/show_spec.rb index 16d029564eb..bf1427d428f 100644 --- a/modules/backlogs/spec/features/burndown/show_spec.rb +++ b/modules/backlogs/spec/features/burndown/show_spec.rb @@ -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)) } diff --git a/modules/backlogs/spec/features/inbox_column_spec.rb b/modules/backlogs/spec/features/inbox_column_spec.rb index 6a6762cef16..27e87674e1a 100644 --- a/modules/backlogs/spec/features/inbox_column_spec.rb +++ b/modules/backlogs/spec/features/inbox_column_spec.rb @@ -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, diff --git a/modules/backlogs/spec/features/projects/settings/backlog_sharing_settings_spec.rb b/modules/backlogs/spec/features/projects/settings/backlog_sharing_settings_spec.rb index b8204087370..f498dd1422c 100644 --- a/modules/backlogs/spec/features/projects/settings/backlog_sharing_settings_spec.rb +++ b/modules/backlogs/spec/features/projects/settings/backlog_sharing_settings_spec.rb @@ -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 diff --git a/modules/backlogs/spec/features/work_packages/create_work_package_spec.rb b/modules/backlogs/spec/features/work_packages/create_work_package_spec.rb index e8641776ca0..cb21b602d31 100644 --- a/modules/backlogs/spec/features/work_packages/create_work_package_spec.rb +++ b/modules/backlogs/spec/features/work_packages/create_work_package_spec.rb @@ -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], diff --git a/modules/backlogs/spec/features/work_packages/drag_in_inbox_spec.rb b/modules/backlogs/spec/features/work_packages/drag_in_inbox_spec.rb index 902a1012322..a8a005b342d 100644 --- a/modules/backlogs/spec/features/work_packages/drag_in_inbox_spec.rb +++ b/modules/backlogs/spec/features/work_packages/drag_in_inbox_spec.rb @@ -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) } diff --git a/modules/backlogs/spec/features/work_packages/drag_in_sprint_spec.rb b/modules/backlogs/spec/features/work_packages/drag_in_sprint_spec.rb index 062b8a8dc80..9cb360a42cc 100644 --- a/modules/backlogs/spec/features/work_packages/drag_in_sprint_spec.rb +++ b/modules/backlogs/spec/features/work_packages/drag_in_sprint_spec.rb @@ -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], diff --git a/modules/backlogs/spec/features/work_packages/filter_spec.rb b/modules/backlogs/spec/features/work_packages/filter_spec.rb index 132734d3f35..417795a41de 100644 --- a/modules/backlogs/spec/features/work_packages/filter_spec.rb +++ b/modules/backlogs/spec/features/work_packages/filter_spec.rb @@ -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 diff --git a/modules/backlogs/spec/features/work_packages/sprints_on_wp_view_spec.rb b/modules/backlogs/spec/features/work_packages/sprints_on_wp_view_spec.rb index 52e938cfd1e..b1fe96a2590 100644 --- a/modules/backlogs/spec/features/work_packages/sprints_on_wp_view_spec.rb +++ b/modules/backlogs/spec/features/work_packages/sprints_on_wp_view_spec.rb @@ -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) } diff --git a/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb b/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb index 360f9989ecd..4a8bace20b3 100644 --- a/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb +++ b/modules/backlogs/spec/lib/api/v3/work_packages/schema/work_package_schema_representer_spec.rb @@ -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 diff --git a/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb b/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb index f545b9692b9..2185062671b 100644 --- a/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb +++ b/modules/backlogs/spec/lib/api/v3/work_packages/work_package_representer_rendering_spec.rb @@ -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 diff --git a/modules/backlogs/spec/lib/open_project/backlogs/permissions_spec.rb b/modules/backlogs/spec/lib/open_project/backlogs/permissions_spec.rb index 7aaa9c0605f..39c9546a71b 100644 --- a/modules/backlogs/spec/lib/open_project/backlogs/permissions_spec.rb +++ b/modules/backlogs/spec/lib/open_project/backlogs/permissions_spec.rb @@ -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 diff --git a/modules/backlogs/spec/models/burndown_spec.rb b/modules/backlogs/spec/models/burndown_spec.rb index 4cecac0c8d8..ad6a8855578 100644 --- a/modules/backlogs/spec/models/burndown_spec.rb +++ b/modules/backlogs/spec/models/burndown_spec.rb @@ -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 diff --git a/modules/backlogs/spec/models/issue_position_spec.rb b/modules/backlogs/spec/models/issue_position_spec.rb deleted file mode 100644 index 5fd1e4c95af..00000000000 --- a/modules/backlogs/spec/models/issue_position_spec.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/spec/models/queries/work_packages/filter/sprint_filter_spec.rb b/modules/backlogs/spec/models/queries/work_packages/filter/sprint_filter_spec.rb index 6751bde7117..7005e1d4d3a 100644 --- a/modules/backlogs/spec/models/queries/work_packages/filter/sprint_filter_spec.rb +++ b/modules/backlogs/spec/models/queries/work_packages/filter/sprint_filter_spec.rb @@ -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) { [] } diff --git a/modules/backlogs/spec/models/work_package_spec.rb b/modules/backlogs/spec/models/work_package_spec.rb index f7852539288..9bd29fdf30f 100644 --- a/modules/backlogs/spec/models/work_package_spec.rb +++ b/modules/backlogs/spec/models/work_package_spec.rb @@ -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 diff --git a/modules/backlogs/spec/models/work_packages/position_spec.rb b/modules/backlogs/spec/models/work_packages/position_spec.rb index e67ece7b9cd..c3c3f678ba0 100644 --- a/modules/backlogs/spec/models/work_packages/position_spec.rb +++ b/modules/backlogs/spec/models/work_packages/position_spec.rb @@ -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) } diff --git a/modules/backlogs/spec/requests/api/v3/sprints/index_resource_spec.rb b/modules/backlogs/spec/requests/api/v3/sprints/index_resource_spec.rb index 75ca1684000..4d725f3aeed 100644 --- a/modules/backlogs/spec/requests/api/v3/sprints/index_resource_spec.rb +++ b/modules/backlogs/spec/requests/api/v3/sprints/index_resource_spec.rb @@ -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 } diff --git a/modules/backlogs/spec/requests/api/v3/sprints/project_index_resource_spec.rb b/modules/backlogs/spec/requests/api/v3/sprints/project_index_resource_spec.rb index 4be6d08774e..bf139b801a7 100644 --- a/modules/backlogs/spec/requests/api/v3/sprints/project_index_resource_spec.rb +++ b/modules/backlogs/spec/requests/api/v3/sprints/project_index_resource_spec.rb @@ -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 diff --git a/modules/backlogs/spec/requests/api/v3/sprints/show_resource_spec.rb b/modules/backlogs/spec/requests/api/v3/sprints/show_resource_spec.rb index 40e68c0fb22..94c7202cbcc 100644 --- a/modules/backlogs/spec/requests/api/v3/sprints/show_resource_spec.rb +++ b/modules/backlogs/spec/requests/api/v3/sprints/show_resource_spec.rb @@ -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 diff --git a/modules/backlogs/spec/requests/rb_master_backlogs_spec.rb b/modules/backlogs/spec/requests/rb_master_backlogs_spec.rb index 9562eb91885..0a85e427627 100644 --- a/modules/backlogs/spec/requests/rb_master_backlogs_spec.rb +++ b/modules/backlogs/spec/requests/rb_master_backlogs_spec.rb @@ -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}", diff --git a/modules/backlogs/spec/routing/inbox_routing_spec.rb b/modules/backlogs/spec/routing/inbox_routing_spec.rb index cfa1e7288da..0bdca34a520 100644 --- a/modules/backlogs/spec/routing/inbox_routing_spec.rb +++ b/modules/backlogs/spec/routing/inbox_routing_spec.rb @@ -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( diff --git a/modules/backlogs/spec/routing/projects/settings/backlog_sharings_routing_spec.rb b/modules/backlogs/spec/routing/projects/settings/backlog_sharings_routing_spec.rb index f24881f9d03..e104d9351d0 100644 --- a/modules/backlogs/spec/routing/projects/settings/backlog_sharings_routing_spec.rb +++ b/modules/backlogs/spec/routing/projects/settings/backlog_sharings_routing_spec.rb @@ -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 diff --git a/modules/backlogs/spec/routing/rb_sprints_routing_spec.rb b/modules/backlogs/spec/routing/rb_sprints_routing_spec.rb index 043a3fb646d..891d66cd8af 100644 --- a/modules/backlogs/spec/routing/rb_sprints_routing_spec.rb +++ b/modules/backlogs/spec/routing/rb_sprints_routing_spec.rb @@ -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 diff --git a/modules/backlogs/spec/routing/rb_stories_routing_spec.rb b/modules/backlogs/spec/routing/rb_stories_routing_spec.rb index 4bf6b239918..f7be891e10a 100644 --- a/modules/backlogs/spec/routing/rb_stories_routing_spec.rb +++ b/modules/backlogs/spec/routing/rb_stories_routing_spec.rb @@ -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( diff --git a/modules/backlogs/spec/services/sprints/finish_service_spec.rb b/modules/backlogs/spec/services/sprints/finish_service_spec.rb index 44c4a31636b..b055d6ecccc 100644 --- a/modules/backlogs/spec/services/sprints/finish_service_spec.rb +++ b/modules/backlogs/spec/services/sprints/finish_service_spec.rb @@ -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]) } diff --git a/modules/backlogs/spec/services/work_packages/rebuild_positions_service_integration_spec.rb b/modules/backlogs/spec/services/work_packages/rebuild_positions_service_integration_spec.rb index 69c721c6507..ae4cc7f8380 100644 --- a/modules/backlogs/spec/services/work_packages/rebuild_positions_service_integration_spec.rb +++ b/modules/backlogs/spec/services/work_packages/rebuild_positions_service_integration_spec.rb @@ -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] }) diff --git a/modules/backlogs/spec/services/work_packages/update_service_sprint_preservation_spec.rb b/modules/backlogs/spec/services/work_packages/update_service_sprint_preservation_spec.rb index 206adc98a48..f49e2822977 100644 --- a/modules/backlogs/spec/services/work_packages/update_service_sprint_preservation_spec.rb +++ b/modules/backlogs/spec/services/work_packages/update_service_sprint_preservation_spec.rb @@ -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, diff --git a/modules/backlogs/spec/services/work_packages/update_service_version_inheritance_spec.rb b/modules/backlogs/spec/services/work_packages/update_service_version_inheritance_spec.rb deleted file mode 100644 index 4582f5b7b63..00000000000 --- a/modules/backlogs/spec/services/work_packages/update_service_version_inheritance_spec.rb +++ /dev/null @@ -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 diff --git a/modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb b/modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb index a950505fdf6..4741e3ce98a 100644 --- a/modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb +++ b/modules/backlogs/spec/views/rb_burndown_charts/show_spec.rb @@ -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)) diff --git a/spec/features/versions/edit_spec.rb b/spec/features/versions/edit_spec.rb index 9c9993b29fd..7640ed0fd19 100644 --- a/spec/features/versions/edit_spec.rb +++ b/spec/features/versions/edit_spec.rb @@ -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, diff --git a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb index 20401dd1c5c..037cb463657 100644 --- a/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb +++ b/spec/models/work_packages/pdf_export/work_package_to_pdf_spec.rb @@ -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:) }