Clean up remaining Backlogs dead code

Restore the minimal admin settings blankslate so the admin menu route
remains valid after the sprint-based cleanup. Remove the remaining
settings-driven story/task classification code, dead models and
services, and the obsolete filter and spec setup that depended on it.
This commit is contained in:
Alexander Brandon Coles
2026-04-14 20:39:24 +01:00
parent 44b434e328
commit 7b3b6bdbf3
123 changed files with 680 additions and 8165 deletions
-6
View File
@@ -551,12 +551,6 @@ en:
sidebar_arrow: "Use the return arrow in the top left corner to return to the projects <b>main menu</b>."
welcome: "Take a three-minute introduction tour to learn the most <b>important features</b>. <br> We recommend completing the steps until the end. You can restart the tour any time."
wiki: "Within the <b>wiki</b> you can document and share knowledge together with your team."
backlogs:
overview: "Manage your work in the <b>backlogs</b> view."
sprints: "On the right you have the product backlog and the bug backlog, on the left you have the respective sprints. Here you can create <b>epics, user stories, and bugs</b>, prioritize via drag & drop and add them to a sprint."
task_board_arrow: "To see your <b>task board</b>, open the sprint drop-down..."
task_board_select: "...and select the <b>task board</b> entry."
task_board: "The task board visualizes the <b>progress for this sprint</b>. Click on the plus (+) icon next to a user story to add new tasks or impediments. <br> The status can be updated by drag and drop."
boards:
overview: "Select <b>boards</b> to shift the view and manage your project using the agile boards view."
lists_kanban: "Here you can create multiple lists (columns) within your board. This feature allows you to create a <b>Kanban board</b>, for example."
@@ -37,8 +37,7 @@ module API
cached_representer key_parts: %i[project type],
dependencies: -> {
all_permissions_granted_to_user_under_project + [Setting.work_package_done_ratio,
Setting.plugin_openproject_backlogs]
all_permissions_granted_to_user_under_project + [Setting.work_package_done_ratio]
}
custom_field_injector type: :schema_representer
@@ -1,63 +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.
++# %>
<%= component_wrapper(tag: :section) do %>
<%= render(Primer::Beta::BorderBox.new(**@system_arguments)) do |border_box| %>
<% border_box.with_header(id: dom_target(backlog, :header)) do %>
<%= render(Backlogs::BacklogHeaderComponent.new(backlog:, project: @project, folded: folded?)) %>
<% end %>
<% if backlog.stories.empty? %>
<% border_box.with_row(data: { empty_list_item: true }) do %>
<%=
render Primer::Beta::Blankslate.new(role: "status", aria: { live: "polite" }) do |blankslate|
blankslate.with_heading(tag: :h4).with_content(t(".blankslate_title", name: sprint.name))
blankslate.with_description_content(t(".blankslate_description"))
end
%>
<% end %>
<% end %>
<% backlog.stories.each do |story| %>
<% border_box.with_row(
id: dom_id(story),
classes: "Box-row--hover-blue Box-row--focus-gray Box-row--clickable Box-row--draggable",
data: draggable_item_config(story).merge(
story: true,
controller: "backlogs--story",
backlogs__story_id_value: story.id,
backlogs__story_split_url_value: details_backlogs_project_backlogs_path(project, story),
backlogs__story_full_url_value: work_package_path(story),
backlogs__story_selected_class: "Box-row--blue"
),
tabindex: 0
) do %>
<%= render(Backlogs::StoryComponent.new(story:, project:, sprint:)) %>
<% end %>
<% end %>
<% end %>
<% end %>
@@ -1,85 +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.
#++
module Backlogs
class BacklogComponent < ApplicationComponent
include Primer::AttributesHelper
include OpTurbo::Streamable
include RbCommonHelper
attr_reader :backlog, :project, :current_user
delegate :sprint, :stories, to: :backlog
def initialize(backlog:, project:, current_user: User.current, **system_arguments)
super()
@backlog = backlog
@project = project
@current_user = current_user
@system_arguments = system_arguments
@system_arguments[:id] = dom_id(backlog)
@system_arguments[:list_id] = "#{@system_arguments[:id]}-list"
@system_arguments[:padding] = :condensed
@system_arguments[:data] = merge_data(
@system_arguments,
{ data: drop_target_config }
)
end
def wrapper_uniq_by
backlog.sprint_id
end
private
def folded?
current_user.backlogs_preference(:versions_default_fold_state) == "closed"
end
def drop_target_config
{
generic_drag_and_drop_target: "container",
target_container_accessor: ":scope > ul",
target_id: "version:#{backlog.sprint_id}",
target_allowed_drag_type: "story"
}
end
def draggable_item_config(story)
{
draggable_id: story.id,
draggable_type: "story",
drop_url: move_legacy_backlogs_project_sprint_story_path(project, sprint, story)
}
end
end
end
@@ -1,90 +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.
++# %>
<%= component_wrapper(tag: :header) do %>
<% if show? %>
<%= grid_layout("op-backlogs-header", tag: :div) do |grid| %>
<% grid.with_area(:collapsible) do %>
<%=
render(
Primer::OpenProject::BorderBox::CollapsibleHeader.new(
collapsible_id: "#{dom_id(backlog)}-list",
collapsed:,
multi_line: false
)
) do |collapsible|
collapsible.with_title { sprint.name }
collapsible.with_count(
scheme: :default,
count: story_count,
round: true,
aria: {
label: t(".label_story_count", count: story_count),
live: "polite"
}
)
collapsible.with_description(role: "group") do
format_date_range(date_range)
end
end
%>
<% end %>
<% grid.with_area(:points) do %>
<%=
render(
Primer::Beta::Text.new(
color: :subtle,
classes: "velocity",
aria: { live: "polite" }
)
) do
%>
<%= story_points %>
<span class="op-backlogs-points-label"> <%= t(:"backlogs.points_label", count: story_points) %></span>
<% end %>
<% end %>
<% grid.with_area(:menu) do %>
<%= render(Backlogs::BacklogMenuComponent.new(backlog:, project: @project)) %>
<% end %>
<% end %>
<% else %>
<%=
primer_form_with(
url: backlogs_project_sprint_path(project, sprint),
model: sprint,
method: :patch,
class: "op-backlogs-header-form"
) do |f|
render(Backlogs::BacklogHeaderForm.new(f, cancel_path: show_name_backlogs_project_sprint_path(project, sprint)))
end
%>
<% end %>
<% end %>
@@ -1,82 +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.
#++
module Backlogs
class BacklogHeaderComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include OpTurbo::Streamable
include Primer::FetchOrFallbackHelper
include Redmine::I18n
include RbCommonHelper
STATE_DEFAULT = :show
STATE_OPTIONS = [STATE_DEFAULT, :edit].freeze
attr_reader :backlog, :project, :state, :collapsed, :current_user
delegate :sprint, :stories, to: :backlog
delegate :name, to: :sprint, prefix: :sprint
delegate :edit?, :show?, to: :state
def initialize(
backlog:,
project:,
state: STATE_DEFAULT,
folded: false,
current_user: User.current
)
super()
@backlog = backlog
@project = project
@state = ActiveSupport::StringInquirer.new(fetch_or_fallback(STATE_OPTIONS, state, STATE_DEFAULT).to_s)
@collapsed = folded
@current_user = current_user
end
def wrapper_uniq_by
backlog.sprint_id
end
private
def story_points
@story_points ||= stories.sum { |story| story.story_points || 0 }
end
def story_count
@story_count ||= stories.size
end
def date_range
[sprint.start_date, sprint.effective_date]
end
end
end
@@ -1,121 +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.
++# %>
<%=
render(Primer::Alpha::ActionMenu.new(**@system_arguments)) do |menu|
menu.with_show_button(
scheme: :invisible,
icon: :"kebab-horizontal",
"aria-label": t(".label_actions"),
tooltip_direction: :se
)
if user_allowed?(:create_sprints)
menu.with_item(
id: dom_target(sprint, :menu, :edit_sprint),
label: t(".action_menu.edit_sprint"),
href: edit_name_backlogs_project_sprint_path(project, sprint),
content_arguments: { data: { turbo_stream: true } }
) do |item|
item.with_leading_visual_icon(icon: :pencil)
end
end
if user_allowed?(:add_work_packages) && user_allowed?(:assign_versions)
menu.with_item(
id: dom_target(sprint, :menu, :new_story),
label: t(".action_menu.new_story"),
href: new_project_work_packages_dialog_path(
project,
version_id: sprint.id,
type_id: available_story_types.first
),
content_arguments: { data: { turbo_stream: true } }
) do |item|
item.with_leading_visual_icon(icon: :compose)
end
end
if user_allowed?(:create_sprints) || user_allowed?(:manage_sprint_items)
menu.with_divider
end
menu.with_item(
id: dom_target(sprint, :menu, :stories_tasks),
label: t(".action_menu.stories_tasks"),
tag: :a,
href: backlogs_project_sprint_query_path(project, sprint)
) do |item|
item.with_leading_visual_icon(icon: :"op-view-list")
end
if backlog.sprint_backlog?
menu.with_item(
id: dom_target(sprint, :menu, :task_board),
label: t(".action_menu.task_board"),
tag: :a,
href: backlogs_project_sprint_taskboard_path(project, sprint)
) do |item|
item.with_leading_visual_icon(icon: :"op-view-cards")
end
menu.with_item(
id: dom_target(sprint, :menu, :burndown_chart),
label: t("backlogs.label_burndown_chart"),
tag: :a,
href: backlogs_project_sprint_burndown_chart_path(project, sprint),
disabled: !sprint.has_burndown?
) do |item|
item.with_leading_visual_icon(icon: :graph)
end
if project.module_enabled? "wiki"
menu.with_item(
id: dom_target(sprint, :menu, :wiki),
label: t(".action_menu.wiki"),
tag: :a,
href: edit_backlogs_project_sprint_wiki_path(project, sprint)
) do |item|
item.with_leading_visual_icon(icon: :book)
end
end
end
if user_allowed?(:create_sprints)
menu.with_item(
id: dom_target(sprint, :menu, :properties),
label: t(".action_menu.properties"),
tag: :a,
href: edit_version_path(sprint, back_url: backlogs_project_backlogs_path(project), project_id: project)
) do |item|
item.with_leading_visual_icon(icon: :gear)
end
end
end
%>
@@ -1,65 +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.
#++
module Backlogs
class BacklogMenuComponent < ApplicationComponent
include RbCommonHelper
attr_reader :backlog, :project, :current_user
delegate :sprint, :stories, to: :backlog
def initialize(backlog:, project:, current_user: User.current, **system_arguments)
super()
@backlog = backlog
@project = project
@current_user = current_user
@system_arguments = system_arguments
@system_arguments[:menu_id] = dom_target(backlog, :menu)
@system_arguments[:anchor_align] = :end
@system_arguments[:classes] = class_names(
@system_arguments[:classes],
"hide-when-print"
)
end
private
def user_allowed?(permission)
current_user.allowed_in_project?(permission, project)
end
def available_story_types
@available_story_types ||= story_types & project.types
end
end
end
@@ -56,8 +56,7 @@ See COPYRIGHT and LICENSE files for more details.
label: t(".action_menu.add_work_package"),
href: new_project_work_packages_dialog_path(
project,
sprint_id: sprint.id,
type_id: available_story_types.first
sprint_id: sprint.id
),
content_arguments: { data: { turbo_stream: true } }
) do |item|
@@ -31,7 +31,6 @@
module Backlogs
class SprintMenuComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
include RbCommonHelper
attr_reader :sprint, :project, :current_user
@@ -51,10 +50,6 @@ module Backlogs
)
end
def stories
@sprint.work_packages
end
private
def show_task_board_link?
@@ -68,9 +63,5 @@ module Backlogs
def user_allowed?(permission)
current_user.allowed_in_project?(permission, project)
end
def available_story_types
@available_story_types ||= story_types & project.types
end
end
end
@@ -53,11 +53,7 @@ module Backlogs
private
def date_range
if @sprint.is_a?(Agile::Sprint)
[@sprint.start_date, @sprint.finish_date]
else
[@sprint.start_date, @sprint.effective_date]
end
[@sprint.start_date, @sprint.finish_date]
end
end
end
@@ -51,20 +51,22 @@ See COPYRIGHT and LICENSE files for more details.
<% end %>
<% grid.with_area(:menu) do %>
<%= render(
Primer::Alpha::ActionMenu.new(
menu_id: dom_target(story, :menu),
src: menu_backlogs_project_sprint_story_path(project, sprint, story),
anchor_align: :end,
classes: "hide-when-print"
)
) do |menu| %>
<% menu.with_show_button(
scheme: :invisible,
icon: :"kebab-horizontal",
"aria-label": t(".label_actions"),
tooltip_direction: :se
) %>
<% if menu_src.present? %>
<%= render(
Primer::Alpha::ActionMenu.new(
menu_id: dom_target(story, :menu),
src: menu_src,
anchor_align: :end,
classes: "hide-when-print"
)
) do |menu| %>
<% menu.with_show_button(
scheme: :invisible,
icon: :"kebab-horizontal",
"aria-label": t(".label_actions"),
tooltip_direction: :se
) %>
<% end %>
<% end %>
<% end %>
@@ -32,15 +32,17 @@ module Backlogs
class StoryComponent < ApplicationComponent
include OpPrimer::ComponentHelpers
attr_reader :story, :sprint, :project, :current_user
attr_reader :story, :sprint, :project, :current_user, :show_actions, :show_drag_handle
def initialize(story:, sprint:, project:, current_user: User.current)
def initialize(story:, sprint:, project:, current_user: User.current, show_actions: true, show_drag_handle: true)
super()
@story = story
@sprint = sprint
@project = project
@current_user = current_user
@show_actions = show_actions
@show_drag_handle = show_drag_handle
end
private
@@ -50,7 +52,13 @@ module Backlogs
end
def draggable?
current_user.allowed_in_project?(:manage_sprint_items, project)
show_drag_handle && current_user.allowed_in_project?(:manage_sprint_items, project)
end
def menu_src
return unless show_actions
menu_project_sprint_story_path(project, sprint, story)
end
end
end
@@ -77,7 +77,7 @@ module Backlogs
id: dom_target(story, :menu, direction),
label:,
tag: :button,
href: reorder_backlogs_project_sprint_story_path(project, sprint, story),
href: reorder_project_sprint_story_path(project, sprint, story),
form_arguments: { method: :post, inputs: [{ name: "direction", value: direction }] }
) do |item|
item.with_leading_visual_icon(icon:)
@@ -34,19 +34,5 @@ class BacklogsSettingsController < ApplicationController
before_action :require_admin
def show
@settings = Admin::Settings::BacklogsSettingsModel.new(Setting.plugin_openproject_backlogs)
end
def update # rubocop:disable Metrics/AbcSize
@settings = Admin::Settings::BacklogsSettingsModel.new(permitted_params.backlogs_admin_settings)
if @settings.valid?
Setting.plugin_openproject_backlogs = @settings.to_h
flash[:notice] = I18n.t(:notice_successful_update)
redirect_to action: :show
else
flash.now[:error] = I18n.t(:notice_unsuccessful_update_with_reason, reason: @settings.errors.full_messages.to_sentence)
render :show, status: :unprocessable_entity
end
end
def show; end
end
@@ -53,7 +53,6 @@ class RbApplicationController < ApplicationController
@sprint_id = params.delete(:sprint_id)
return unless @sprint_id
@sprint = Agile::Sprint.for_project(@project).visible.find_by(id: @sprint_id) ||
Sprint.visible.apply_to(@project).find(@sprint_id)
@sprint = Agile::Sprint.for_project(@project).visible.find(@sprint_id)
end
end
@@ -32,11 +32,7 @@ class RbBurndownChartsController < RbApplicationController
helper :burndown_charts
def show
@burndown = if @sprint.is_a?(Agile::Sprint)
Burndown.new(@sprint, @project)
else
@sprint.burndown(@project)
end
@burndown = Burndown.new(@sprint, @project) if @sprint.date_range_set?
respond_to do |format|
format.html { render layout: true }
@@ -1,85 +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.
#++
class RbImpedimentsController < RbApplicationController
def create
call = Impediments::CreateService
.new(user: current_user)
.call(attributes: impediment_params(Impediment.new).merge(project: @project))
respond_with_impediment call
end
def update
@impediment = Impediment.find(params[:id])
call = Impediments::UpdateService
.new(user: current_user, impediment: @impediment)
.call(attributes: impediment_params(@impediment))
respond_with_impediment call
end
private
def respond_with_impediment(call)
status = call.success? ? 200 : 400
@impediment = call.result
@include_meta = true
respond_to do |format|
format.html { render partial: "impediment", object: @impediment, status:, locals: { errors: call.errors } }
end
end
def impediment_params(instance)
# We do not need project_id, since ApplicationController will take care of
# fetching the record.
params.delete(:project_id)
hash = params
.permit(:version_id, :status_id, :id, :sprint_id,
:assigned_to_id, :remaining_hours, :subject, :blocks_ids)
.to_h
.symbolize_keys
# We block block_ids only when user is not allowed to create or update the
# instance passed.
unless instance && ((instance.new_record? && User.current.allowed_in_project?(:add_work_packages,
@project)) || User.current.allowed_in_any_work_package?(
:edit_work_packages, in_project: @project
))
hash.delete(:block_ids)
end
hash
end
end
@@ -70,7 +70,6 @@ class RbMasterBacklogsController < RbApplicationController
end
def load_backlogs
@owner_backlogs = Backlog.owner_backlogs(@project)
@sprints = Agile::Sprint.for_project(@project).not_completed.order_by_date
@stories_by_sprint_id = WorkPackage
.where(sprint: @sprints, project: @project)
@@ -1,54 +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.
#++
class RbQueriesController < RbApplicationController
include WorkPackagesFilterHelper
def show
filters = []
if @sprint_id
filters.push(filter_object("status_id", "*"))
filters.push(filter_object("version_id", "=", [@sprint_id]))
# Note: We need a filter for backlogs_work_package_type but currently it's not possible for plugins to introduce new filter types
else
filters.push(filter_object("status_id", "o"))
filters.push(filter_object("version_id", "!*", [@sprint_id]))
# Same as above
end
query = {
f: filters,
c: ["type", "status", "priority", "subject", "assigned_to", "updated_at", "position"],
t: "position:desc"
}
redirect_to project_work_packages_with_query_path(@project, query)
end
end
@@ -135,53 +135,8 @@ class RbSprintsController < RbApplicationController
end
end
def edit_name
update_header_component_via_turbo_stream(state: :edit)
respond_with_turbo_streams
end
def show_name
update_header_component_via_turbo_stream(state: :show)
respond_with_turbo_streams
end
def update
call = Versions::UpdateService
.new(user: current_user, model: @sprint)
.call(attributes: sprint_params)
if call.success?
status = 200
state = :show
@sprint = call.result
render_success_flash_message_via_turbo_stream(message: I18n.t(:notice_successful_update))
else
status = 422
state = :edit
render_error_flash_message_via_turbo_stream(
message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message)
)
end
update_header_component_via_turbo_stream(state:)
respond_with_turbo_streams(status:)
end
private
def update_header_component_via_turbo_stream(state: :show)
@backlog = Backlog.for(sprint: @sprint, project: @project)
update_via_turbo_stream(
component: Backlogs::BacklogHeaderComponent.new(
backlog: @backlog,
project: @project,
state:
),
method: :morph
)
end
def update_sprint_header_component_via_turbo_stream(sprint:)
update_via_turbo_stream(
component: Backlogs::SprintHeaderComponent.new(sprint:, project: @project),
@@ -213,15 +168,7 @@ class RbSprintsController < RbApplicationController
def load_sprint_and_project
load_project
@sprint = if (NEW_SPRINT_ACTIONS + SPRINT_STATE_ACTIONS).include?(action_name.to_sym)
Agile::Sprint.for_project(@project).visible.find(params[:id])
else
Sprint.visible.find(params[:id])
end
end
def sprint_params
params.expect(sprint: %i[name start_date effective_date])
@sprint = Agile::Sprint.for_project(@project).visible.find(params[:id])
end
def agile_sprint_params
@@ -47,26 +47,6 @@ class RbStoriesController < RbApplicationController
layout: false)
end
# Move a story from a Sprint to another Sprint or an Agile::Sprint.
def move_legacy
# The update service reloads the story internally (via #move_after),
# so we memoize the previous version_id before the call.
version_id_was = @story.version_id
move_attributes = infer_attributes_from_target
unless move_story(move_attributes).success?
return respond_with_turbo_streams(status: :unprocessable_entity)
end
if target_sprint?(move_attributes)
moved_to_sprint
elsif target_version?(move_attributes) && @story.version_id != version_id_was
moved_to_version
end
respond_with_turbo_streams
end
# Move a story from an Agile::Sprint to another Agile::Sprint, or the Inbox.
def move
# The update service reloads the story internally (via #move_after),
@@ -80,8 +60,6 @@ class RbStoriesController < RbApplicationController
if target_inbox?(move_attributes)
moved_to_inbox
elsif target_version?(move_attributes)
moved_to_version
elsif target_sprint?(move_attributes) && @story.sprint_id != sprint_id_was
moved_to_sprint
end
@@ -101,7 +79,7 @@ class RbStoriesController < RbApplicationController
return respond_with_turbo_streams(status: :unprocessable_entity)
end
replace_typed_component_via_turbo_stream(sprint: @sprint)
replace_sprint_component_via_turbo_stream(sprint: @sprint)
respond_with_turbo_streams
end
@@ -113,7 +91,7 @@ class RbStoriesController < RbApplicationController
if call.success?
# Update source component so that the moved story disappears
replace_typed_component_via_turbo_stream(sprint: @sprint)
replace_sprint_component_via_turbo_stream(sprint: @sprint)
else
render_error_flash_message_via_turbo_stream(
message: I18n.t(:notice_unsuccessful_update_with_reason, reason: call.message)
@@ -129,14 +107,6 @@ class RbStoriesController < RbApplicationController
.call(attributes:, **position_attributes)
end
def replace_typed_component_via_turbo_stream(sprint:)
if sprint.is_a?(Agile::Sprint)
replace_sprint_component_via_turbo_stream(sprint:)
else
replace_backlog_component_via_turbo_stream(sprint:)
end
end
def moved_to_inbox
render_success_flash_message_via_turbo_stream(
message: I18n.t(:notice_successful_move, from: @sprint.name, to: I18n.t(:label_inbox))
@@ -148,12 +118,8 @@ class RbStoriesController < RbApplicationController
)
end
def moved_to_version
moved_to(new_sprint: @story.version.becomes(Sprint))
end
def moved_to_sprint
moved_to(new_sprint: @story.sprint.becomes(Agile::Sprint))
moved_to(new_sprint: @story.sprint)
end
def moved_to(new_sprint:)
@@ -162,51 +128,28 @@ class RbStoriesController < RbApplicationController
)
# Update the target component so that the moved story shows up
replace_typed_component_via_turbo_stream(sprint: new_sprint)
replace_sprint_component_via_turbo_stream(sprint: new_sprint)
end
def infer_attributes_from_target
target_type, target_id = move_params[:target_id].split(":")
case target_type
when "version"
{ version_id: target_id, sprint_id: nil }
when "sprint"
# If the story is assigned to a version, we will only nullify the version
# if it is used as a backlog. We will keep a "regular" version reference.
# Otherwise, moving a story to a sprint would delete it from any version it is
# assigned to.
if @story.version&.used_as_backlog?
{ version_id: nil, sprint_id: target_id }
else
{ sprint_id: target_id }
end
{ sprint_id: target_id }
when "inbox"
{ sprint_id: nil }
else
raise ArgumentError, "target_type must include one of: version, sprint, inbox."
raise ArgumentError, "target_type must include one of: sprint, inbox."
end
end
def target_version?(move_attributes)
move_attributes[:version_id].present?
end
def target_sprint?(move_attributes)
move_attributes[:sprint_id].present?
end
def target_inbox?(move_attributes)
move_attributes.key?(:sprint_id) && move_attributes[:sprint_id].nil? &&
!move_attributes.key?(:version_id)
end
def replace_backlog_component_via_turbo_stream(sprint:)
@backlog = Backlog.for(sprint:, project: @project)
replace_via_turbo_stream(
component: Backlogs::BacklogComponent.new(backlog: @backlog, project: @project),
method: :morph
)
move_attributes.key?(:sprint_id) && move_attributes[:sprint_id].nil?
end
def replace_sprint_component_via_turbo_stream(sprint:)
@@ -215,12 +158,7 @@ class RbStoriesController < RbApplicationController
end
def load_story
@allowed_stories =
if @sprint.is_a?(Agile::Sprint)
WorkPackage.visible.where(sprint: @sprint, project: @project)
else
Story.visible.where(Story.condition(@project, @sprint))
end
@allowed_stories = WorkPackage.visible.where(sprint: @sprint, project: @project)
@story = @allowed_stories.find(params[:id])
end
@@ -31,32 +31,11 @@
class RbTaskboardsController < RbApplicationController
menu_item :backlogs
helper :taskboards
def show
if @sprint.is_a?(Agile::Sprint)
@board = @sprint.task_board_for(@project)
@board = @sprint.task_board_for(@project)
return redirect_to(project_work_package_board_path(@project, @board)) if @board
return redirect_to(project_work_package_board_path(@project, @board)) if @board
render_404
else
@statuses = Type.find(Task.type).statuses
@story_ids = @sprint.stories(@project).map(&:id)
@last_updated = Task.children_of(@story_ids)
.order(Arel.sql("updated_at DESC"))
.first
end
end
private
def load_sprint_and_project
@project = Project.visible.find(params[:project_id])
return unless (@sprint_id = params.delete(:sprint_id))
@sprint = Agile::Sprint.for_project(@project).visible.find_by(id: @sprint_id) ||
Sprint.visible.apply_to(@project).find(@sprint_id)
render_404
end
end
@@ -1,72 +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.
#++
class RbTasksController < RbApplicationController
# This is a constant here because we will recruit it elsewhere to whitelist
# attributes. This is necessary for now as we still directly use `attributes=`
# in non-controller code.
PERMITTED_PARAMS = ["id", "subject", "assigned_to_id", "remaining_hours", "parent_id",
"estimated_hours", "status_id", "sprint_id"].freeze
def create
call = ::Tasks::CreateService
.new(user: current_user)
.call(attributes: task_params.merge(project: @project), prev_id: params[:prev])
respond_with_task call
end
def update
task = Task.find(task_params[:id])
call = ::Tasks::UpdateService
.new(user: current_user, task:)
.call(attributes: task_params, prev_id: params[:prev])
respond_with_task call
end
private
def respond_with_task(call)
status = call.success? ? 200 : 400
@task = call.result
@include_meta = true
respond_to do |format|
format.html { render partial: "task", object: @task, status:, locals: { errors: call.errors } }
end
end
def task_params
params.permit(PERMITTED_PARAMS).to_h.symbolize_keys
end
end
@@ -1,43 +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.
#++
class RbWikisController < RbApplicationController
# NOTE: The methods #show and #edit are public (see init.rb). We will let
# OpenProject's WikiController#index take care of authorization
#
# NOTE: The methods #show and #edit create a template page when called.
def show
redirect_to controller: "/wiki", action: "index", project_id: @project, id: @sprint.wiki_page
end
def edit
redirect_to controller: "/wiki", action: "edit", project_id: @project, id: @sprint.wiki_page
end
end
@@ -1,124 +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.
#++
module Admin
module Settings
class BacklogsSettingsForm < ApplicationForm
include ::Settings::FormHelper
form do |f|
f.autocompleter(
name: :story_types,
label: I18n.t(:backlogs_story_type),
caption: setting_caption(:plugin_openproject_backlogs, :story_types),
autocomplete_options: {
multiple: true,
closeOnSelect: false,
clearable: false,
decorated: true,
data: {
admin__backlogs_settings_target: "storyTypes",
test_selector: "story_type_autocomplete"
}
}
) do |list|
available_types.each do |label, value|
active = value.in?(Story.types)
in_use = Task.type == value
list.option(
label:,
value:,
selected: active,
disabled: in_use
)
end
end
f.autocompleter(
name: :task_type,
label: I18n.t(:backlogs_task_type),
caption: setting_caption(:plugin_openproject_backlogs, :task_type),
input_width: :small,
autocomplete_options: {
multiple: false,
closeOnSelect: true,
clearable: false,
decorated: true,
data: {
admin__backlogs_settings_target: "taskType",
test_selector: "task_type_autocomplete"
}
}
) do |list|
available_types.each do |label, value|
active = Task.type == value
in_use = value.in?(Story.types)
list.option(
label:,
value:,
selected: active,
disabled: in_use
)
end
end
f.radio_button_group(
name: :points_burn_direction,
label: I18n.t(:backlogs_points_burn_direction)
) do |group|
group.radio_button(
label: I18n.t(:label_points_burn_up),
value: "up"
)
group.radio_button(
label: I18n.t(:label_points_burn_down),
value: "down"
)
end
f.text_field(
name: :wiki_template,
label: I18n.t(:backlogs_wiki_template),
input_width: :medium
)
f.submit(scheme: :primary, name: :apply, label: I18n.t(:button_save))
end
private
def available_types
Type.pluck(:name, :id)
end
end
end
end
@@ -1,60 +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.
#++
module Admin
module Settings
class BacklogsSettingsModel
include ActiveModel::Model
include ActiveModel::Attributes
attribute :story_types, array: true, default: []
attribute :task_type, :integer
attribute :points_burn_direction, :string
attribute :wiki_template, :string
validates :task_type, exclusion: {
in: ->(setting) { setting.story_types }, message: :cannot_be_story_type
}
def story_types=(value)
super(Array(value).map(&:to_i))
end
def to_h
{
story_types:,
task_type:,
points_burn_direction:,
wiki_template:
}
end
end
end
end
@@ -1,85 +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.
#++
module Backlogs
class BacklogHeaderForm < ApplicationForm
attr_reader :cancel_path
form do |f|
f.text_field(
name: :name,
label: attribute_name(:name),
placeholder: attribute_name(:name),
visually_hide_label: true,
autofocus: true,
autocomplete: "off"
)
f.group(layout: :horizontal) do |dates|
dates.single_date_picker(
name: :start_date,
input_width: :xsmall,
full_width: false,
label: attribute_name(:start_date),
placeholder: attribute_name(:start_date),
visually_hide_label: true,
leading_visual: { icon: :calendar }
)
dates.single_date_picker(
name: :effective_date,
input_width: :xsmall,
full_width: false,
label: attribute_name(:effective_date),
placeholder: attribute_name(:effective_date),
visually_hide_label: true,
leading_visual: { icon: :calendar }
)
end
f.group(layout: :horizontal) do |buttons|
buttons.submit(scheme: :primary, name: :submit, label: I18n.t(:button_save))
buttons.button(
scheme: :secondary,
name: :cancel,
label: I18n.t(:button_cancel),
tag: :a,
data: { turbo_stream: true },
href: cancel_path
)
end
end
def initialize(cancel_path:)
super()
@cancel_path = cancel_path
end
end
end
@@ -34,104 +34,6 @@ module RbCommonHelper
safe_join([from, "", to], " ") # &ndash; and &nbsp;
end
def assignee_id_or_empty(story)
story.assigned_to_id.to_s
end
def assignee_name_or_empty(story)
story.blank? || story.assigned_to.blank? ? "" : "#{story.assigned_to.firstname} #{story.assigned_to.lastname}"
end
def blocks_ids(ids)
ids.sort.join(",")
end
def build_inline_style(task)
is_assigned_task?(task) ? color_style(task) : ""
end
def color_style(task)
background_color = get_backlogs_preference(task.assigned_to, :task_color)
"style=\"background-color:#{background_color};\"".html_safe
end
def color_contrast_class(task)
if is_assigned_task?(task)
color_contrast(background_color_hex(task)) ? "light" : "dark"
else
""
end
end
def color_contrast(color)
_, bright = find_color_diff 0x000000, color
(bright > 128)
end
# Return the contrast and brightness difference between two RGB values
def find_color_diff(c1, c2)
r1, g1, b1 = break_color c1
r2, g2, b2 = break_color c2
cont_diff = (r1 - r2).abs + (g1 - g2).abs + (b1 - b2).abs # Color contrast
bright1 = ((r1 * 299) + (g1 * 587) + (b1 * 114)) / 1000
bright2 = ((r2 * 299) + (g2 * 587) + (b2 * 114)) / 1000
brt_diff = (bright1 - bright2).abs # Color brightness diff
[cont_diff, brt_diff]
end
# Break a color into the R, G and B components
def break_color(rgb)
r = (rgb & 0xff0000) >> 16
g = (rgb & 0x00ff00) >> 8
b = rgb & 0x0000ff
[r, g, b]
end
def is_assigned_task?(task)
!(task.blank? || task.assigned_to.blank?)
end
def background_color_hex(task)
background_color = get_backlogs_preference(task.assigned_to, :task_color)
background_color.sub("#", "0x").hex
end
def id_or_empty(item)
item.id.to_s
end
def work_package_link_or_empty(work_package)
modal_link_to_work_package(work_package.id, work_package, class: "prevent_edit") unless work_package.new_record?
end
def modal_link_to_work_package(title, work_package, options = {})
modal_link_to(title, work_package_path(work_package), options)
end
def modal_link_to(title, path, options = {})
html_id = "modal_work_package_#{SecureRandom.hex(10)}"
link_to(title, path, options.merge(id: html_id, target: "_blank"))
end
def mark_if_closed(story)
!story.new_record? && work_package_status_for_id(story.status_id).is_closed? ? "closed" : ""
end
def story_html_id_or_empty(story)
story.id.nil? ? "" : "story_#{story.id}"
end
def date_string_with_milliseconds(d, add = 0)
return "" if d.blank?
d.strftime("%B %d, %Y %H:%M:%S") + "." + ((d.to_f % 1) + add).to_s.split(".")[1]
end
def remaining_hours(item)
item.remaining_hours.blank? || item.remaining_hours == 0 ? "" : item.remaining_hours
end
def allow_sprint_creation?(project)
current_user.allowed_in_project?(:create_sprints, project) &&
!project.receive_shared_sprints?
@@ -140,35 +42,4 @@ module RbCommonHelper
def show_all_backlog
ActiveRecord::Type::Boolean.new.cast(params[:all]) || false
end
private
def work_package_status_for_id(id)
@all_work_package_status_by_id ||= all_work_package_status.inject({}) do |mem, status|
mem[status.id] = status
mem
end
@all_work_package_status_by_id[id]
end
def all_work_package_status
@all_work_package_status ||= Status.order(Arel.sql("position ASC"))
end
def backlogs_types
[]
end
def story_types
[]
end
def get_backlogs_preference(assignee, attr)
assignee.is_a?(User) ? assignee.backlogs_preference(attr) : "#24B3E7"
end
def sprint_board_label
t("backlogs.label_sprint_board")
end
end
@@ -1,36 +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 TaskboardsHelper
def impediments_by_position_for_status(sprint, project, status)
@impediments_by_position_for_status ||= sprint.impediments(project).group_by(&:status_id)
(@impediments_by_position_for_status[status.id] || [])
.sort_by { |i| i.position.presence || 0 }
end
end
+1 -22
View File
@@ -33,34 +33,13 @@ class Backlog
delegate :id, to: :sprint, prefix: true
def self.for(sprint:, project:)
owner_backlog = sprint.settings(project)&.display == VersionSetting::DISPLAY_RIGHT
new(sprint:, stories: sprint.stories(project), owner_backlog:)
end
def self.inbox_for(project:)
WorkPackage
.visible
.with_status_open
.where(project:, sprint_id: nil)
.includes(:type)
.order(Arel.sql(Story::ORDER))
end
def self.owner_backlogs(project)
backlogs = Sprint.apply_to(project).with_status_open.displayed_right(project).order(:name)
stories_by_sprints = Story.backlogs(project.id, backlogs.map(&:id))
backlogs.map { |sprint| new(stories: stories_by_sprints[sprint.id], owner_backlog: true, sprint:) }
end
def self.sprint_backlogs(project)
sprints = Sprint.apply_to(project).with_status_open.displayed_left(project).order_by_date
stories_by_sprints = Story.backlogs(project.id, sprints.map(&:id))
sprints.map { |sprint| new(stories: stories_by_sprints[sprint.id], sprint:) }
.order(WorkPackage.arel_table[:position].asc.nulls_last, WorkPackage.arel_table[:id].asc)
end
def initialize(sprint:, stories:, owner_backlog: false)
-80
View File
@@ -1,80 +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.
#++
class Impediment < Task
extend OpenProject::Backlogs::Mixins::PreventIssueSti
before_save :update_blocks_list
validate :validate_blocks_list
def self.default_scope
roots
.where(type_id: type)
end
def blocks_ids=(ids)
@blocks_ids = [ids] if ids.is_a?(Integer)
@blocks_ids = ids.split(/\D+/).map(&:to_i) if ids.is_a?(String)
@blocks_ids = ids.map(&:to_i) if ids.is_a?(Array)
end
def blocks_ids
@blocks_ids ||= blocks_relations.map(&:to_id)
end
private
def update_blocks_list
mark_blocks_to_destroy
build_new_blocks
end
def validate_blocks_list
if blocks_ids.empty?
errors.add :blocks_ids, :must_block_at_least_one_work_package
else
other_version_ids = WorkPackage.where(id: blocks_ids).pluck(:version_id).uniq
if other_version_ids.size != 1 || other_version_ids[0] != version_id
errors.add :blocks_ids,
:can_only_contain_work_packages_of_current_sprint
end
end
end
def mark_blocks_to_destroy
blocks_relations.reject { |relation| blocks_ids.include?(relation.to_id) }.each(&:mark_for_destruction)
end
def build_new_blocks
(blocks_ids - blocks_relations.select { |relation| blocks_ids.include?(relation.to_id) }.map(&:to_id)).each do |id|
blocks_relations.build(to_id: id)
end
end
end
+1 -7
View File
@@ -94,7 +94,7 @@ class Sprint < Version
return false if wiki_page_title.blank?
page = project.wiki.find_page(wiki_page_title)
return false if !page
return false unless page
template = project.wiki.find_page(Setting.plugin_openproject_backlogs["wiki_template"])
return false if template && page.text == template.text
@@ -150,12 +150,6 @@ class Sprint < Version
Version.where(conditions).each(&:burndown)
end
def impediments(project)
# for reasons beyond me,
# the default_scope needs to be explicitly applied.
Impediment.default_scope.where(version_id: self, project_id: project)
end
def settings(project)
version_settings.find { it.project_id == project.id || it.project_id.nil? }
end
+1 -105
View File
@@ -29,44 +29,6 @@
class Story < WorkPackage
extend OpenProject::Backlogs::Mixins::PreventIssueSti
def self.backlogs(project_id, sprint_ids, options = {}) # rubocop:disable Metrics/AbcSize
options.reverse_merge!(order: Story::ORDER,
conditions: Story.condition(project_id, sprint_ids))
candidates = Story.where(options[:conditions])
.includes(:status, :type)
.order(Arel.sql(options[:order]))
stories_by_version = Hash.new do |hash, sprint_id|
hash[sprint_id] = []
end
candidates.each do |story|
last_rank = if stories_by_version[story.version_id].size > 0
stories_by_version[story.version_id].last.rank
else
0
end
story.rank = last_rank + 1
stories_by_version[story.version_id] << story
end
stories_by_version
end
def self.sprint_backlog(project, sprint, options = {})
Story.backlogs(project.id, [sprint.id], options)[sprint.id]
end
def self.at_rank(project_id, sprint_id, rank)
Story.where(Story.condition(project_id, sprint_id))
.joins(:status)
.order(Arel.sql(Story::ORDER))
.offset(rank - 1)
.first
end
def self.types
types = Setting.plugin_openproject_backlogs["story_types"]
return [] if types.blank?
@@ -75,19 +37,7 @@ class Story < WorkPackage
end
def tasks
Task.tasks_for(id)
end
def tasks_and_subtasks
return [] unless Task.type
descendants.where(type_id: Task.type)
end
def direct_tasks_and_subtasks
return [] unless Task.type
children.where(type_id: Task.type).map { |t| [t] + t.descendants }.flatten
Task.children_of(id).order(:position)
end
def set_points(p)
@@ -109,58 +59,4 @@ class Story < WorkPackage
nil
end
end
# TODO: Refactor and add tests
#
# groups = tasks.partition(&:closed?)
# {:open => tasks.last.size, :closed => tasks.first.size}
#
def task_status
closed = 0
open = 0
tasks.each do |task|
if task.closed?
closed += 1
else
open += 1
end
end
{ open:, closed: }
end
def rank=(r)
@rank = r
end
def rank
if position.blank?
extras = [
"and ((#{WorkPackage.table_name}.position is NULL and #{WorkPackage.table_name}.id <= ?) or not #{WorkPackage.table_name}.position is NULL)", id
]
else
extras = ["and not #{WorkPackage.table_name}.position is NULL and #{WorkPackage.table_name}.position <= ?", position]
end
@rank ||= WorkPackage.where(Story.condition(project.id, version_id, extras))
.joins(:status)
.count
@rank
end
def self.condition(project_id, sprint_ids, extras = [])
c = ["project_id = ? AND type_id in (?) AND version_id in (?)",
project_id, Story.types, sprint_ids]
if extras.size > 0
c[0] += " " + extras.shift
c += extras
end
c
end
# This forces NULLS-LAST ordering
ORDER = "CASE WHEN #{WorkPackage.table_name}.position IS NULL THEN 1 ELSE 0 END ASC, CASE WHEN #{WorkPackage.table_name}.position IS NULL THEN #{WorkPackage.table_name}.id ELSE #{WorkPackage.table_name}.position END ASC"
end
-28
View File
@@ -30,32 +30,4 @@ require "date"
class Task < WorkPackage
extend OpenProject::Backlogs::Mixins::PreventIssueSti
def self.type
task_type = Setting.plugin_openproject_backlogs["task_type"]
task_type.blank? ? nil : task_type.to_i
end
# This method is used by Backlogs::List. It ensures, that tasks and stories
# follow a similar interface
def self.types
[type]
end
def self.tasks_for(story_id)
Task.children_of(story_id).order(:position).each_with_index do |task, i|
task.rank = i + 1
end
end
def status_id=(id)
super
self.remaining_hours = 0 if Status.find(id).is_closed?
end
def rank=(r)
@rank = r
end
attr_reader :rank
end
@@ -1,104 +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 BasicData
module Backlogs
class SettingSeeder < ::Seeder
self.needs = [
BasicData::TypeSeeder
]
BACKLOGS_SETTINGS_KEYS = %w[
story_types
task_type
points_burn_direction
wiki_template
].freeze
def seed_data!
configure_backlogs_settings
end
def applicable?
not backlogs_configured?
end
private
def configure_backlogs_settings
Setting.plugin_openproject_backlogs = current_backlogs_settings.merge(missing_backlogs_settings)
end
def backlogs_configured?
BACKLOGS_SETTINGS_KEYS.all? { configured?(it) }
end
def configured?(key)
current_backlogs_settings[key] != nil
end
def current_backlogs_settings
Hash(Setting.plugin_openproject_backlogs)
end
def missing_backlogs_settings
BACKLOGS_SETTINGS_KEYS
.reject { |key| configured?(key) }
.index_with { |key| setting_value(key) }
.compact
end
def setting_value(setting_key)
case setting_key
when "story_types"
backlogs_story_types.map(&:id)
when "task_type"
backlogs_task_type.try(:id)
when "points_burn_direction"
"up"
when "wiki_template"
""
end
end
def backlogs_story_types
type_references = %i[
default_type_feature
default_type_epic
default_type_user_story
default_type_bug
]
seed_data.find_references(type_references, default: nil).compact
end
def backlogs_task_type
seed_data.find_reference(:default_type_task, default: nil)
end
end
end
end
@@ -1,43 +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.
#++
class Impediments::CreateService
attr_accessor :user
def initialize(user:)
self.user = user
end
def call(attributes: {})
attributes[:type_id] = Impediment.type
WorkPackages::CreateService
.new(user:)
.call(**attributes.merge(work_package: Impediment.new).symbolize_keys)
end
end
@@ -1,43 +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.
#++
class Impediments::UpdateService
attr_accessor :user, :impediment
def initialize(user:, impediment:)
self.user = user
self.impediment = impediment
end
def call(attributes: {})
WorkPackages::UpdateService
.new(user:,
model: impediment)
.call(**attributes)
end
end
@@ -1,49 +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.
#++
class Tasks::CreateService
attr_accessor :user
def initialize(user:)
self.user = user
end
def call(attributes: {}, prev_id: "")
attributes[:type_id] = Task.type
create_call = WorkPackages::CreateService
.new(user:)
.call(**attributes)
if create_call.success?
create_call.result.move_after prev_id:
end
create_call
end
end
@@ -1,49 +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.
#++
class Tasks::UpdateService
attr_accessor :user, :task
def initialize(user:, task:)
self.user = user
self.task = task
end
def call(attributes: {}, prev_id: "")
create_call = WorkPackages::UpdateService
.new(user:,
model: task)
.call(**attributes)
if create_call.success?
create_call.result.move_after prev_id:
end
create_call
end
end
@@ -39,11 +39,11 @@ See COPYRIGHT and LICENSE files for more details.
tag: :a,
href: backlogs_project_sprint_taskboard_path(@project, @sprint),
mobile_icon: :"op-view-cards",
mobile_label: sprint_board_label,
aria: { label: sprint_board_label }
mobile_label: t("backlogs.label_sprint_board"),
aria: { label: t("backlogs.label_sprint_board") }
) do |button|
button.with_leading_visual_icon(icon: :"op-view-cards")
sprint_board_label
t("backlogs.label_sprint_board")
end
end
%>
@@ -1,72 +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.
++#%>
<%
project ||= impediment.project
prevent_edit = if User.current.allowed_in_project?(:edit_work_packages, project)
""
else
"prevent_edit"
end
%>
<div class="model work_package impediment <%= color_contrast_class(impediment) %> <%= prevent_edit %> <%= mark_if_closed(impediment) %><%= color_contrast_class(impediment) %>"
id="work_package_<%= impediment.id %>"
<%= build_inline_style(impediment) %>>
<div class="id">
<div class="t"><%= work_package_link_or_empty(impediment) %></div>
<div class="v"><%= id_or_empty(impediment) %></div>
</div>
<div class="subject editable"
fieldtype="textarea"
fieldname="subject"
fieldlabel="<%= WorkPackage.human_attribute_name(:subject) %>"
field_id="<%= impediment.id %>"><%= impediment.subject %></div>
<div class="blocks editable"
fieldname="blocks_ids"
fieldlabel="<%= t(:label_blocks_ids) %>"
field_id="<%= impediment.id %>"><%= blocks_ids(impediment.blocks_ids) %></div>
<div class="assigned_to_id editable"
fieldtype="select"
fieldname="assigned_to_id"
fieldlabel="<%= WorkPackage.human_attribute_name(:assigned_to) %>"
field_id="<%= impediment.id %>">
<div class="t"><%= assignee_name_or_empty(impediment) %></div>
<div class="v"><%= assignee_id_or_empty(impediment) %></div>
</div>
<div class="remaining_hours editable<%= " empty" if remaining_hours(impediment).blank? %>"
fieldname="remaining_hours"
fieldlabel="<%= WorkPackage.human_attribute_name(:remaining_hours) %>"
field_id="<%= impediment.id %>"><%= remaining_hours(impediment) %></div>
<div class="indicator"> </div>
<div class="meta">
<div class="story_id"><%= impediment.parent_id %></div>
<div class="status_id"><%= impediment.status_id %></div>
<%= render(partial: "shared/model_errors", object: errors) if defined?(errors) && !errors.empty? %>
</div>
</div>
@@ -1,52 +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.
++# %>
<%= turbo_frame_tag :backlogs_container, refresh: :morph do %>
<% if @owner_backlogs.empty? && @sprint_backlogs.empty? %>
<%=
render(Primer::Beta::Blankslate.new(border: true, spacious: true)) do |blankslate|
blankslate.with_visual_icon(icon: :versions)
blankslate.with_heading(tag: :h2).with_content(t(:backlogs_empty_title))
if current_user.allowed_in_project?(:create_sprints, @project)
blankslate.with_description_content(t(:backlogs_empty_action_text))
end
end
%>
<% else %>
<div class="op-backlogs-container" data-controller="generic-drag-and-drop">
<div id="sprint_backlogs_container" class="op-backlogs-lists">
<%= render(Backlogs::BacklogComponent.with_collection(@sprint_backlogs, project: @project)) %>
</div>
<div id="owner_backlogs_container" class="op-backlogs-lists">
<%= render(Backlogs::BacklogComponent.with_collection(@owner_backlogs, project: @project)) %>
</div>
</div>
<% end %>
<% end %>
@@ -1,66 +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.
++#%>
<% html_title t(:label_backlogs) %>
<% content_controller "backlogs" %>
<% content_for :content_header do %>
<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { t(:label_backlogs) }
header.with_breadcrumbs(
[{ href: project_overview_path(@project), text: @project.name },
t(:label_backlogs)]
)
end
%>
<%=
render(Primer::OpenProject::SubHeader.new) do |subheader|
subheader.with_action_button(
scheme: :primary,
leading_icon: :plus,
label: I18n.t(:label_version_new),
tag: :a,
href: new_project_version_path(@project)
) do
Version.human_model_name
end
end
%>
<% end %>
<% content_for :content_body do %>
<%= turbo_frame_tag :backlogs_container, refresh: :morph, src: backlogs_project_backlogs_path(@project), class: "op-backlogs-page" %>
<% end %>
<% content_for :content_body_right do %>
<%= render(split_view_instance) if render_work_package_split_view? %>
<% end %>
@@ -1,172 +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.
++#%>
<% content_for :additional_js_dom_ready do %>
<%= render(partial: "shared/server_variables", formats: [:js]) %>
<% end %>
<% content_controller "backlogs--taskboard-legacy" %>
<% html_title @sprint.name %>
<%=
render(Backlogs::SprintPageHeaderComponent.new(sprint: @sprint, project: @project)) do |header|
header.with_action_button(
tag: :a,
href: backlogs_project_sprint_burndown_chart_path(@project, @sprint),
mobile_icon: :graph,
mobile_label: t(:"backlogs.show_burndown_chart"),
aria: { label: t(:"backlogs.show_burndown_chart") },
disabled: !@sprint.has_burndown?
) do |button|
button.with_leading_visual_icon(icon: :graph)
t(:"backlogs.show_burndown_chart")
end
end
%>
<%= render(Primer::OpenProject::SubHeader.new) do |component| %>
<% component.with_filter_component(id: "col_width") do %>
<%=
render(
Primer::Alpha::TextField.new(
name: :col_width_input,
type: :number,
label: t(:"backlogs.column_width"),
placeholder: t(:"backlogs.column_width"),
visually_hide_label: true,
leading_visual: { icon: :"zoom-in" },
step: 1,
input_width: :xsmall,
autocomplete: "off"
)
)
%>
<% end %>
<% end %>
<div id='rb'>
<div id="taskboard">
<table id="board_header" cellspacing="0">
<tr>
<td><%= t(:backlogs_story) %></td>
<% @statuses.each do |status| %>
<td class="swimlane"><%= status.name %></td>
<% end %>
</tr>
</table>
<table id="impediments" class="board" cellspacing="0">
<tr>
<td><div class="label_sprint_impediments"><%= t(:label_sprint_impediments) %></div></td>
<% if User.current.allowed_in_project?(:add_work_packages, @project) %>
<td class="add_new clickable">+</td>
<% else %>
<td class="add_new"></td>
<% end %>
<% @statuses.each do |status| %>
<td class="swimlane list <%= status.is_closed? ? "closed" : "" %>" id="impcell_<%= status.id %>">
<%= render partial: "rb_impediments/impediment",
collection: impediments_by_position_for_status(@sprint, @project, status) %>
</td>
<% end %>
</tr>
</table>
<table id="tasks" class="board" cellspacing="0">
<% @sprint.stories(@project).each do |story| %>
<% tasks_by_status_id = story.tasks.group_by(&:status_id) %>
<tr class="<%= story_html_id_or_empty(story) %>">
<td>
<div class="story <%= mark_if_closed(story) %>" id="<%= story_html_id_or_empty(story) %>">
<div class='story-bar'>
<div class="status">
<%= story.status.name %>
</div>
<div class="id">
<%= work_package_link_or_empty(story) %>
</div>
</div>
<div class="subject"><%= story.subject %></div>
<div class='story-footer'>
<div class="assigned_to_id">
<% if story.assigned_to.present? %>
<%= link_to_user(story.assigned_to) %>
<% else %>
<em><%= t("backlogs.unassigned") %></em>
<% end %>
</div>
<div class="story-points">
<%= story.story_points %>
</div>
</div>
</div>
</td>
<% if User.current.allowed_in_project?(:add_work_packages, @project) %>
<td class="add_new clickable">+</td>
<% else %>
<td class="add_new"></td>
<% end %>
<% @statuses.each do |status| %>
<td class="swimlane list <%= status.is_closed? ? "closed" : "" %>" id="<%= story.id %>_<%= status.id %>">
<%= render partial: "rb_tasks/task",
collection: tasks_by_status_id[status.id] %>
</td>
<% end %>
</tr>
<% end %>
</table>
</div>
<div id="helpers">
<select class="assigned_to_id template" id="assigned_to_id_options">
<option value=""> </option>
<% Principal.possible_assignee(@project).each do |user| %>
<option value="<%= user.id %>" color="<%= get_backlogs_preference(user, :task_color) %>">
<%= user.name %>
</option>
<% end %>
</select>
<div id="task_template">
<%= render partial: "rb_tasks/task", object: Task.new, locals: { project: @project } %>
</div>
<div id="impediment_template">
<%= render partial: "rb_impediments/impediment", object: Impediment.new, locals: { project: @project } %>
</div>
<div id="work_package_editor"> </div>
<div class="meta" id="last_updated"><%= date_string_with_milliseconds((@last_updated.blank? ? Time.zone.now : @last_updated.updated_at)) %></div>
<div id="charts"> </div>
<div id="preloader">
<div id="spinner"> </div>
<div id="warning"> </div>
</div>
</div>
</div>
@@ -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.
++#%>
<%
project ||= task.project
prevent_edit = if User.current.allowed_in_project?(:edit_work_packages, project)
""
else
"prevent_edit"
end
%>
<div class="model work_package task <%= color_contrast_class(task) %> <%= prevent_edit %> <%= mark_if_closed(task) %>" id="work_package_<%= task.id %>" <%= build_inline_style(task) %>>
<div class="id">
<div class="t"><%= work_package_link_or_empty(task) %></div>
<div class="v"><%= id_or_empty(task) %></div>
</div>
<div class="subject editable"
fieldtype="textarea"
fieldname="subject"
fieldlabel="<%= WorkPackage.human_attribute_name(:subject) %>"
field_id="<%= task.id %>"><%= task.subject %></div>
<div class="assigned_to_id editable"
fieldtype="select"
fieldname="assigned_to_id"
fieldlabel="<%= WorkPackage.human_attribute_name(:assigned_to) %>"
field_id="<%= task.id %>">
<div class="t"><%= assignee_name_or_empty(task) %></div>
<div class="v"><%= assignee_id_or_empty(task) %></div>
</div>
<div class="remaining_hours editable<%= " empty" if remaining_hours(task).blank? %>"
fieldname="remaining_hours"
fieldlabel="<%= WorkPackage.human_attribute_name(:remaining_hours) %>"><%= remaining_hours(task) %></div>
<div class="indicator"> </div>
<div class="meta">
<div class="story_id"><%= task.parent_id %></div>
<div class="status_id"><%= task.status_id %></div>
<%= render(partial: "shared/model_errors", object: errors) if defined?(errors) && !errors.empty? %>
</div>
</div>
@@ -1,36 +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.
++#%>
<div>
<div class="meta" id="last_updated"><%= date_string_with_milliseconds((@last_updated.blank? ? Time.zone.parse(params[:after]) : @last_updated.updated_at), 0.001) %></div>
<%= render partial: "task", collection: @tasks, locals: { include_meta: @include_meta } %>
<%- if @impediments %>
<%= render partial: "impediment", collection: @impediments, locals: { include_meta: @include_meta } %>
<%- end %>
</div>
@@ -1,34 +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.
++#%>
<div class="errors">
<% model_errors.full_messages.each do |err| %>
<div><%= err %></div>
<% end %>
</div>
@@ -1,67 +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.
++#%>
if (window.RB === null || window.RB === undefined) {
window.RB = {};
}
RB.constants = {
project_id: <%= @project.id %>,
sprint_id: <%= @sprint ? @sprint.id : "null" %>
};
RB.urlFor = (function () {
const routes = {
update_sprint: '<%= backlogs_project_sprint_path(project_id: @project, id: ":id") %>',
create_task: '<%= backlogs_project_sprint_tasks_path(project_id: @project, sprint_id: ":sprint_id") %>',
update_task: '<%= backlogs_project_sprint_task_path(project_id: @project, sprint_id: ":sprint_id", id: ":id") %>',
create_impediment: '<%= backlogs_project_sprint_impediments_path(project_id: @project, sprint_id: ":sprint_id") %>',
update_impediment: '<%= backlogs_project_sprint_impediment_path(project_id: @project, sprint_id: ":sprint_id", id: ":id") %>'
};
return function (routeName, options) {
let route = routes[routeName];
if (options) {
if (options.id) {
route = route.replace(":id", options.id);
}
if (options.project_id) {
route = route.replace(":project_id", options.project_id);
}
if (options.sprint_id) {
route = route.replace(":sprint_id", options.sprint_id);
}
}
return route;
};
}());
@@ -1,55 +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.
++# %>
<% html_title t(:label_backlogs) %>
<% content_for :content_header do %>
<%=
render Primer::OpenProject::PageHeader.new do |header|
header.with_title { t(:label_backlogs) }
header.with_breadcrumbs(
[{ href: project_overview_path(@project), text: @project.name },
t(:label_backlogs)]
)
end
%>
<% end %>
<% content_for :content_body do %>
<%=
render(Primer::Beta::Blankslate.new(border: true, spacious: true)) do |blankslate|
blankslate.with_visual_icon(icon: :"op-backlogs")
blankslate.with_heading(tag: :h2).with_content(t(:backlogs_not_configured_title))
blankslate.with_description_content(t(:backlogs_not_configured_description))
blankslate.with_secondary_action(href: admin_backlogs_settings_path, scheme: :default) do
t(:backlogs_not_configured_action_text)
end
end
%>
<% end %>
+3 -53
View File
@@ -104,8 +104,6 @@ en:
done_status: "Done status"
sharing_description: "This project can either share its own sprints, receive shared sprints or handle sprints independently (no sharing)."
sharing: "Sharing"
impediment: "Impediment"
label_versions_default_fold_state: "Show versions folded"
label_burndown_chart: "Burndown chart"
label_sprint_board: "Sprint board"
work_package_is_closed: "Work package is done, when"
@@ -122,32 +120,20 @@ en:
story_points:
one: "%{count} story point"
other: "%{count} story points"
task_color: "Task color"
unassigned: "Unassigned"
administration_blankslate:
title: "Backlog admin settings are evolving"
text: "We are currently redesigning the Backlogs module. Admin settings for sprints and backlogs will be visible here in the near future. Project-level settings remain available."
user_preference:
header_backlogs: "Backlogs module"
button_update_backlogs: "Update backlogs module"
backlog_component:
blankslate_title: "%{name} is empty"
blankslate_description: "No items planned yet. Drag items here to add them."
administration_blankslate:
title: "Backlog admin settings are evolving"
text: "We are currently redesigning the Backlogs module. Admin settings for sprints and backlogs will be visible here in the near future. Project-level settings remain available."
sprint_component:
blankslate_title: "%{name} is empty"
blankslate_description: "No items planned yet. Drag items here to add them."
backlog_header_component:
label_toggle_backlog: "Collapse/Expand %{name}"
label_story_count:
zero: "No stories in backlog"
one: "%{count} story in backlog"
other: "%{count} stories in backlog"
inbox_component:
blankslate_title: "Backlog inbox is empty"
blankslate_description: "All open work packages in this project will automatically appear here."
@@ -192,16 +178,6 @@ en:
one: "%{count} story in sprint"
other: "%{count} stories in sprint"
backlog_menu_component:
label_actions: "Backlog actions"
action_menu:
edit_sprint: "Edit sprint"
new_story: "New story"
stories_tasks: "Stories/Tasks"
task_board: "Task board"
wiki: "Wiki"
properties: "Properties"
finish_sprint_dialog_component:
title: "There are work in progress items"
body: "%{message} What would you like to do with these?"
@@ -229,34 +205,18 @@ en:
copy_work_package_id: "Copy work package ID"
move_menu: "Move"
backlogs_points_burn_direction: "Points burn up/down"
backlogs_story_type: "Story types"
backlogs_task_type: "Task type"
backlogs_wiki_template: "Template for sprint wiki page"
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)"
errors:
attributes:
task_type:
cannot_be_story_type: "can not also be a story type"
label_backlog: "Backlog"
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_burndown_chart: "Burndown chart"
label_column_in_backlog: "Column in backlog"
label_sprint_board: "Sprint board"
label_points_burn_down: "Down"
label_points_burn_up: "Up"
label_sprint_edit: "Edit sprint"
label_sprint_impediments: "Sprint Impediments"
label_sprint_new: "New sprint"
label_backlog_and_sprints: "Backlog and sprints"
label_task_board: "Task board"
@@ -303,13 +263,3 @@ en:
blankslate_description: "Set start and end date for the sprint to generate a burndown chart."
remaining_hours: "remaining work"
version_settings_display_label: "Column in backlog"
version_settings_display_option_left: "left"
version_settings_display_option_none: "none"
version_settings_display_option_right: "right"
setting_plugin_openproject_backlogs_story_types_caption: |
Types treated as backlog stories (e.g., Feature, User story). Must differ from task type.
setting_plugin_openproject_backlogs_task_type_caption: |
Type used for story tasks. Must differ from story types.
+6 -32
View File
@@ -29,6 +29,10 @@
#++
Rails.application.routes.draw do
scope "admin" do
resource :backlogs, only: :show, controller: :backlogs_settings, as: "admin_backlogs_settings"
end
# Routes for the new Agile::Sprint
# Scoped under projects for permissions:
resources :projects, only: [] do
@@ -49,6 +53,7 @@ Rails.application.routes.draw do
member do
get :menu
put :move
post :reorder
end
end
end
@@ -84,34 +89,10 @@ Rails.application.routes.draw do
end
end
resources :sprints, controller: :rb_sprints, only: %i[update] do
resource :query, controller: :rb_queries, only: :show
resources :sprints, controller: :rb_sprints, only: [] do
resource :taskboard, controller: :rb_taskboards, only: :show
resource :wiki, controller: :rb_wikis, only: %i[show edit]
resource :burndown_chart, controller: :rb_burndown_charts, only: :show
resources :impediments, controller: :rb_impediments, only: %i[create update]
resources :tasks, controller: :rb_tasks, only: %i[create update]
resources :stories, controller: :rb_stories, only: [] do
member do
get :menu
put :move_legacy
post :reorder
end
end
member do
get :edit_name
get :show_name
end
end
resource :query, controller: :rb_queries, only: :show
end
end
@@ -124,11 +105,4 @@ Rails.application.routes.draw do
end
end
end
scope "admin" do
resource :backlogs,
only: %i[show update],
controller: :backlogs_settings,
as: "admin_backlogs_settings"
end
end
@@ -91,7 +91,6 @@ module OpenProject::Backlogs::Burndown
AND journals.data_type = '#{Journal::WorkPackageJournal.name}'
AND #{container_query}
AND #{project_id_query}
AND #{type_id_query}
#{and_status_query}
JOIN
(#{day_query.to_sql}) days
@@ -129,14 +128,6 @@ module OpenProject::Backlogs::Burndown
"(#{Journal::WorkPackageJournal.table_name}.project_id = #{project.id})"
end
def type_id_query
if sprint.is_a?(Agile::Sprint)
"1 = 1"
else
"(#{Journal::WorkPackageJournal.table_name}.type_id in (#{collected_types.join(',')}))"
end
end
def day_query
lower_bound = sprint.start_date
upper_date = sprint.is_a?(Agile::Sprint) ? sprint.finish_date : sprint.effective_date
@@ -146,9 +137,5 @@ module OpenProject::Backlogs::Burndown
Day.working.from_range(from: lower_bound, to: upper_bound)
end
def collected_types
Story.types << Task.type
end
end
end
@@ -54,30 +54,14 @@ module OpenProject::Backlogs
author_url: "https://www.openproject.org",
bundled: true,
settings:) do
Rails.application.reloader.to_prepare do
OpenProject::AccessControl.permission(:add_work_packages).tap do |add|
add.controller_actions << "rb_tasks/create"
add.controller_actions << "rb_impediments/create"
end
OpenProject::AccessControl.permission(:edit_work_packages).tap do |edit|
edit.controller_actions << "rb_tasks/update"
edit.controller_actions << "rb_impediments/update"
end
end
project_module :backlogs, dependencies: :work_package_tracking do
permission :view_sprints,
{ rb_master_backlogs: %i[index backlog details],
rb_sprints: %i[index show show_name],
rb_wikis: :show,
rb_sprints: %i[index show],
rb_stories: %i[index show menu],
inbox: :menu,
rb_queries: :show,
rb_burndown_charts: :show,
rb_taskboards: :show,
rb_tasks: %i[index show],
rb_impediments: %i[index show] },
rb_taskboards: :show },
permissible_on: :project,
dependencies: %i[view_work_packages show_board_views]
@@ -89,8 +73,7 @@ module OpenProject::Backlogs
require: :member
permission :create_sprints,
{ rb_sprints: %i[new_dialog refresh_form create edit_name update edit_dialog update_agile_sprint],
rb_wikis: %i[edit update] },
{ rb_sprints: %i[new_dialog refresh_form create edit_dialog update_agile_sprint] },
permissible_on: :project,
require: :member,
dependencies: :view_sprints
@@ -102,7 +85,7 @@ module OpenProject::Backlogs
dependencies: %i[view_sprints manage_board_views manage_sprint_items]
permission :manage_sprint_items,
{ rb_stories: %i[move move_legacy reorder],
{ rb_stories: %i[move reorder],
inbox: %i[move reorder move_to_sprint_dialog] },
permissible_on: :project,
require: :member,
@@ -143,7 +126,6 @@ module OpenProject::Backlogs
patches %i[PermittedParams
WorkPackage
Status
Type
Project
User
Version]
@@ -153,7 +135,6 @@ module OpenProject::Backlogs
patch_with_namespace :WorkPackages, :SetAttributesService
patch_with_namespace :WorkPackages, :BaseContract
patch_with_namespace :WorkPackages, :UpdateContract
patch_with_namespace :Versions, :RowComponent
patch_with_namespace :API, :V3, :WorkPackages, :EagerLoading, :Checksum
config.to_prepare do
@@ -229,12 +210,8 @@ module OpenProject::Backlogs
end
config.to_prepare do
enabled_backlogs_story = ->(type, project: nil) do
if project.present?
project.backlogs_enabled?
else
true
end
enabled_backlogs_story = ->(_type, project: nil) do
project.nil? || project.backlogs_enabled?
end
story_and_sprint_permission = ->(_type, project: nil) do
@@ -32,51 +32,17 @@ module OpenProject::Backlogs::List
extend ActiveSupport::Concern
included do
acts_as_list touch_on_update: false
acts_as_list touch_on_update: false, scope: %i[project_id sprint_id]
# acts as list adds a before destroy hook which messes
# with the parent_id_was value
skip_callback(:destroy, :before, :reload)
private
# Used by acts_list to limit the list to a certain subset within
# the table.
def scope_condition
{ project_id:, sprint_id: }
end
# 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?
(scope_condition.keys & changed.map(&:to_sym)).any?
end
# Copied from acts_as_list to support our custom hash-based scope condition.
def destroyed_via_scope?
return false unless destroyed_by_association
foreign_key = destroyed_by_association.foreign_key
if foreign_key.is_a?(Array)
(scope_condition.keys & foreign_key.map(&:to_sym)).any?
else
scope_condition.keys.include?(foreign_key.to_sym)
end
end
include InstanceMethods
end
module InstanceMethods
def move_after(position: nil, prev_id: nil)
if acts_as_list_list.all?(position: nil)
# If no items have a position, create an order on position
# silently. This can happen when sorting inside a version for the first
# time after backlogs was activated and there have already been items
# inside the version at the time of backlogs activation
set_default_prev_positions_silently(acts_as_list_list.last)
end
# Remove so the potential 'prev' has a correct position
remove_from_list
reload
@@ -101,13 +67,5 @@ module OpenProject::Backlogs::List
def set_list_position(new_position, _raise_exception_if_save_fails = false) # rubocop:disable Style/OptionalBooleanParameter
update_columns(position: new_position)
end
def set_default_prev_positions_silently(prev)
return if prev.nil?
WorkPackages::RebuildPositionsService.new(project: prev.project).call
prev.reload.position
end
end
end
@@ -40,12 +40,6 @@ module OpenProject::Backlogs::Patches::PermittedParamsPatch
permitted_params
end
def backlogs_admin_settings
params
.require(:settings)
.permit(:task_type, :points_burn_direction, :wiki_template, story_types: [])
end
end
end
PermittedParams.include OpenProject::Backlogs::Patches::PermittedParamsPatch
@@ -37,13 +37,6 @@ module OpenProject::Backlogs::Patches::ProjectPatch
has_many :sprints, class_name: "Agile::Sprint", dependent: :destroy
end
def rebuild_positions
return unless backlogs_enabled?
shared_versions.each { |v| v.rebuild_story_positions(self) }
nil
end
def backlogs_enabled?
module_enabled? "backlogs"
end
@@ -1,49 +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::TypePatch
def self.included(base)
base.class_eval do
include InstanceMethods
extend ClassMethods
end
end
module ClassMethods
end
module InstanceMethods
def story?
false
end
def task?
false
end
end
end
@@ -1,3 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -1,46 +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.
#++
module OpenProject::Backlogs::Patches::Versions::RowComponentPatch
def button_links
(super + [backlogs_edit_link]).compact
end
private
def backlogs_edit_link
return if version.project == table.project || !table.project.module_enabled?("backlogs")
helpers.link_to_if_authorized "",
{ controller: "/versions", action: "edit", id: version, project_id: table.project.id },
class: "icon icon-edit",
title: t(:button_edit)
end
end
@@ -59,21 +59,6 @@ module OpenProject::Backlogs::Patches::WorkPackagePatch
project.done_statuses.to_a.include?(status)
end
def story
if Story.types.include?(type_id)
Story.find(id)
elsif Task.type.present? && type_id == Task.type
ancestors.where(type_id: Story.types).first
end
end
def blocks
# return work_packages that I block that aren't closed
return [] if closed?
blocks_relations.includes(:to).merge(WorkPackage.with_status_open).map(&:to)
end
def backlogs_enabled?
project&.backlogs_enabled?
end
@@ -1,152 +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 "rails_helper"
RSpec.describe Backlogs::BacklogComponent, type: :component do
include Rails.application.routes.url_helpers
shared_let(:type_feature) { create(:type_feature) }
shared_let(:type_task) { create(:type_task) }
shared_let(:default_status) { create(:default_status) }
shared_let(:default_priority) { create(:default_priority) }
shared_let(:user) { create(:admin) }
current_user { user }
let(:project) { create(:project, types: [type_feature, type_task]) }
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: Date.yesterday, effective_date: Date.tomorrow) }
let(:stories) { [] }
let(:backlog) { Backlog.new(sprint:, stories:) }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s)
allow(user).to receive(:backlogs_preference).with(:versions_default_fold_state).and_return("open")
end
def render_component
render_inline(described_class.new(backlog:, project:, current_user: user))
end
describe "rendering" do
context "with stories" do
let(:story1) do
create(:story,
project:,
type: type_feature,
status: default_status,
priority: default_priority,
story_points: 5,
position: 1,
version: sprint)
end
let(:story2) do
create(:story,
project:,
type: type_feature,
status: default_status,
priority: default_priority,
story_points: 3,
position: 2,
version: sprint)
end
let(:stories) { [story1, story2] }
it "renders a Primer::Beta::BorderBox" do
render_component
expect(page).to have_css(".Box")
end
it "has the sprint ID in the DOM id" do
render_component
expect(page).to have_css(".Box#backlog_#{sprint.id}")
end
it "renders BacklogHeaderComponent in header" do
render_component
expect(page).to have_css(".Box-header h3", text: "Sprint 1")
end
it "renders a stable id on the backlog header" do
render_component
expect(page).to have_element(:div, class: "Box-header", id: /\Abacklog_#{sprint.id}_header\z/)
end
it "renders StoryComponent for each story" do
render_component
expect(page).to have_css(".Box-row", count: 2) # 2 stories
expect(page).to have_text(story1.subject)
expect(page).to have_text(story2.subject)
end
it "has drop target data attributes" do
render_component
box = page.find(".Box")
expect(box["data-generic-drag-and-drop-target"]).to eq("container")
expect(box["data-target-container-accessor"]).to eq(":scope > ul")
expect(box["data-target-id"]).to eq("version:#{sprint.id}")
expect(box["data-target-allowed-drag-type"]).to eq("story")
end
it "has draggable data attributes on story rows" do
render_component
story_row = page.find(".Box-row[id='story_#{story1.id}']")
expect(story_row["data-draggable-id"]).to eq(story1.id.to_s)
expect(story_row["data-draggable-type"]).to eq("story")
expect(story_row["data-drop-url"]).to end_with(move_legacy_backlogs_project_sprint_story_path(project, sprint, story1))
end
it "renders story rows with proper classes" do
render_component
story_row = page.find(".Box-row[id='story_#{story1.id}']")
expect(story_row[:class]).to include("Box-row--hover-blue")
expect(story_row[:class]).to include("Box-row--focus-gray")
expect(story_row[:class]).to include("Box-row--clickable")
end
end
context "without stories" do
let(:stories) { [] }
let(:rendered_component) { render_component }
it_behaves_like "rendering Blank Slate", heading: "Sprint 1 is empty"
end
end
end
@@ -1,221 +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 "rails_helper"
RSpec.describe Backlogs::BacklogHeaderComponent, type: :component do
shared_let(:type_feature) { create(:type_feature) }
shared_let(:type_task) { create(:type_task) }
shared_let(:default_status) { create(:default_status) }
shared_let(:default_priority) { create(:default_priority) }
shared_let(:user) { create(:admin) }
current_user { user }
let(:project) { create(:project, types: [type_feature, type_task]) }
let(:start_date) { Date.new(2024, 1, 15) }
let(:effective_date) { Date.new(2024, 1, 29) }
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date:, effective_date:) }
let(:stories) { [] }
let(:backlog) { Backlog.new(sprint:, stories:) }
let(:state) { :show }
let(:folded) { false }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s)
end
def render_component(state: :show, folded: false)
render_inline(described_class.new(backlog:, project:, state:, folded:, current_user: user))
end
describe "show state (default)" do
context "with stories" do
let(:story1) do
create(:story,
project:,
type: type_feature,
status: default_status,
priority: default_priority,
story_points: 5,
version: sprint)
end
let(:story2) do
create(:story,
project:,
type: type_feature,
status: default_status,
priority: default_priority,
story_points: 3,
version: sprint)
end
let(:story_with_nil_points) do
create(:story,
project:,
type: type_feature,
status: default_status,
priority: default_priority,
story_points: nil,
version: sprint)
end
let(:stories) { [story1, story2, story_with_nil_points] }
it "displays sprint name in h4" do
render_component
expect(page).to have_css("h3", text: "Sprint 1")
end
it "shows story count via Primer::Beta::Counter" do
render_component
expect(page).to have_css(".Counter", text: "3")
end
it "shows formatted date range with time tags" do
render_component
expect(page).to have_css("time[datetime='2024-01-15']")
expect(page).to have_css("time[datetime='2024-01-29']")
end
it "shows story points total (nil treated as 0)" do
render_component
# 5 + 3 + 0 = 8 points
expect(page).to have_text("8 points", normalize_ws: true)
end
it "renders collapse/expand chevrons" do
render_component
expect(page).to have_octicon(:"chevron-up", visible: :all)
expect(page).to have_octicon(:"chevron-down", visible: :all)
end
it "renders BacklogMenuComponent" do
render_component
expect(page).to have_css("action-menu")
end
end
context "with no stories" do
let(:stories) { [] }
it "shows 0 story count" do
render_component
expect(page).to have_css(".Counter", text: "0")
end
it "shows 0 points" do
render_component
expect(page).to have_text("0 points", normalize_ws: true)
end
end
context "when sprint has no dates" do
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: nil, effective_date: nil) }
it "renders without date range" do
render_component
expect(page).to have_no_css("time")
end
end
end
describe "folded state" do
context "when folded is true" do
it "renders chevron-up hidden and chevron-down visible" do
render_component(folded: true)
# When folded, chevron-up is hidden (has hidden attribute on svg)
# and chevron-down is visible (for expanding)
expect(page).to have_css("svg[hidden][data-target='collapsible-header.arrowUp']", visible: :hidden)
expect(page).to have_css("svg[data-target='collapsible-header.arrowDown']:not([hidden])", visible: :all)
end
end
context "when folded is false" do
it "renders chevron-down hidden and chevron-up visible" do
render_component(folded: false)
# When expanded, chevron-down is hidden (has hidden attribute)
# and chevron-up is visible (for collapsing)
expect(page).to have_css("svg[hidden][data-target='collapsible-header.arrowDown']", visible: :hidden)
expect(page).to have_css("svg[data-target='collapsible-header.arrowUp']:not([hidden])", visible: :all)
end
end
end
describe "edit state" do
it "renders a form" do
render_component(state: :edit)
expect(page).to have_css("form")
end
it "renders text field for name" do
render_component(state: :edit)
expect(page).to have_field(Sprint.human_attribute_name(:name), with: "Sprint 1")
end
it "renders date picker components" do
render_component(state: :edit)
# Date pickers have calendar icons as leading visuals
expect(page).to have_octicon(:calendar, count: 2)
end
it "shows Save button" do
render_component(state: :edit)
expect(page).to have_button(I18n.t(:button_save))
end
it "shows Cancel button" do
render_component(state: :edit)
expect(page).to have_link(I18n.t(:button_cancel))
end
end
describe "state validation" do
it "raises an InvalidValueError for invalid state values" do
expect { render_component(state: :invalid) }
.to raise_error(Primer::FetchOrFallbackHelper::InvalidValueError)
end
end
end
@@ -1,340 +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 "rails_helper"
RSpec.describe Backlogs::BacklogMenuComponent, type: :component do
shared_let(:type_feature) { create(:type_feature) }
shared_let(:type_task) { create(:type_task) }
let(:project) { create(:project, types: [type_feature, type_task]) }
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: Date.yesterday, effective_date: Date.tomorrow) }
let(:stories) { [] }
let(:backlog) { Backlog.new(sprint:, stories:, owner_backlog:) }
let(:owner_backlog) { true }
let(:user) { create(:user) }
let(:permissions) { [] }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s)
mock_permissions_for user do |mock|
mock.allow_in_project(*permissions, project:)
end
end
def render_component
render_inline(described_class.new(backlog:, project:, current_user: user))
end
it "renders a stable id on the action menu and stories/tasks item" do
render_component
expect(page).to have_element(:button, id: /\Abacklog_#{sprint.id}_menu-button\z/)
expect(page).to have_element(:ul, id: /\Abacklog_#{sprint.id}_menu-list\z/)
expect(page).to have_element(:a, id: /\Asprint_#{sprint.id}_menu_stories_tasks\z/)
end
context "for a product owner backlog" do
let(:owner_backlog) { true }
describe "permission-based items" do
context "with :add_work_packages and :assign_versions permission" do
let(:permissions) { %i[view_sprints add_work_packages assign_versions] }
it "shows Add new story item with compose icon" do
render_component
expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.new_story"))
expect(page).to have_octicon(:compose)
end
end
context "with :add_work_packages but without :assign_versions permission" do
let(:permissions) { %i[view_sprints add_work_packages] }
it "does not show Add new story item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.new_story"))
end
end
context "with :assign_versions but without :add_work_packages permission" do
let(:permissions) { %i[view_sprints assign_versions] }
it "does not show Add new story item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.new_story"))
end
end
context "without :assign_versions but with :add_work_packages and :manage_sprint_items permission" do
let(:permissions) { %i[view_sprints add_work_packages manage_sprint_items] }
it "does not show Add new story item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.new_story"))
end
end
context "with :create_sprints permission" do
let(:permissions) { %i[view_sprints create_sprints] }
it "shows Properties item with gear icon" do
render_component
expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.properties"))
expect(page).to have_octicon(:gear)
end
it "shows Edit item with pencil icon" do
render_component
expect(page).to have_css("action-menu")
expect(page).to have_text(I18n.t("backlogs.backlog_menu_component.action_menu.edit_sprint"))
expect(page).to have_octicon(:pencil)
end
end
context "without :create_sprints permission" do
let(:permissions) { [:view_sprints] }
it "does not show Properties item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.properties"))
end
it "does not show Edit item" do
render_component
expect(page).to have_no_text(I18n.t("backlogs.backlog_menu_component.action_menu.edit_sprint"))
end
end
context "with :view_sprints permission" do
let(:permissions) { %i[view_sprints] }
it "does not show Task board item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.task_board"))
end
end
end
describe "permission independent items" do
let(:permissions) { [:view_sprints] }
it "shows Stories/Tasks link" do
render_component
expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.stories_tasks"))
end
it "shows no Burndown chart link" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.label_burndown_chart"))
end
end
describe "module-based items" do
context "when wiki module is enabled" do
let(:permissions) { [:view_sprints] }
let(:project) { create(:project, types: [type_feature, type_task], enabled_module_names: %w[backlogs wiki]) }
it "does not show a Wiki item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.wiki"))
end
end
end
end
context "for a sprint backlog" do
let(:owner_backlog) { false }
describe "permission-based items" do
context "with :add_work_packages and :assign_versions permission" do
let(:permissions) { %i[view_sprints add_work_packages assign_versions] }
it "shows Add new story item with compose icon" do
render_component
expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.new_story"))
expect(page).to have_octicon(:compose)
end
end
context "with :add_work_packages but without :assign_versions permission" do
let(:permissions) { %i[view_sprints add_work_packages] }
it "does not show Add new story item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.new_story"))
end
end
context "with :assign_versions but without :add_work_packages permission" do
let(:permissions) { %i[view_sprints assign_versions] }
it "does not show Add new story item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.new_story"))
end
end
context "without :assign_versions but with :add_work_packages and :manage_sprint_items permission" do
let(:permissions) { %i[view_sprints add_work_packages manage_sprint_items] }
it "does not show Add new story item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.new_story"))
end
end
context "with :create_sprints permission" do
let(:permissions) { %i[view_sprints create_sprints] }
it "shows Properties item with gear icon" do
render_component
expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.properties"))
expect(page).to have_octicon(:gear)
end
it "shows Edit item with pencil icon" do
render_component
expect(page).to have_css("action-menu")
expect(page).to have_text(I18n.t("backlogs.backlog_menu_component.action_menu.edit_sprint"))
expect(page).to have_octicon(:pencil)
end
end
context "without :create_sprints permission" do
let(:permissions) { [:view_sprints] }
it "does not show Properties item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.properties"))
end
it "does not show Edit item" do
render_component
expect(page).to have_no_text(I18n.t("backlogs.backlog_menu_component.action_menu.edit_sprint"))
end
end
context "with :view_sprints permission" do
let(:permissions) { %i[view_sprints] }
it "shows Task board item" do
render_component
expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.task_board"))
end
end
end
describe "permission independent items" do
let(:permissions) { [:view_sprints] }
it "shows Stories/Tasks link" do
render_component
expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.stories_tasks"))
end
it "shows Burndown chart link" do
render_component
expect(page).to have_css("li", text: I18n.t(:"backlogs.label_burndown_chart"))
end
context "when sprint has no burndown (no dates)" do
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: nil, effective_date: nil) }
it "shows Burndown chart link as disabled" do
render_component
burndown_item = page.find("li", text: I18n.t(:"backlogs.label_burndown_chart"))
expect(burndown_item[:class]).to include("ActionListItem--disabled")
end
end
context "when sprint has burndown" do
it "shows Burndown chart link as enabled" do
render_component
burndown_item = page.find("li", text: I18n.t(:"backlogs.label_burndown_chart"))
expect(burndown_item[:class]).not_to include("ActionListItem--disabled")
end
end
end
describe "module-based items" do
context "when wiki module is enabled" do
let(:permissions) { [:view_sprints] }
let(:project) { create(:project, types: [type_feature, type_task], enabled_module_names: %w[backlogs wiki]) }
it "shows Wiki item" do
render_component
expect(page).to have_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.wiki"))
expect(page).to have_octicon(:book)
end
end
context "when wiki module is disabled" do
let(:permissions) { [:view_sprints] }
let(:project) { create(:project, types: [type_feature, type_task], enabled_module_names: %w[backlogs]) }
it "does not show Wiki item" do
render_component
expect(page).to have_no_text(I18n.t(:"backlogs.backlog_menu_component.action_menu.wiki"))
end
end
end
end
end
@@ -49,7 +49,7 @@ RSpec.describe Backlogs::InboxItemComponent, type: :component do
priority: default_priority,
position: 1)
end
let(:work_packages) { WorkPackage.where(id: work_package.id).order(Arel.sql(Story::ORDER)) }
let(:work_packages) { WorkPackage.where(id: work_package.id).order(:position, :id) }
before do
render_inline(
@@ -44,10 +44,6 @@ RSpec.describe Backlogs::SprintComponent, type: :component do
let(:sprint) { create(:agile_sprint, project:, name: "Sprint 1", start_date: Date.yesterday, finish_date: Date.tomorrow) }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s)
allow(user).to receive(:backlogs_preference).with(:versions_default_fold_state).and_return("open")
end
@@ -90,7 +86,7 @@ RSpec.describe Backlogs::SprintComponent, type: :component do
expect(page).to have_css(".Box#agile_sprint_#{sprint.id}")
end
it "renders BacklogHeaderComponent in header" do
it "renders SprintHeaderComponent in header" do
render_component
expect(page).to have_css(".Box-header h3", text: "Sprint 1")
@@ -45,12 +45,6 @@ RSpec.describe Backlogs::SprintHeaderComponent, type: :component do
let(:state) { :show }
let(:folded) { false }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s)
end
def render_component(folded: false, active_sprint_ids: nil)
render_inline(described_class.new(sprint:, project:, folded:, current_user: user, active_sprint_ids:))
end
@@ -118,7 +112,7 @@ RSpec.describe Backlogs::SprintHeaderComponent, type: :component do
expect(page).to have_octicon(:"chevron-down", visible: :all)
end
it "renders BacklogMenuComponent" do
it "renders SprintMenuComponent" do
render_component
expect(page).to have_css("action-menu")
@@ -40,10 +40,6 @@ RSpec.describe Backlogs::SprintMenuComponent, type: :component do
let(:permissions) { [] }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s)
create(:member,
project:,
principal: user,
@@ -64,10 +60,11 @@ RSpec.describe Backlogs::SprintMenuComponent, type: :component do
let(:permissions) { %i[view_sprints manage_sprint_items] }
it "shows Add new work package item with plus icon" do
render_component
rendered_component = render_component
expect(page).to have_text(I18n.t(:"backlogs.sprint_menu_component.action_menu.add_work_package"))
expect(page).to have_octicon(:plus)
expect(rendered_component.to_s).to include("sprint_id=#{sprint.id}")
end
end
@@ -33,8 +33,8 @@ require "rails_helper"
RSpec.describe Backlogs::SprintPageHeaderComponent, type: :component do
let(:project) { create(:project, name: "Test Project") }
let(:start_date) { Date.new(2024, 1, 15) }
let(:effective_date) { Date.new(2024, 1, 29) }
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date:, effective_date:) }
let(:finish_date) { Date.new(2024, 1, 29) }
let(:sprint) { create(:agile_sprint, project:, name: "Sprint 1", start_date:, finish_date:) }
def render_component
render_inline(described_class.new(sprint:, project:))
@@ -89,7 +89,7 @@ RSpec.describe Backlogs::SprintPageHeaderComponent, type: :component do
describe "date handling" do
context "when sprint has only start_date" do
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date:, effective_date: nil) }
let(:sprint) { create(:agile_sprint, project:, name: "Sprint 1", start_date:, finish_date: nil) }
it "renders only start date" do
render_component
@@ -99,10 +99,10 @@ RSpec.describe Backlogs::SprintPageHeaderComponent, type: :component do
end
end
context "when sprint has only effective_date" do
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: nil, effective_date:) }
context "when sprint has only finish_date" do
let(:sprint) { create(:agile_sprint, project:, name: "Sprint 1", start_date: nil, finish_date:) }
it "renders only effective date" do
it "renders only finish date" do
render_component
expect(page).to have_no_css("time[datetime='2024-01-15']")
@@ -111,7 +111,7 @@ RSpec.describe Backlogs::SprintPageHeaderComponent, type: :component do
end
context "when sprint has no dates" do
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: nil, effective_date: nil) }
let(:sprint) { create(:agile_sprint, project:, name: "Sprint 1", start_date: nil, finish_date: nil) }
it "renders no time elements" do
render_component
@@ -39,10 +39,10 @@ RSpec.describe Backlogs::StoryComponent, type: :component do
current_user { user }
let(:project) { create(:project, types: [type_feature, type_task]) }
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: Date.yesterday, effective_date: Date.tomorrow) }
let(:sprint) { create(:agile_sprint, project:, name: "Sprint 1", start_date: Date.yesterday, finish_date: Date.tomorrow) }
let(:story_points) { 5 }
let(:story) do
create(:story,
create(:work_package,
subject: "Test Story Subject",
project:,
type: type_feature,
@@ -50,15 +50,11 @@ RSpec.describe Backlogs::StoryComponent, type: :component do
priority: default_priority,
story_points:,
position: 1,
version: sprint)
sprint: sprint)
end
let(:permissions) { %i[manage_sprint_items] }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s)
mock_permissions_for(current_user) do |mock|
mock.allow_in_project(*permissions, project:)
end
@@ -93,7 +89,7 @@ RSpec.describe Backlogs::StoryComponent, type: :component do
expect(page).to have_css("action-menu")
expect(page).to have_css(%(include-fragment[src*="menu"]))
expect(page).to have_element(:button, id: /\Astory_#{story.id}_menu-button\z/)
expect(page).to have_element(:button, id: /\Awork_package_#{story.id}_menu-button\z/)
end
describe "drag handle behaviour" do
@@ -39,11 +39,11 @@ RSpec.describe Backlogs::StoryMenuListComponent, type: :component do
current_user { user }
let(:project) { create(:project, types: [type_feature, type_task]) }
let(:sprint) { create(:sprint, project:, name: "Sprint 1", start_date: Date.yesterday, effective_date: Date.tomorrow) }
let(:sprint) { create(:agile_sprint, project:, name: "Sprint 1", start_date: Date.yesterday, finish_date: Date.tomorrow) }
let(:position) { 2 }
let(:max_position) { 3 }
let(:story) do
create(:story,
create(:work_package,
subject: "Test Story",
project:,
type: type_feature,
@@ -51,13 +51,7 @@ RSpec.describe Backlogs::StoryMenuListComponent, type: :component do
priority: default_priority,
story_points: 5,
position:,
version: sprint)
end
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [type_feature.id.to_s], "task_type" => type_task.id.to_s)
sprint: sprint)
end
def render_component(position: 2, max_position: 3)
@@ -69,11 +63,11 @@ RSpec.describe Backlogs::StoryMenuListComponent, type: :component do
it "renders stable ids for the list and primary actions" do
render_component
expect(page).to have_element(:ul, id: /\Astory_#{story.id}_menu-list\z/)
expect(page).to have_element(:a, id: /\Astory_#{story.id}_menu_open_details\z/)
expect(page).to have_element(:a, id: /\Astory_#{story.id}_menu_open_fullscreen\z/)
expect(page).to have_element(:"clipboard-copy", id: /\Astory_#{story.id}_menu_copy_url_to_clipboard\z/)
expect(page).to have_element(:"clipboard-copy", id: /\Astory_#{story.id}_menu_copy_work_package_id\z/)
expect(page).to have_element(:ul, id: /\Awork_package_#{story.id}_menu-list\z/)
expect(page).to have_element(:a, id: /\Awork_package_#{story.id}_menu_open_details\z/)
expect(page).to have_element(:a, id: /\Awork_package_#{story.id}_menu_open_fullscreen\z/)
expect(page).to have_element(:"clipboard-copy", id: /\Awork_package_#{story.id}_menu_copy_url_to_clipboard\z/)
expect(page).to have_element(:"clipboard-copy", id: /\Awork_package_#{story.id}_menu_copy_work_package_id\z/)
end
it "shows Open details link (split view)" do
@@ -104,7 +98,7 @@ RSpec.describe Backlogs::StoryMenuListComponent, type: :component do
expect(page).to have_octicon(:copy)
expect(page).to have_element(
:"clipboard-copy",
id: "story_#{story.id}_menu_copy_url_to_clipboard",
id: "work_package_#{story.id}_menu_copy_url_to_clipboard",
value: /\/work_packages\/#{story.id}\z/,
text: "Copy URL to clipboard"
)
@@ -116,7 +110,7 @@ RSpec.describe Backlogs::StoryMenuListComponent, type: :component do
expect(page).to have_octicon(:hash)
expect(page).to have_element(
:"clipboard-copy",
id: "story_#{story.id}_menu_copy_work_package_id",
id: "work_package_#{story.id}_menu_copy_work_package_id",
value: story.id.to_s,
text: "Copy work package ID"
)
@@ -141,10 +141,6 @@ RSpec.describe WorkPackages::BaseContract, type: :model do
.to receive(:relatable)
.and_return(relatable_scope)
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
describe "story_points" do
@@ -1,3 +1,5 @@
# frozen_string_literal: true
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) the OpenProject GmbH
@@ -29,84 +31,22 @@
require "spec_helper"
RSpec.describe BacklogsSettingsController do
current_user { build_stubbed(:admin) }
let(:user) { create(:admin) }
before { login_as(user) }
describe "GET show" do
it "performs that request" do
it "renders successfully" do
get :show
expect(response).to be_successful
expect(response).to render_template :show
expect(response).to have_http_status(:ok)
end
context "as regular user" do
current_user { build_stubbed(:user) }
context "when not an admin" do
let(:user) { create(:user) }
it "fails" do
it "requires admin" do
get :show
expect(response).to have_http_status :forbidden
end
end
end
describe "PUT update" do
before do
allow(Setting).to receive(:plugin_openproject_backlogs=)
end
subject do
put :update,
params: {
settings: {
task_type:,
story_types:
}
}
end
context "with invalid settings (Regression test #35157)" do
let(:task_type) { "1234" }
let(:story_types) { ["1234"] }
it "does not update the settings" do
subject
expect(response).to render_template "show"
expect(flash[:error]).to start_with I18n.t(:notice_unsuccessful_update_with_reason, reason: "")
expect(Setting).not_to have_received(:plugin_openproject_backlogs=).with(any_args)
end
end
context "with valid settings" do
let(:task_type) { "1234" }
let(:story_types) { ["5555"] }
it "does update the settings" do
subject
expect(response).to redirect_to action: :show
expect(flash[:notice]).to include I18n.t(:notice_successful_update)
expect(flash[:error]).to be_nil
expect(Setting).to have_received(:plugin_openproject_backlogs=).with(
points_burn_direction: nil,
story_types: [5555],
task_type: 1234,
wiki_template: nil
)
end
context "with a non-admin" do
current_user { build_stubbed(:user) }
it "does not update the settings" do
subject
expect(response).not_to be_successful
expect(response).to have_http_status :forbidden
expect(Setting).not_to have_received(:plugin_openproject_backlogs=).with(any_args)
end
expect(response).to have_http_status(:forbidden)
end
end
end
@@ -1,105 +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 "spec_helper"
RSpec.describe RbSprintsController, "permissions" do
let(:sprint_project) do
create(:project, enabled_module_names: %w[work_package_tracking backlogs])
end
let(:sprint) { create(:sprint, project: sprint_project) }
let(:other_project) do
create(:project, enabled_module_names: %w[work_package_tracking backlogs]).tap do |p|
create(:member,
user: current_user,
roles: [create(:project_role, permissions: [:create_sprints])],
project: p)
end
end
let(:current_user) { create(:user) }
before do
allow(Setting).to receive(:plugin_openproject_backlogs)
.and_return({ "story_types" => ["1"], "task_type" => "2" })
login_as current_user
end
describe "#update" do
let(:original_name) { sprint.name }
let(:new_name) { "a better name!" }
context "when the user has access to a different project but not the sprint's project" do
it "does not allow updating the sprint via a foreign project_id" do
original_name # memoize before request
patch :update,
params: {
project_id: other_project.id,
id: sprint.id,
sprint: {
name: new_name
}
},
format: :turbo_stream
sprint.reload
expect(response).to have_http_status(:not_found)
expect(sprint.name).to eq(original_name)
end
end
context "when the user has access to the sprint's own project" do
before do
create(:member,
user: current_user,
roles: [create(:project_role, permissions: %i[view_work_packages view_versions create_sprints])],
project: sprint_project)
end
it "allows updating the sprint" do
skip "Incorrect permissions for updating Sprint"
patch :update,
params: {
project_id: sprint_project.id,
id: sprint.id,
sprint: {
name: new_name
}
},
format: :turbo_stream
sprint.reload
expect(sprint.name).to eq(new_name)
end
end
end
end
@@ -31,128 +31,6 @@
require "rails_helper"
RSpec.describe RbSprintsController do
describe "inline name actions" do
shared_let(:type_feature) { create(:type_feature) }
shared_let(:type_task) { create(:type_task) }
shared_let(:user) { create(:admin) }
current_user { user }
let(:visible_projects_scope) { instance_double(ActiveRecord::Relation) }
let(:visible_sprints_scope) { instance_double(ActiveRecord::Relation) }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return({ "story_types" => [type_feature.id], "task_type" => type_task.id })
allow(Project)
.to receive(:visible)
.and_return(visible_projects_scope)
allow(visible_projects_scope)
.to receive(:find)
.with(project.identifier)
.and_return(project)
allow(Sprint)
.to receive(:visible)
.and_return(visible_sprints_scope)
allow(visible_sprints_scope)
.to receive(:find)
.with(sprint.id.to_s)
.and_return(sprint)
end
describe "GET #edit_name" do
let(:project) { build_stubbed(:project) }
let(:sprint) { build_stubbed(:sprint) }
it "responds with success", :aggregate_failures do
get :edit_name, params: { project_id: project.identifier, 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: "update", target: "backlogs-backlog-header-component-#{sprint.id}"
assert_select %(turbo-stream[action="update"][target="backlogs-backlog-header-component-#{sprint.id}"][method="morph"])
expect(assigns(:project)).to eq(project)
expect(assigns(:sprint)).to eq(sprint)
expect(assigns(:backlog)).to be_a(Backlog)
end
end
describe "GET #show_name" do
let(:project) { build_stubbed(:project) }
let(:sprint) { build_stubbed(:sprint) }
it "responds with success", :aggregate_failures do
get :show_name, params: { project_id: project.identifier, 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: "update", target: "backlogs-backlog-header-component-#{sprint.id}"
assert_select %(turbo-stream[action="update"][target="backlogs-backlog-header-component-#{sprint.id}"][method="morph"])
expect(assigns(:project)).to eq(project)
expect(assigns(:sprint)).to eq(sprint)
expect(assigns(:backlog)).to be_a(Backlog)
end
end
describe "PATCH #update" do
let(:project) { build_stubbed(:project) }
let(:sprint) { build_stubbed(:sprint) }
before do
update_service = instance_double(Versions::UpdateService, call: service_result)
allow(Versions::UpdateService)
.to receive(:new)
.with(user:, model: sprint)
.and_return(update_service)
end
context "when service call succeeds" do
let(:service_result) { ServiceResult.success(result: sprint) }
it "responds with success", :aggregate_failures do
patch :update, params: { project_id: project.identifier, id: sprint.id, sprint: { name: "Updated Sprint" } },
format: :turbo_stream
expect(response).to be_successful
expect(response).to have_http_status :ok
expect(response).to have_turbo_stream action: "update", target: "backlogs-backlog-header-component-#{sprint.id}"
assert_select %(turbo-stream[action="update"][target="backlogs-backlog-header-component-#{sprint.id}"][method="morph"])
expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component"
expect(assigns(:project)).to eq(project)
expect(assigns(:sprint)).to eq(sprint)
expect(assigns(:backlog)).to be_a(Backlog)
end
end
context "when service call fails" do
let(:service_result) { ServiceResult.failure(result: sprint) }
before do
project.name = ""
end
it "responds with 422", :aggregate_failures do
patch :update, params: { project_id: project.identifier, id: sprint.id, sprint: { name: "" } },
format: :turbo_stream
expect(response).not_to be_successful
expect(response).to have_http_status :unprocessable_entity
expect(response).to have_turbo_stream action: "update", target: "backlogs-backlog-header-component-#{sprint.id}"
assert_select %(turbo-stream[action="update"][target="backlogs-backlog-header-component-#{sprint.id}"][method="morph"])
expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component"
expect(assigns(:project)).to eq(project)
expect(assigns(:sprint)).to eq(sprint)
expect(assigns(:backlog)).to be_a(Backlog)
end
end
end
end
describe "new actions" do
shared_let(:type_feature) { create(:type_feature) }
shared_let(:type_task) { create(:type_task) }
@@ -166,13 +44,6 @@ RSpec.describe RbSprintsController do
current_user { user }
before do
# Necessary to get the controller running due to check_if_plugin_is_configured
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return({ "story_types" => [type_feature.id], "task_type" => type_task.id })
end
describe "GET #new_dialog" do
it "responds with success", :aggregate_failures do
get :new_dialog, params: { project_id: project.id }, format: :turbo_stream
@@ -301,156 +172,156 @@ RSpec.describe RbSprintsController do
end
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) }
let(:source_permissions) { %i[view_sprints start_complete_sprint] }
let!(:board) { create(:board_grid_with_query, project:, linked: sprint) }
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) }
let(:source_permissions) { %i[view_sprints start_complete_sprint] }
let!(:board) { create(:board_grid_with_query, project:, linked: sprint) }
before do
create(:member,
project: source_project,
principal: user,
roles: [create(:project_role, permissions: source_permissions)])
end
it "starts the sprint and redirects to the board", :aggregate_failures do
post :start, format: :turbo_stream, params: request_params
expect(response).to be_successful
expect(response).to have_turbo_stream(action: "redirect_to")
expect(service).to have_received(:call)
end
context "without source-project start permission" do
let(:source_permissions) { %i[view_sprints] }
it "responds with forbidden and does not call the service", :aggregate_failures do
post :start, params: request_params
expect(response).not_to be_successful
expect(response).to have_http_status(:forbidden)
expect(service).not_to have_received(:call)
end
end
context "without rendered-project board access" do
let(:permissions) { all_permissions - [:show_board_views] }
it "responds with forbidden and does not call the service", :aggregate_failures do
post :start, params: request_params
expect(response).not_to be_successful
expect(response).to have_http_status(:forbidden)
expect(service).not_to have_received(:call)
end
end
before do
create(:member,
project: source_project,
principal: user,
roles: [create(:project_role, permissions: source_permissions)])
end
context "when a board already exists" do
let!(:existing_board) do
create(:board_grid_with_query,
project:,
linked: sprint)
end
it "starts the sprint and redirects to the board", :aggregate_failures do
post :start, format: :turbo_stream, params: request_params
it "starts the sprint and redirects to the board", :aggregate_failures do
post :start, format: :turbo_stream, params: request_params
expect(response).to be_successful
expect(response).to have_turbo_stream(action: "redirect_to")
expect(service).to have_received(:call)
end
expect(response).to be_successful
expect(response).to have_turbo_stream(action: "redirect_to")
expect(service).to have_received(:call)
end
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" }
allow(started_sprint).to receive(:task_board_for).with(project).and_return(board)
context "without source-project start permission" do
let(:source_permissions) { %i[view_sprints] }
ServiceResult.success(
result: started_sprint
)
end
it "creates the board, starts the sprint, and redirects to the board", :aggregate_failures do
post :start, format: :turbo_stream, params: request_params
expect(response).to be_successful
expect(response).to have_turbo_stream(action: "redirect_to")
expect(flash[:notice]).to eq(I18n.t(:notice_successful_start))
expect(service).to have_received(:call)
end
end
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
post :start, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(
I18n.t(:notice_unsuccessful_start_with_reason, reason: "something went wrong")
)
expect(sprint.reload).to be_in_planning
end
end
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
post :start, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_start))
expect(service).to have_received(:call)
end
end
context "when another sprint is already active" do
let!(:active_sprint) { create(:agile_sprint, project:, status: "active") }
let(:service_result) do
ServiceResult.failure(
result: sprint,
message: sprint.errors.full_messages.to_sentence
)
end
it "redirects back to the backlog and leaves the sprint in planning", :aggregate_failures do
post :start, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_start))
expect(service).to have_received(:call)
end
end
context "without the 'start_complete_sprint' permission" do
let(:permissions) { all_permissions - [:start_complete_sprint] }
it "responds with forbidden", :aggregate_failures do
it "responds with forbidden and does not call the service", :aggregate_failures do
post :start, params: request_params
expect(response).not_to be_successful
expect(response).to have_http_status(:forbidden)
expect(service).not_to have_received(:call)
end
end
context "when the sprint is already active" do
let!(:sprint) { create(:agile_sprint, project:, status: "active") }
let(:service_result) { ServiceResult.failure }
context "without rendered-project board access" do
let(:permissions) { all_permissions - [:show_board_views] }
it "redirects back with the default start failure message", :aggregate_failures do
it "responds with forbidden and does not call the service", :aggregate_failures do
post :start, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_start))
expect(service).to have_received(:call)
expect(response).not_to be_successful
expect(response).to have_http_status(:forbidden)
expect(service).not_to have_received(:call)
end
end
end
context "when a board already exists" do
let!(:existing_board) do
create(:board_grid_with_query,
project:,
linked: sprint)
end
it "starts the sprint and redirects to the board", :aggregate_failures do
post :start, format: :turbo_stream, params: request_params
expect(response).to be_successful
expect(response).to have_turbo_stream(action: "redirect_to")
expect(service).to have_received(:call)
end
end
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" }
allow(started_sprint).to receive(:task_board_for).with(project).and_return(board)
ServiceResult.success(
result: started_sprint
)
end
it "creates the board, starts the sprint, and redirects to the board", :aggregate_failures do
post :start, format: :turbo_stream, params: request_params
expect(response).to be_successful
expect(response).to have_turbo_stream(action: "redirect_to")
expect(flash[:notice]).to eq(I18n.t(:notice_successful_start))
expect(service).to have_received(:call)
end
end
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
post :start, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(
I18n.t(:notice_unsuccessful_start_with_reason, reason: "something went wrong")
)
expect(sprint.reload).to be_in_planning
end
end
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
post :start, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_start))
expect(service).to have_received(:call)
end
end
context "when another sprint is already active" do
let!(:active_sprint) { create(:agile_sprint, project:, status: "active") }
let(:service_result) do
ServiceResult.failure(
result: sprint,
message: sprint.errors.full_messages.to_sentence
)
end
it "redirects back to the backlog and leaves the sprint in planning", :aggregate_failures do
post :start, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_start))
expect(service).to have_received(:call)
end
end
context "without the 'start_complete_sprint' permission" do
let(:permissions) { all_permissions - [:start_complete_sprint] }
it "responds with forbidden", :aggregate_failures do
post :start, params: request_params
expect(response).not_to be_successful
expect(response).to have_http_status(:forbidden)
end
end
context "when the sprint is already active" do
let!(:sprint) { create(:agile_sprint, project:, status: "active") }
let(:service_result) { ServiceResult.failure }
it "redirects back with the default start failure message", :aggregate_failures do
post :start, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_start))
expect(service).to have_received(:call)
end
end
end
describe "POST #finish" do
@@ -471,43 +342,20 @@ RSpec.describe RbSprintsController do
end
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") }
let(:source_permissions) { %i[view_sprints start_complete_sprint] }
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") }
let(:source_permissions) { %i[view_sprints start_complete_sprint] }
before do
create(:member,
project: source_project,
principal: user,
roles: [create(:project_role, permissions: source_permissions)])
end
it "finishes the sprint and redirects to the backlog", :aggregate_failures do
post :finish, params: request_params
expect(response).to be_successful
expect(response.body).to include("action=\"redirect_to\"")
expect(response.body).to include(backlogs_project_backlogs_path(project))
expect(flash[:notice]).to eq(I18n.t(:notice_successful_finish))
expect(service).to have_received(:call)
end
context "without source-project start permission" do
let(:source_permissions) { %i[view_sprints] }
it "responds with forbidden and does not call the service", :aggregate_failures do
post :finish, params: request_params
expect(response).not_to be_successful
expect(response).to have_http_status(:forbidden)
expect(service).not_to have_received(:call)
end
end
before do
create(:member,
project: source_project,
principal: user,
roles: [create(:project_role, permissions: source_permissions)])
end
it "finishes the sprint and redirects to the backlog via turbo stream", :aggregate_failures do
post :finish, format: :turbo_stream, params: request_params
it "finishes the sprint and redirects to the backlog", :aggregate_failures do
post :finish, params: request_params
expect(response).to be_successful
expect(response.body).to include("action=\"redirect_to\"")
@@ -516,81 +364,104 @@ RSpec.describe RbSprintsController do
expect(service).to have_received(:call)
end
context "when finishing fails" do
let(:service_result) { ServiceResult.failure(message: "something went wrong") }
context "without source-project start permission" do
let(:source_permissions) { %i[view_sprints] }
it "redirects back to the backlog", :aggregate_failures do
post :finish, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(
I18n.t(:notice_unsuccessful_finish_with_reason, reason: "something went wrong")
)
expect(service).to have_received(:call)
end
end
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
post :finish, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_finish))
expect(service).to have_received(:call)
end
end
context "without the 'start_complete_sprint' permission" do
let(:permissions) { all_permissions - [:start_complete_sprint] }
it "responds with forbidden", :aggregate_failures do
it "responds with forbidden and does not call the service", :aggregate_failures do
post :finish, params: request_params
expect(response).not_to be_successful
expect(response).to have_http_status(:forbidden)
expect(service).not_to have_received(:call)
end
end
end
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
expect(response.body).to include("action=\"redirect_to\"")
expect(response.body).to include(backlogs_project_backlogs_path(project))
expect(flash[:notice]).to eq(I18n.t(:notice_successful_finish))
expect(service).to have_received(:call)
end
context "when finishing fails" do
let(:service_result) { ServiceResult.failure(message: "something went wrong") }
it "redirects back to the backlog", :aggregate_failures do
post :finish, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(
I18n.t(:notice_unsuccessful_finish_with_reason, reason: "something went wrong")
)
expect(service).to have_received(:call)
end
end
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
post :finish, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_finish))
expect(service).to have_received(:call)
end
end
context "without the 'start_complete_sprint' permission" do
let(:permissions) { all_permissions - [:start_complete_sprint] }
it "responds with forbidden", :aggregate_failures do
post :finish, params: request_params
expect(response).not_to be_successful
expect(response).to have_http_status(:forbidden)
end
end
context "when the sprint is already completed" do
let!(:sprint) { create(:agile_sprint, project:, status: "completed") }
let(:service_result) { ServiceResult.failure }
let!(:sprint) { create(:agile_sprint, project:, status: "completed") }
let(:service_result) { ServiceResult.failure }
it "redirects back with the default finish failure message", :aggregate_failures do
post :finish, params: request_params
it "redirects back with the default finish failure message", :aggregate_failures do
post :finish, params: request_params
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_finish))
expect(service).to have_received(:call)
end
expect(response).to redirect_to(backlogs_project_backlogs_path(project))
expect(flash[:alert]).to eq(I18n.t(:notice_unsuccessful_finish))
expect(service).to have_received(:call)
end
end
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" } }
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
post :finish, format: :turbo_stream, params: request_params
it "passes unfinished_action to the service and redirects via turbo stream", :aggregate_failures do
post :finish, format: :turbo_stream, params: request_params
expect(response).to be_successful
expect(response.body).to include("action=\"redirect_to\"")
expect(service).to have_received(:call)
.with(hash_including(unfinished_action: "move_to_top_of_backlog"))
end
expect(response).to be_successful
expect(response.body).to include("action=\"redirect_to\"")
expect(service).to have_received(:call)
.with(hash_including(unfinished_action: "move_to_top_of_backlog"))
end
end
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" } }
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
post :finish, format: :turbo_stream, params: request_params
it "passes unfinished_action to the service and redirects via turbo stream", :aggregate_failures do
post :finish, format: :turbo_stream, params: request_params
expect(response).to be_successful
expect(response.body).to include("action=\"redirect_to\"")
expect(service).to have_received(:call)
.with(hash_including(unfinished_action: "move_to_bottom_of_backlog"))
end
expect(response).to be_successful
expect(response.body).to include("action=\"redirect_to\"")
expect(service).to have_received(:call)
.with(hash_including(unfinished_action: "move_to_bottom_of_backlog"))
end
end
end
describe "GET #refresh_form" do
@@ -39,17 +39,8 @@ RSpec.describe RbStoriesController do
let(:user) { create(:admin) }
let(:project) { create(:project) }
let(:status) { create(:status, name: "status 1", is_default: true) }
let(:version_sprint) { create(:sprint, project:) }
let(:story) { create(:story, status:, version: version_sprint, project:) }
# Via this setting, version_sprint is used as backlog:
let!(:version_setting) { create(:version_setting, version: version_sprint, project:, display: VersionSetting::DISPLAY_RIGHT) }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return({ "story_types" => [type_feature.id], "task_type" => type_task.id })
end
let(:agile_sprint) { create(:agile_sprint, name: "Agile Sprint 1", project:) }
let(:story) { create(:work_package, status:, sprint: agile_sprint, project:) }
describe "load_story" do
subject do
@@ -58,167 +49,38 @@ RSpec.describe RbStoriesController do
format: :html
end
context "when loading from a version sprint" do
let(:load_story_id) { story.id }
let(:requested_sprint) { version_sprint }
let(:load_story_id) { story.id }
context "when the story is in the requested sprint" do
it "assigns the visible story", :aggregate_failures do
subject
expect(response).to be_successful
expect(response).to have_http_status :ok
expect(assigns(:story)).to eq(story)
end
end
context "when the story is not in the requested sprint" do
let(:requested_sprint) { create(:sprint, name: "Sprint load_story other", project:) }
it { is_expected.to have_http_status :not_found }
end
end
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 }
context "when the work package is in the requested sprint" do
let(:requested_sprint) { agile_sprint }
it "assigns the visible work package", :aggregate_failures do
subject
expect(response).to be_successful
expect(response).to have_http_status :ok
expect(assigns(:story)).to eq(work_package_in_sprint)
end
end
context "when the work package is not in the requested sprint" do
let(:requested_sprint) { create(:agile_sprint, name: "Other Sprint load_story", project:) }
it { is_expected.to have_http_status :not_found }
end
end
end
describe "PUT #move_legacy" do
context "with a user lacking project permission" do
let(:user) { create(:user) }
it "responds with 403" do
put :move_legacy, params: {
project_id: project.id,
sprint_id: version_sprint.id,
id: story.id,
target_id: "foo",
position: 1
},
format: :turbo_stream
expect(response).not_to be_successful
expect(response).to have_http_status :not_found
end
end
context "with a version from the same project" do
let(:other_version_sprint) { create(:sprint, name: "Sprint 2", project:) }
it "responds with success", :aggregate_failures do
put :move_legacy, params: {
project_id: project.id,
sprint_id: version_sprint.id,
id: story.id,
target_id: "version:#{other_version_sprint.id}",
position: 1
},
format: :turbo_stream
context "when the work package is in the requested sprint" do
let(:requested_sprint) { agile_sprint }
it "assigns the visible work package", :aggregate_failures do
subject
expect(response).to be_successful
expect(response).to have_http_status :ok
expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{version_sprint.id}"
expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{other_version_sprint.id}"
assert_select %(turbo-stream[action="replace"][target="backlogs-backlog-component-#{version_sprint.id}"][method="morph"])
assert_select %(turbo-stream[action="replace"][target="backlogs-backlog-component-#{other_version_sprint.id}"][method="morph"]) # rubocop:disable Layout/LineLength
expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component"
expect(assigns(:project)).to eq(project)
expect(assigns(:sprint)).to eq(version_sprint)
expect(assigns(:story)).to eq(story)
expect(assigns(:backlog)).to be_a(Backlog)
end
end
context "with a version from another project" do
let(:other_project) { create(:project) }
let(:other_version_sprint) { create(:sprint, name: "Sprint 2", project: other_project, sharing: "system") }
let(:story) { create(:story, status:, version: other_version_sprint, project:) }
context "when the work package is not in the requested sprint" do
let(:requested_sprint) { create(:agile_sprint, name: "Other Sprint load_story", project:) }
it "responds with success", :aggregate_failures do
put :move_legacy, params: {
project_id: project.id,
sprint_id: other_version_sprint.id,
id: story.id,
target_id: "version:#{version_sprint.id}",
position: 1
},
format: :turbo_stream
expect(response).to be_successful
expect(response).to have_http_status :ok
expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{other_version_sprint.id}"
expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{version_sprint.id}"
assert_select %(turbo-stream[action="replace"][target="backlogs-backlog-component-#{other_version_sprint.id}"][method="morph"]) # rubocop:disable Layout/LineLength
assert_select %(turbo-stream[action="replace"][target="backlogs-backlog-component-#{version_sprint.id}"][method="morph"])
expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component"
expect(assigns(:project)).to eq(project)
expect(assigns(:sprint)).to eq(other_version_sprint)
expect(assigns(:story)).to eq(story)
expect(assigns(:backlog)).to be_a(Backlog)
end
end
context "when service call fails" do
let(:other_version_sprint) { create(:sprint, name: "Sprint 2", project:) }
let(:service_result) { ServiceResult.failure(message: "Something went wrong") }
before do
update_service = instance_double(Stories::UpdateService, call: service_result)
allow(Stories::UpdateService)
.to receive(:new)
.and_return(update_service)
end
it "renders an error flash with 422", :aggregate_failures do
put :move_legacy, params: {
project_id: project.id,
sprint_id: version_sprint.id,
id: story.id,
target_id: "version:#{other_version_sprint.id}",
position: 1
},
format: :turbo_stream
expect(response).to have_http_status :unprocessable_entity
expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component"
expect(response).not_to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{version_sprint.id}"
end
it { is_expected.to have_http_status :not_found }
end
end
describe "POST #reorder" do
it "responds with success", :aggregate_failures do
post :reorder, params: { project_id: project.id, sprint_id: version_sprint.id, id: story.id, direction: "highest" },
post :reorder, params: { project_id: project.id, sprint_id: agile_sprint.id, id: story.id, direction: "highest" },
format: :turbo_stream
expect(response).to be_successful
expect(response).to have_http_status :ok
expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{version_sprint.id}"
assert_select %(turbo-stream[action="replace"][target="backlogs-backlog-component-#{version_sprint.id}"][method="morph"])
expect(response).to have_turbo_stream action: "replace", target: "backlogs-sprint-component-#{agile_sprint.id}"
assert_select %(turbo-stream[action="replace"][target="backlogs-sprint-component-#{agile_sprint.id}"][method="morph"])
expect(assigns(:project)).to eq(project)
expect(assigns(:sprint)).to eq(version_sprint)
expect(assigns(:sprint)).to eq(agile_sprint)
expect(assigns(:story)).to eq(story)
expect(assigns(:backlog)).to be_a(Backlog)
end
context "when service call fails" do
@@ -233,18 +95,17 @@ RSpec.describe RbStoriesController do
end
it "renders an error flash with 422", :aggregate_failures do
post :reorder, params: { project_id: project.id, sprint_id: version_sprint.id, id: story.id, direction: "highest" },
post :reorder, params: { project_id: project.id, sprint_id: agile_sprint.id, id: story.id, direction: "highest" },
format: :turbo_stream
expect(response).to have_http_status :unprocessable_entity
expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component"
expect(response).not_to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{version_sprint.id}"
expect(response).not_to have_turbo_stream action: "replace", target: "backlogs-sprint-component-#{agile_sprint.id}"
end
end
end
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" do
@@ -271,62 +132,6 @@ RSpec.describe RbStoriesController do
expect(assigns(:sprint)).to eq(agile_sprint)
expect(assigns(:story)).to eq(story_in_agile_sprint)
end
context "when the story has a version that is not used as backlog" do
let(:story_in_agile_sprint) { create(:work_package, status:, sprint: agile_sprint, version: version_sprint, project:) }
# Via this setting, version_sprint is NOT used as backlog:
let!(:version_setting) { create(:version_setting, version: version_sprint, project:, display: VersionSetting::DISPLAY_NONE) }
it "responds with success and moves story to Agile::Sprint, keeping the version", :aggregate_failures do
put :move, params: {
project_id: project.id,
sprint_id: agile_sprint.id,
id: story_in_agile_sprint.id,
target_id: "sprint:#{other_agile_sprint.id}",
prev_id: nil
},
format: :turbo_stream
expect(response).to be_successful
expect(response).to have_http_status :ok
expect(response).to have_turbo_stream action: "replace", target: "backlogs-sprint-component-#{agile_sprint.id}"
expect(response).to have_turbo_stream action: "replace", target: "backlogs-sprint-component-#{other_agile_sprint.id}"
assert_select %(turbo-stream[action="replace"][target="backlogs-sprint-component-#{agile_sprint.id}"])
assert_select %(turbo-stream[action="replace"][target="backlogs-sprint-component-#{other_agile_sprint.id}"])
expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component"
expect(assigns(:project)).to eq(project)
expect(assigns(:sprint)).to eq(agile_sprint)
expect(assigns(:story)).to eq(story_in_agile_sprint)
# It will preserve the version since it is not used as backlog/sprint.
expect(story_in_agile_sprint.reload.version).to eq(version_sprint)
end
end
end
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,
sprint_id: agile_sprint.id,
id: story_in_agile_sprint.id,
target_id: "version:#{version_sprint.id}",
prev_id: nil
},
format: :turbo_stream
expect(response).to be_successful
expect(response).to have_http_status :ok
expect(response).to have_turbo_stream action: "replace", target: "backlogs-sprint-component-#{agile_sprint.id}"
expect(response).to have_turbo_stream action: "replace", target: "backlogs-backlog-component-#{version_sprint.id}"
assert_select %(turbo-stream[action="replace"][target="backlogs-sprint-component-#{agile_sprint.id}"])
assert_select %(turbo-stream[action="replace"][target="backlogs-backlog-component-#{version_sprint.id}"][method="morph"])
expect(response).to have_turbo_stream action: "flash", target: "op-primer-flash-component"
expect(assigns(:project)).to eq(project)
expect(assigns(:sprint)).to eq(agile_sprint)
expect(assigns(:story)).to eq(story_in_agile_sprint)
expect(assigns(:backlog)).to be_a(Backlog)
end
end
context "with Inbox as target" do
@@ -388,7 +193,7 @@ RSpec.describe RbStoriesController do
describe "GET #menu" do
subject do
get :menu, params: { project_id: project.id, sprint_id: version_sprint.id, id: story.id }, format: :html
get :menu, params: { project_id: project.id, sprint_id: agile_sprint.id, id: story.id }, format: :html
end
it "returns deferred action menu list HTML", :aggregate_failures do
@@ -41,12 +41,6 @@ RSpec.describe RbTaskboardsController do
current_user { user }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return({ "story_types" => [type_feature.id], "task_type" => type_task.id })
end
describe "GET show" do
let(:sprint) { create(:agile_sprint, project:) }
@@ -1,58 +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 "rails_helper"
RSpec.describe "Backlogs Admin Settings", :js do
let!(:type1) { create(:type, name: "Story", position: 1) }
let!(:type2) { create(:type_feature, position: 2) }
let!(:type3) { create(:type_task, position: 3) }
let!(:type4) { create(:type_milestone, position: 4) }
let(:story_autocompleter) { FormFields::Primerized::AutocompleteField.new("story_types", selector: "[data-test-selector='story_type_autocomplete']") }
let(:task_autocompleter) { FormFields::Primerized::AutocompleteField.new("story_types", selector: "[data-test-selector='task_type_autocomplete']") }
let(:current_user) { create(:admin) }
before do
login_as current_user
visit admin_backlogs_settings_path
end
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']"
expect(page).to have_no_css "fieldset", text: "Points burn up/down"
expect(page).to have_content "Backlog admin settings are evolving"
end
end
@@ -1,160 +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 "spec_helper"
require_relative "../../support/pages/backlogs"
RSpec.describe "Backlogs context menu", :js do
shared_let(:story_type) { create(:type_feature) }
shared_let(:task_type) { create(:type_task) }
shared_let(:project) { create(:project, types: [story_type, task_type]) }
shared_let(:user) do
create(:user,
member_with_permissions: { project => %i[add_work_packages
view_sprints
view_work_packages
assign_versions] })
end
shared_let(:sprint) do
create(:version,
project:,
name: "Sprint",
start_date: Date.yesterday,
effective_date: Date.tomorrow)
end
shared_let(:default_status) { create(:default_status) }
shared_let(:default_priority) { create(:default_priority) }
shared_let(:story) do
create(:work_package,
type: story_type,
project:,
status: default_status,
priority: default_priority,
position: 1,
story_points: 3,
version: sprint)
end
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [story_type.id.to_s],
"task_type" => task_type.id.to_s)
login_as(user)
end
let(:backlogs_page) { Pages::Backlogs.new(project) }
def within_backlog_context_menu(&)
backlogs_page.visit!
backlogs_page.within_backlog_menu(sprint, &)
end
context "when the backlog is a sprint backlog (displayed on the left, the default)" do
it "displays all menu entries" do
within_backlog_context_menu do |menu|
expect(menu).to have_selector :menuitem, count: 5
expect(menu).to have_selector :menuitem, "New story"
expect(menu).to have_selector :menuitem, "Stories/Tasks"
expect(menu).to have_selector :menuitem, "Task board"
expect(menu).to have_selector :menuitem, "Burndown chart"
expect(menu).to have_selector :menuitem, "Wiki"
end
end
end
context "when the backlog is an owner backlog (displayed on the right)" do
let!(:version_setting) do
create(:version_setting,
project:,
version: sprint,
display: VersionSetting::DISPLAY_RIGHT)
end
it "only displays 2 menu entries" do
within_backlog_context_menu do |menu|
expect(menu).to have_selector :menuitem, count: 2
expect(menu).to have_selector :menuitem, "New story"
expect(menu).to have_selector :menuitem, "Stories/Tasks"
expect(menu).to have_no_selector :menuitem, "Task board"
expect(menu).to have_no_selector :menuitem, "Burndown chart"
expect(menu).to have_no_selector :menuitem, "Wiki"
end
end
end
context "when the sprint does not have a start date" do
before do
sprint.update(start_date: nil)
end
it 'disables the "Burndown chart" menu entry' do
within_backlog_context_menu do |menu|
expect(menu).to have_selector :menuitem, "Burndown chart", disabled: true
end
end
end
context "when the sprint does not have an effective date" do
before do
sprint.update(effective_date: nil)
end
it 'disables the "Burndown chart" menu entry' do
within_backlog_context_menu do |menu|
expect(menu).to have_selector :menuitem, "Burndown chart", disabled: true
end
end
end
context "when the user does not have assign_versions permission" do
before do
RolePermission.where(permission: "assign_versions").delete_all
end
it 'does not display the "New story" menu entry' do
within_backlog_context_menu do |menu|
expect(menu).to have_no_selector :menuitem, "New story"
end
end
end
context "when the wiki module is not enabled" do
before do
project.enabled_module_names -= ["wiki"]
end
it 'does not display the "Wiki" menu entry' do
within_backlog_context_menu do |menu|
expect(menu).to have_no_selector :menuitem, "Wiki"
end
end
end
end
@@ -1,151 +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"
require_relative "../../support/pages/backlogs"
RSpec.describe "Backlogs", :js do
let(:story_type) do
create(:type_feature)
end
let(:story_type2) do
type = create(:type)
project.types << type
type
end
let(:inactive_story_type) do
create(:type)
end
let(:task_type) do
type = create(:type_task)
project.types << type
type
end
let(:user) do
create(:user,
member_with_permissions: { project => %i(add_work_packages
view_sprints
view_work_packages
assign_versions) })
end
let(:project) { create(:project) }
let(:backlog_version) { create(:version, project:) }
let!(:existing_story1) do
create(:work_package,
type: story_type,
project:,
status: default_status,
priority: default_priority,
position: 1,
story_points: 3,
version: backlog_version)
end
let!(:existing_story2) do
create(:work_package,
type: story_type,
project:,
status: default_status,
priority: default_priority,
position: 2,
story_points: 4,
version: backlog_version)
end
let!(:default_status) do
create(:default_status)
end
let!(:default_priority) do
create(:default_priority)
end
let(:backlogs_page) { Pages::Backlogs.new(project) }
before do
login_as(user)
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [story_type.id.to_s,
story_type2.id.to_s,
inactive_story_type.id.to_s],
"task_type" => task_type.id.to_s)
end
it "allows creating a new story" do
backlogs_page.visit!
backlogs_page.click_in_backlog_menu(backlog_version, "New story")
within_dialog "New work package" do
fill_in "Subject", with: "The new story"
# TODO: removed in OP #57688, to be reimplemented
# fill_in "Story Points", with: "5"
select_combo_box_option story_type2.name, from: "Type"
# saving the new story
click_on "Create"
end
expect_and_dismiss_flash type: :success, exact_message: "Successful creation."
# velocity should be summed up immediately
# TODO: removed in OP #57688, to be reimplemented
# xpect(page).to have_css(".velocity", text: "12")
# this will ensure that the page refresh is through before we check the order
backlogs_page.click_in_backlog_menu(backlog_version, "New story")
within_dialog "New work package" do
fill_in "Subject", with: "Another story"
end
# the order is kept even after a page refresh -> it is persisted in the db
page.driver.refresh
expect(page)
.to have_no_content "Another story"
new_story = WorkPackage.find_by(subject: "The new story")
# stories are ordered by position (ASC), with NULL positions at the end ordered by ID
# existing stories have positions 1 and 2, new story has no position so appears at end
backlogs_page.expect_stories_in_order(backlog_version, existing_story1, existing_story2, new_story)
# created with the selected type (HighlightedTypeComponent renders type name in uppercase)
within("#story_#{new_story.id}") do
expect(page).to have_text(story_type2.name.upcase)
end
end
end
@@ -32,7 +32,9 @@ require "spec_helper"
require_relative "../../support/pages/backlog"
require_relative "../../../../boards/spec/features/support/board_page"
RSpec.describe "Start and finish sprints", :js do
RSpec.describe "Start and finish sprints",
:js,
with_ee: %i[board_view] do
shared_let(:project) do
create(:project, enabled_module_names: %i[backlogs work_package_tracking board_view])
end
@@ -47,6 +49,7 @@ RSpec.describe "Start and finish sprints", :js do
create(:user, member_with_permissions: { project => permissions })
end
let(:planning_page) { Pages::Backlog.new(project) }
let(:story_type) { create(:type_feature) }
let(:task_type) do
type = create(:type_task)
project.types << type
@@ -1,266 +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"
require_relative "../support/pages/backlogs"
RSpec.describe "Backlogs in backlog view", :js do
let!(:project) do
create(:project,
types: [story, task],
enabled_module_names: %w(work_package_tracking backlogs))
end
let!(:story) { create(:type_feature) }
let!(:other_story) { create(:type) }
let!(:task) { create(:type_task) }
let!(:priority) { create(:default_priority) }
let!(:default_status) { create(:status, is_default: true) }
let!(:other_status) { create(:status) }
let!(:workflows) do
create(:workflow,
old_status: default_status,
new_status: other_status,
role:,
type_id: story.id)
end
let(:role) do
create(:project_role,
permissions: %i(
view_project
view_sprints
create_sprints
manage_sprint_items
add_work_packages
view_work_packages
edit_work_packages
manage_subtasks
manage_versions
))
end
let!(:current_user) do
create(:user,
member_with_roles: { project => role })
end
let!(:sprint) do
create(:version,
project:,
start_date: 10.days.ago,
effective_date: 10.days.from_now,
version_settings_attributes: [{ project:, display: VersionSetting::DISPLAY_LEFT }])
end
let!(:backlog) do
create(:version,
project:,
version_settings_attributes: [{ project:, display: VersionSetting::DISPLAY_RIGHT }])
end
let!(:other_project) do
create(:project, member_with_roles: { current_user => role })
end
let!(:other_project_sprint) do
create(:version,
project: other_project,
sharing: "system",
start_date: 10.days.ago,
effective_date: 10.days.from_now)
end
let!(:sprint_story1) do
create(:work_package,
project:,
type: story,
status: default_status,
version: sprint,
position: 1,
story_points: 10)
end
let(:backlogs_page) { Pages::Backlogs.new(project) }
before do
login_as current_user
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [story.id.to_s],
"task_type" => task.id.to_s)
end
it "displays stories which are editable" do
backlogs_page.visit!
backlogs_page
.expect_sprint(sprint)
# Shared versions are also displayed as a sprint.
# Without version settings, it is displayed as a sprint
backlogs_page
.expect_sprint(other_project_sprint)
backlogs_page
.expect_backlog(backlog)
# Versions can be folded
backlogs_page
.expect_story_in_backlog(sprint_story1, sprint)
backlogs_page
.fold_backlog(sprint)
backlogs_page
.expect_story_not_in_backlog(sprint_story1, sprint)
# The backlogs can be folded by default
visit my_interface_path
check "Show sprints folded"
click_button "Update backlogs module"
expect_and_dismiss_flash(message: "Account was successfully updated.")
backlogs_page.visit!
backlogs_page
.expect_story_not_in_backlog(sprint_story1, sprint)
backlogs_page
.fold_backlog(sprint)
backlogs_page
.expect_story_in_backlog(sprint_story1, sprint)
# Alter the attributes of the sprint
sleep(0.5)
backlogs_page
.edit_backlog(sprint, name: "")
backlogs_page
.expect_and_dismiss_error("Name can't be blank.")
sleep(0.2)
backlogs_page
.edit_backlog(sprint,
name: "New sprint name",
start_date: 5.days.from_now,
effective_date: 20.days.from_now)
sleep(0.5)
sprint.reload
expect(sprint.name)
.to eql "New sprint name"
expect(sprint.start_date)
.to eql Date.today + 5.days
expect(sprint.effective_date)
.to eql Date.today + 20.days
# Alter displaying a sprints as a backlog
backlogs_page
.click_in_backlog_menu(sprint, "Properties")
select "right", from: "Column in backlog"
click_button "Save"
expect_and_dismiss_flash(message: "Successful update.")
backlogs_page
.expect_backlog(sprint)
# The others are unchanged
backlogs_page
.expect_backlog(backlog)
backlogs_page
.expect_sprint(other_project_sprint)
# Alter displaying a backlog as a sprint
backlogs_page
.click_in_backlog_menu(backlog, "Properties")
select "left", from: "Column in backlog"
click_button "Save"
expect_and_dismiss_flash(message: "Successful update.")
# Now works as a sprint instead of a backlog
backlogs_page
.expect_sprint(backlog)
# The others are unchanged
backlogs_page
.expect_backlog(sprint)
backlogs_page
.expect_sprint(other_project_sprint)
# Alter displaying a version not at all
backlogs_page
.click_in_backlog_menu(backlog, "Properties")
select "none", from: "Column in backlog"
click_button "Save"
expect_and_dismiss_flash(message: "Successful update.")
# the disabled backlog/sprint is no longer visible
expect(page)
.to have_no_content(backlog.name)
# The others are unchanged
backlogs_page
.expect_backlog(sprint)
backlogs_page
.expect_sprint(other_project_sprint)
# Inherited versions can also be modified
backlogs_page
.click_in_backlog_menu(other_project_sprint, "Properties")
select "none", from: "Column in backlog"
click_button "Save"
expect_and_dismiss_flash(message: "Successful update.")
# the disabled backlog/sprint is no longer visible
expect(page)
.to have_no_content(other_project_sprint.name)
# The others are unchanged
backlogs_page
.expect_backlog(sprint)
expect(page)
.to have_no_content(backlog.name)
end
end
@@ -37,11 +37,6 @@ RSpec.describe "Empty backlogs project",
before do
login_as current_user
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [story.id.to_s],
"task_type" => task.id.to_s)
visit backlogs_project_backlogs_path(project)
end
@@ -49,9 +44,15 @@ RSpec.describe "Empty backlogs project",
let(:current_user) { create(:admin) }
it "shows blankslate with description" do
within ".blankslate" do
expect(page).to have_heading(I18n.t(:backlogs_empty_title))
expect(page).to have_text(I18n.t(:backlogs_empty_action_text))
within "#owner_backlogs_container .blankslate" do
expect(page).to have_heading(I18n.t(:"backlogs.inbox_component.blankslate_title"))
expect(page).to have_text(I18n.t(:"backlogs.inbox_component.blankslate_description"))
end
within "#sprint_backlogs_container .blankslate" do
expect(page).to have_heading(I18n.t(:"backlogs.backlog.blankslate.title"))
expect(page).to have_text(I18n.t(:"backlogs.backlog.blankslate.description_html",
settings_link: "project settings"))
end
end
end
@@ -61,9 +62,14 @@ RSpec.describe "Empty backlogs project",
let(:current_user) { create(:user, member_with_roles: { project => role }) }
it "shows a blankslate without description" do
within ".blankslate" do
expect(page).to have_heading(I18n.t(:backlogs_empty_title))
expect(page).to have_no_text(I18n.t(:backlogs_empty_action_text))
within "#owner_backlogs_container .blankslate" do
expect(page).to have_heading(I18n.t(:"backlogs.inbox_component.blankslate_title"))
expect(page).to have_text(I18n.t(:"backlogs.inbox_component.blankslate_description"))
end
within "#sprint_backlogs_container .blankslate" do
expect(page).to have_heading(I18n.t(:"backlogs.backlog.blankslate.title"))
expect(page).to have_text(I18n.t(:"backlogs.backlog.blankslate.no_actions_description_text"))
end
end
end
@@ -1,190 +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 "Impediments on taskboard", :js,
:selenium do
let!(:project) do
create(:project,
types: [story_type, task_type],
enabled_module_names: %w(work_package_tracking backlogs))
end
let!(:story_type) { create(:type_feature) }
let!(:task_type) { create(:type_task) }
let!(:priority) { create(:default_priority) }
let!(:status) { create(:status, is_default: true) }
let!(:other_status) { create(:status) }
let!(:workflows) do
create(:workflow,
old_status: status,
new_status: other_status,
role:,
type_id: story_type.id)
create(:workflow,
old_status: status,
new_status: other_status,
role:,
type_id: task_type.id)
end
let(:role) do
create(:project_role,
permissions: %i(view_sprints
add_work_packages
view_work_packages
edit_work_packages
manage_subtasks
assign_versions
work_package_assigned))
end
let!(:current_user) do
create(:user,
member_with_roles: { project => role })
end
let!(:task1) do
create(:work_package,
status:,
project:,
type: task_type,
version: sprint,
parent: story1)
end
let!(:story1) do
create(:work_package,
project:,
type: story_type,
version: sprint)
end
let!(:other_task) do
create(:work_package,
project:,
type: task_type,
version: sprint,
parent: other_story)
end
let!(:other_story) do
create(:work_package,
project:,
type: story_type,
version: other_sprint)
end
let!(:sprint) do
create(:version, project:)
end
let!(:other_sprint) do
create(:version, project:)
end
before do
login_as current_user
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [story_type.id.to_s],
"task_type" => task_type.id.to_s)
end
it "allows creating and updating impediments" do
visit backlogs_project_sprint_taskboard_path(project, sprint)
find("#impediments .add_new").click
fill_in "subject", with: "New impediment"
fill_in "blocks_ids", with: task1.id
select current_user.name, from: "assigned_to_id"
click_on "OK"
# Saves successfully
expect(page)
.to have_css("div.impediment", text: "New impediment")
expect(page)
.to have_no_css("div.impediment.error", text: "New impediment")
# Attempt to create a new impediment with the id of a story from another sprint
find("#impediments .add_new").click
fill_in "subject", with: "Other sprint impediment"
fill_in "blocks_ids", with: other_story.id
click_on "OK"
# Saves unsuccessfully
expect(page)
.to have_css("div.impediment", text: "Other sprint impediment")
expect(page)
.to have_css("div.impediment.error", text: "Other sprint impediment")
expect(page)
.to have_css("#msgBox",
text: "IDs of blocked work packages can only contain IDs of work packages in the current sprint.")
click_on "OK"
# Attempt to create a new impediment with a non existing id
find("#impediments .add_new").click
fill_in "subject", with: "Invalid id impediment"
fill_in "blocks_ids", with: "0"
click_on "OK"
# Saves unsuccessfully
expect(page)
.to have_css("div.impediment", text: "Invalid id impediment")
expect(page)
.to have_css("div.impediment.error", text: "Invalid id impediment")
expect(page)
.to have_css("#msgBox",
text: "IDs of blocked work packages can only contain IDs of work packages in the current sprint.")
click_on "OK"
# Attempt to create a new impediment without specifying the blocked story/task
find("#impediments .add_new").click
fill_in "subject", with: "Unblocking impediment"
click_on "OK"
# Saves unsuccessfully
expect(page)
.to have_css("div.impediment", text: "Unblocking impediment")
expect(page)
.to have_css("div.impediment.error", text: "Unblocking impediment")
expect(page)
.to have_css("#msgBox", text: "IDs of blocked work packages must contain the ID of at least one ticket")
click_on "OK"
# Updating an impediment
find("#impediments .subject", text: "New impediment").click
fill_in "subject", with: "Updated impediment"
fill_in "blocks_ids", with: story1.id
click_on "OK"
# Saves successfully
expect(page)
.to have_css("div.impediment", text: "Updated impediment")
expect(page)
.to have_no_css("div.impediment.error", text: "Updated impediment")
end
end
@@ -1,209 +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 "spec_helper"
require_relative "../support/pages/backlogs"
RSpec.describe "Stories in backlog", :js, :settings_reset do
let!(:project) do
create(:project,
types: [story, task, other_story],
enabled_module_names: %w(work_package_tracking backlogs))
end
let!(:story) { create(:type_feature) }
let!(:other_story) { create(:type, name: "Story") }
let!(:task) { create(:type_task) }
let!(:priority) { create(:default_priority) }
let!(:default_status) { create(:status, is_default: true) }
let!(:other_status) { create(:status) }
let!(:workflows) do
create(:workflow,
old_status: default_status,
new_status: other_status,
role:,
type_id: story.id)
end
let(:role) do
create(:project_role,
permissions: %i(view_sprints
assign_versions
add_work_packages
view_work_packages
edit_work_packages
manage_subtasks))
end
let!(:current_user) do
create(:user,
member_with_roles: { project => role })
end
let!(:sprint_story1) do
create(:work_package,
project:,
type: story,
status: default_status,
version: sprint,
position: 1,
story_points: 8)
end
let!(:sprint_story1_task) do
create(:work_package,
project:,
type: task,
status: default_status,
version: sprint)
end
let!(:sprint_story2_parent) do
create(:work_package,
project:,
type: create(:type),
status: default_status,
version: sprint)
end
let!(:sprint_story2) do
create(:work_package,
project:,
type: story,
status: default_status,
version: sprint,
position: 2,
story_points: 13)
end
let!(:backlog_story1) do
create(:work_package,
project:,
type: story,
status: default_status,
version: backlog)
end
let!(:sprint) do
create(:version,
project:,
start_date: Time.zone.today - 10.days,
effective_date: Time.zone.today + 10.days,
version_settings_attributes: [{ project:, display: VersionSetting::DISPLAY_LEFT }])
end
let!(:backlog) do
create(:version,
project:,
version_settings_attributes: [{ project:, display: VersionSetting::DISPLAY_RIGHT }])
end
let!(:other_project) do
create(:project).tap do |p|
create(:member,
principal: current_user,
project: p,
roles: [role])
end
end
let!(:sprint_story_in_other_project) do
create(:work_package,
project: other_project,
type: story,
status: default_status,
version: sprint,
story_points: 5)
end
let(:backlogs_page) { Pages::Backlogs.new(project) }
before do
Setting.plugin_openproject_backlogs = {
"story_types" => [story.id.to_s, other_story.id.to_s],
"task_type" => task.id.to_s
}
login_as current_user
backlogs_page.visit!
end
it "displays stories in correct order, calculates velocity, and allows editing story points" do
backlogs_page
.expect_story_in_backlog(sprint_story1, sprint)
backlogs_page
.expect_story_in_backlog(sprint_story2, sprint)
backlogs_page
.expect_story_in_backlog(backlog_story1, backlog)
backlogs_page
.expect_story_not_in_backlog(sprint_story2_parent, sprint)
backlogs_page
.expect_story_not_in_backlog(sprint_story1_task, sprint)
backlogs_page
.expect_story_not_in_backlog(sprint_story_in_other_project, sprint)
backlogs_page
.expect_stories_in_order(sprint, sprint_story1, sprint_story2)
# Velocity is calculated by summing up all story points in a sprint
backlogs_page.expect_velocity(sprint, 21)
backlogs_page
.edit_story_in_details_view(sprint_story1, story_points: 5)
backlogs_page.expect_velocity(sprint, 18)
backlogs_page
.edit_story_in_details_view(sprint_story2, subject: "Updated story", story_points: 3)
backlogs_page.expect_velocity(sprint, 8)
end
it "moves story from sprint to backlog when version is changed via details view" do
backlogs_page
.edit_story_in_details_view(sprint_story1, version: backlog)
backlogs_page.expect_story_not_in_backlog(sprint_story1, sprint)
backlogs_page.expect_story_in_backlog(sprint_story1, backlog)
end
it "switches the details view from one story to another" do
backlogs_page
.click_in_story_menu(sprint_story1, "Open details view")
backlogs_page.expect_details_view(sprint_story1)
backlogs_page.expect_story_in_backlog(sprint_story2, sprint)
backlogs_page
.click_in_story_menu(sprint_story2, "Open details view")
backlogs_page.expect_details_view(sprint_story2)
backlogs_page.expect_story_in_backlog(sprint_story1, sprint)
end
it "removes story from sprint when type is changed to non-story type via details view" do
backlogs_page
.edit_story_in_details_view(sprint_story2, type: task.name)
backlogs_page.expect_story_not_in_backlog(sprint_story2, sprint)
end
end
@@ -1,254 +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"
require_relative "../support/pages/taskboard"
RSpec.describe "Tasks on taskboard", :js,
:selenium do
let!(:project) do
create(:project,
types: [story, task, other_story],
enabled_module_names: %w(work_package_tracking backlogs))
end
let!(:story) { create(:type_feature) }
let!(:other_story) { create(:type) }
let!(:task) { create(:type_task) }
let!(:priority) { create(:default_priority) }
let!(:default_status) { create(:status, is_default: true) }
let!(:other_status) { create(:status) }
let!(:workflows) do
create(:workflow,
old_status: default_status,
new_status: other_status,
role:,
type_id: task.id)
end
let(:role) do
create(:project_role,
permissions: %i(view_sprints
add_work_packages
view_work_packages
edit_work_packages
manage_subtasks
assign_versions
work_package_assigned))
end
let!(:current_user) do
create(:user,
member_with_roles: { project => role })
end
let!(:story1) do
create(:work_package,
project:,
type: story,
status: default_status,
version: sprint,
position: 1,
story_points: 10)
end
let!(:story1_task) do
create(:work_package,
project:,
parent: story1,
type: task,
status: default_status,
version: sprint)
end
let!(:story1_task_subtask) do
create(:work_package,
project:,
parent: story1_task,
type: task,
status: default_status,
version: sprint)
end
let!(:other_work_package) do
create(:work_package,
project:,
type: create(:type),
status: default_status,
version: sprint)
end
let!(:other_work_package_subtask) do
create(:work_package,
project:,
parent: other_work_package,
type: task,
status: default_status,
version: sprint)
end
let!(:story2) do
create(:work_package,
project:,
type: story,
status: default_status,
version: sprint,
position: 2,
story_points: 20)
end
let!(:sprint) do
create(:version,
project:,
start_date: Date.today - 10.days,
effective_date: Date.today + 10.days,
version_settings_attributes: [{ project:, display: VersionSetting::DISPLAY_LEFT }])
end
let!(:other_project) do
create(:project).tap do |p|
create(:member,
principal: current_user,
project: p,
roles: [role])
end
end
let!(:story_in_other_project) do
create(:work_package,
project: other_project,
type: story,
status: default_status,
version: sprint,
story_points: 10)
end
let(:taskboard_page) { Pages::Taskboard.new(project, sprint) }
before do
login_as current_user
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [story.id.to_s, other_story.id.to_s],
"task_type" => task.id.to_s)
end
it "displays stories which are editable" do
taskboard_page.visit!
# All stories of the sprint are visible
taskboard_page
.expect_story(story1)
taskboard_page
.expect_story(story2)
# All tasks of the sprint are visible
taskboard_page
.expect_task(story1_task)
# Other work packages also assigned to the sprint are not visible
taskboard_page
.expect_work_package_not_visible(other_work_package)
# Tasks that have a non story as their parent are not visible
taskboard_page
.expect_work_package_not_visible(other_work_package_subtask)
# Tasks that have a task and not a story as their parent are not visible
taskboard_page
.expect_work_package_not_visible(story1_task_subtask)
# The task is in the first status column belonging to its parent story
taskboard_page
.expect_task_in_story_column(story1_task, story1, 1)
# Adding a task will have it added to the same sprint and belonging to the story
taskboard_page
.add_task(story1,
subject: "Added task",
assignee: current_user.name,
remaining_hours: 7)
added_task = WorkPackage.find_by(subject: "Added task")
expect(added_task.version)
.to eql sprint
expect(added_task.parent)
.to eql story1
# Added task will also be displayed
taskboard_page
.expect_task_in_story_column(added_task, story1, 1)
# Updating a task
taskboard_page
.update_task(story1_task,
subject: "Updated task",
assignee: current_user.name)
story1_task.reload
expect(story1_task.subject)
.to eql "Updated task"
# Dragging a task within the same column (switching order)
taskboard_page
.drag_to_task(story1_task, added_task, :before)
taskboard_page
.expect_task_in_story_column(added_task, story1, 1)
taskboard_page
.expect_task_in_story_column(story1_task, story1, 1)
sleep(0.5)
expect(added_task.reload.higher_item.id)
.to eql story1_task.id
# Dragging a task to the next column (switching status)
taskboard_page
.drag_to_column(story1_task, story1, 2)
taskboard_page
.expect_task_in_story_column(story1_task, story1, 2)
sleep(0.5)
expect(story1_task.reload.status)
.to eql other_status
# There is a button to the burndown chart
expect(page)
.to have_css("a[href='#{backlogs_project_sprint_burndown_chart_path(project, sprint)}']",
text: "Burndown chart")
# Tasks can get a color per assigned user
visit my_interface_path
fill_in "Task color", with: "#FBC4B3"
click_button "Update backlogs module"
expect_and_dismiss_flash(message: "Account was successfully updated.")
taskboard_page.visit!
taskboard_page
.expect_color_for_task("#FBC4B3", story1_task)
end
end
@@ -75,13 +75,6 @@ RSpec.describe "Create work package in sprint", :js do
end
before do
# Faulty and mostly irrelevant for the test. Only needed to make the sprints appear on the page.
# To be removed once the setting is removed.
Setting.plugin_openproject_backlogs = {
"story_types" => [type.id.to_s],
"task_type" => type.id.to_s
}
backlogs_page.visit!
end
@@ -77,13 +77,6 @@ RSpec.describe "Dragging work packages in and between sprints",
end
before do
# Faulty and mostly irrelevant for the test. Only needed to make the sprints appear on the page.
# To be removed once the setting is removed.
Setting.plugin_openproject_backlogs = {
"story_types" => [type.id.to_s],
"task_type" => type.id.to_s
}
backlogs_page.visit!
end
@@ -50,8 +50,14 @@ RSpec.describe "Filter work packages by backlog filters", :js do
shared_let(:work_package_in_own_sprint) { create(:work_package, type: task_type, project:, sprint: own_sprint) }
shared_let(:work_package_in_shared_sprint) { create(:work_package, type: task_type, project:, sprint: shared_sprint) }
let(:user) { create(:user, member_with_permissions: { project => permissions }) }
let(:permissions) { %i(view_work_packages save_queries view_sprints) }
let(:user) do
create(:user,
member_with_permissions: {
project => permissions,
shared_sprint.project => %i[show_board_views view_sprints]
})
end
let(:permissions) { %i(view_work_packages save_queries show_board_views view_sprints) }
let(:wp_table) { Pages::WorkPackagesTable.new(project) }
let(:filters) { Components::WorkPackages::Filters.new }
@@ -59,60 +65,9 @@ RSpec.describe "Filter work packages by backlog filters", :js do
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)
wp_table.visit!
end
context "on the backlog type" do
it "allows filtering, saving and retaining the filter" do
filters.open
filters.add_filter_by("Backlog type", "is (OR)", "Story", "backlogsWorkPackageType")
wp_table.expect_work_package_listed work_package_with_story_type
wp_table.ensure_work_package_not_listed! work_package_with_task_type
wp_table.save_as("Some query name")
filters.remove_filter "backlogsWorkPackageType"
wp_table.expect_work_package_listed work_package_with_story_type, work_package_with_task_type
last_query = Query.last
wp_table.visit_query(last_query)
wp_table.expect_work_package_listed work_package_with_story_type
wp_table.ensure_work_package_not_listed! work_package_with_task_type
filters.open
filters.expect_filter_by("Backlog type", "is (OR)", "Story", "backlogsWorkPackageType")
end
it "can filter by task or any" do
filters.open
filters.add_filter_by("Backlog type", "is (OR)", "Story", "backlogsWorkPackageType")
wp_table.ensure_work_package_not_listed! work_package_with_task_type
filters.remove_filter "backlogsWorkPackageType"
filters.add_filter_by("Backlog type", "is (OR)", "Task", "backlogsWorkPackageType")
wp_table.expect_work_package_listed work_package_with_task_type
filters.remove_filter "backlogsWorkPackageType"
filters.add_filter_by("Backlog type", "is (OR)", "any", "backlogsWorkPackageType")
wp_table.expect_work_package_listed work_package_with_story_type, work_package_with_task_type
end
end
context "on the sprint" do
shared_examples_for "filtering on sprints" do
it "allows filtering by sprint" do
@@ -125,9 +80,8 @@ RSpec.describe "Filter work packages by backlog filters", :js do
work_package_with_task_type
wp_table.expect_work_package_listed work_package_in_own_sprint
filters.remove_filter "sprint"
filters.add_filter_by("Sprint", "is (OR)", shared_sprint.name)
filters.clear_filter_value "sprint"
filters.set_filter("Sprint", "is (OR)", shared_sprint.name)
wp_table.ensure_work_package_not_listed! work_package_in_own_sprint,
work_package_with_story_type,
@@ -31,10 +31,6 @@ require "spec_helper"
RSpec.describe "Work packages having story points", :js do
before do
login_as current_user
allow(Setting).to receive(:plugin_openproject_backlogs).and_return("points_burn_direction" => "down",
"wiki_template" => "",
"story_types" => [story_type.id.to_s],
"task_type" => task_type.id.to_s)
end
let(:current_user) { create(:admin) }
@@ -1,68 +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 "rails_helper"
RSpec.describe Admin::Settings::BacklogsSettingsForm, type: :forms do
include_context "with rendered form"
let(:form_arguments) { { url: "/foo", model: false, scope: :settings } }
subject(:rendered_form) do
vc_render_form
page
end
it "renders", :aggregate_failures do
expect(rendered_form).to have_element "opce-autocompleter", "data-label-for-id": "\"settings_story_types\"" do |autocompleter|
expect(autocompleter["data-multiple"]).to be_json_eql(%{true})
end
expect(rendered_form).to have_element "opce-autocompleter", "data-label-for-id": "\"settings_task_type\"" do |autocompleter|
expect(autocompleter["data-multiple"]).to be_json_eql(%{false})
end
expect(rendered_form).to have_field "Template for sprint wiki page", type: :text do |field|
expect(field["name"]).to eq "settings[wiki_template]"
end
expect(rendered_form).to have_field "Up", type: :radio do |field|
expect(field["name"]).to eq "settings[points_burn_direction]"
expect(field["value"]).to eq "up"
end
expect(rendered_form).to have_field "Down", type: :radio do |field|
expect(field["name"]).to eq "settings[points_burn_direction]"
expect(field["value"]).to eq "down"
end
expect(rendered_form).to have_button "Save", type: "submit"
end
end
@@ -1,43 +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 "rails_helper"
RSpec.describe Admin::Settings::BacklogsSettingsModel, type: :model do
describe "validations" do
subject(:model) { described_class.new(story_types: [1, 2, 3]) }
it "validates that a story type cannot be used as a task type" do
expect(subject).to validate_exclusion_of(:task_type)
.in_array([1, 2, 3])
.with_message(I18n.t("errors.attributes.task_type.cannot_be_story_type"))
end
end
end
@@ -52,7 +52,6 @@ RSpec.describe API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
login_as(current_user)
allow(schema.project).to receive(:backlogs_enabled?).and_return(true)
allow(work_package.type).to receive(:story?).and_return(true)
allow(work_package).to receive(:leaf?).and_return(true)
end
@@ -77,19 +76,6 @@ RSpec.describe API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
end
end
context "when not a story" do
before do
allow(schema.type).to receive(:story?).and_return(false)
end
it_behaves_like "has basic schema properties" do
let(:path) { "storyPoints" }
let(:type) { "Integer" }
let(:name) { I18n.t("activerecord.attributes.work_package.story_points") }
let(:required) { false }
let(:writable) { true }
end
end
end
describe "position" do
@@ -111,19 +97,6 @@ RSpec.describe API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
end
end
context "when not a story" do
before do
allow(schema.type).to receive(:story?).and_return(false)
end
it_behaves_like "has basic schema properties" do
let(:path) { "position" }
let(:type) { "Integer" }
let(:name) { I18n.t("activerecord.attributes.work_package.position") }
let(:required) { false }
let(:writable) { false }
end
end
end
describe "sprint" do
@@ -161,18 +134,5 @@ RSpec.describe API::V3::WorkPackages::Schema::WorkPackageSchemaRepresenter do
end
end
context "when not a story" do
before do
allow(schema.type).to receive(:story?).and_return(false)
end
it_behaves_like "has basic schema properties" do
let(:type) { "Sprint" }
let(:name) { I18n.t("activerecord.attributes.work_package.sprint") }
let(:required) { false }
let(:writable) { true }
let(:location) { "_links" }
end
end
end
end
@@ -65,11 +65,6 @@ RSpec.describe API::V3::WorkPackages::WorkPackageRepresenter, "rendering" do
current_user { build_stubbed(:user) }
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return("story_types" => [story_type.id.to_s],
"task_type" => task_type.id.to_s)
mock_permissions_for(current_user) do |mock|
permissions.each do |permission|
mock.allow_in_project(*permission, project:) if project
@@ -32,9 +32,6 @@ RSpec.describe Backlog do
let(:project) { build(:project) }
before do
@feature = create(:type_feature)
allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ "story_types" => [@feature.id.to_s],
"task_type" => "0" })
@status = create(:status)
end
@@ -77,19 +74,6 @@ RSpec.describe Backlog do
end
end
describe "#owner_backlogs" do
describe "WITH one open version defined in the project" do
before do
@project = project
@work_packages = [create(:work_package, subject: "work_package1", project: @project, type: @feature,
status: @status)]
@version = create(:version, project:, work_packages: @work_packages)
@version_settings = @version.version_settings.create(display: VersionSetting::DISPLAY_RIGHT, project:)
end
it { expect(Backlog.owner_backlogs(@project)[0]).to be_owner_backlog }
end
end
end
describe "ActiveModel naming" do
+263 -110
View File
@@ -55,123 +55,214 @@ 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 "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 "for a version sprint" do
let(:version) { create(:version, project:) }
let(:sprint) { Sprint.find(version.id) }
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 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
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 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
describe "WITH the story having story_point defined on creation" do
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 a work package of another type assigned to the sprint" do
let(:other_type) { create(:type) }
let(:other_work_package) do
build(:work_package,
subject: "Other work package",
project:,
version:,
type: other_type,
status: issue_open,
priority: issue_priority,
story_points: 7,
created_at: Time.zone.today - 20.days,
updated_at: Time.zone.today - 20.days)
end
before do
story.story_points = 9
story.save!
story.last_journal.update_columns(created_at: story.created_at, updated_at: story.created_at)
other_work_package.save!
other_work_package.last_journal.update_columns(created_at: other_work_package.created_at,
updated_at: other_work_package.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(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] }
it "includes its story points in the burndown" do
expect(burndown.story_points).to eql(Array.new(burndown.story_points.size, 7.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)
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
stories
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 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
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 5 stories having been reduced to 0 story points, one story per day" do
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
5.times do |i|
set_attribute_journalized stories[i], :story_points=, nil, sprint.start_date + i.days + 1.hour
stories.each_with_index do |s, _i|
set_attribute_journalized s, :story_points=, 10, version.start_date - 3.days
end
end
describe "THEN" do
it { expect(burndown.story_points).to eql [90.0, 80.0, 70.0, 60.0, 50.0, 50.0] }
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
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 }
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!
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)
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 {
@@ -179,32 +270,94 @@ RSpec.describe Burndown do
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] }
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 {
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
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
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
it "generates an empty burndown" do
expect(burndown.series[:story_points]).to be_empty
end
end
end
end
@@ -1,123 +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 Impediment do
let(:user) { @user ||= create(:user) }
let(:role) { @role ||= create(:project_role) }
let(:type_feature) { @type_feature ||= create(:type_feature) }
let(:type_task) { @type_task ||= create(:type_task) }
let(:issue_priority) { @issue_priority ||= create(:priority, is_default: true) }
let(:status) { create(:status) }
let(:task) do
build(:task, type: type_task,
project:,
author: user,
priority: issue_priority,
status:)
end
let(:feature) do
build(:work_package, type: type_feature,
project:,
author: user,
priority: issue_priority,
status:)
end
let(:version) { create(:version, project:) }
let(:project) do
unless @project
@project = build(:project, types: [type_feature, type_task])
@project.members = [build(:member, principal: user,
project: @project,
roles: [role])]
end
@project
end
let(:impediment) do
build(:impediment, author: user,
version:,
assigned_to: user,
priority: issue_priority,
project:,
type: type_task,
status:)
end
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 })
login_as user
end
describe "instance methods" do
describe "blocks_ids=/blocks_ids" do
describe "WITH an integer" do
it do
impediment.blocks_ids = 2
expect(impediment.blocks_ids).to eql [2]
end
end
describe "WITH a string" do
it do
impediment.blocks_ids = "1, 2, 3"
expect(impediment.blocks_ids).to eql [1, 2, 3]
end
end
describe "WITH an array" do
it do
impediment.blocks_ids = [1, 2, 3]
expect(impediment.blocks_ids).to eql [1, 2, 3]
end
end
describe "WITH loading from the backend" do
before do
feature.version = version
feature.save
task.version = version
task.save
impediment.blocks_ids = [feature.id, task.id]
impediment.save
end
it { expect(described_class.find(impediment.id).blocks_ids).to eql [feature.id, task.id] }
end
end
end
end
-200
View File
@@ -1,200 +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 Story do
let(:user) { @user ||= create(:user) }
let(:role) { @role ||= create(:project_role) }
let(:status1) { @status1 ||= create(:status, name: "status 1", is_default: true) }
let(:type_feature) { @type_feature ||= create(:type_feature) }
let(:version) { @version ||= create(:version, project:) }
let(:version2) { create(:version, project:) }
let(:sprint) { @sprint ||= create(:sprint, project:) }
let(:issue_priority) { @issue_priority ||= create(:priority) }
let(:task_type) { create(:type_task) }
let(:task) do
create(:story, version:,
project:,
status: status1,
type: task_type,
priority: issue_priority)
end
let(:story1) do
create(:story, version:,
project:,
status: status1,
type: type_feature,
priority: issue_priority)
end
let(:story2) do
create(:story, version:,
project:,
status: status1,
type: type_feature,
priority: issue_priority)
end
let(:project) do
unless @project
@project = build(:project)
@project.members = [build(:member, principal: user,
project: @project,
roles: [role])]
end
@project
end
before do
ActionController::Base.perform_caching = false
allow(Setting).to receive(:plugin_openproject_backlogs).and_return({ "points_burn_direction" => "down",
"wiki_template" => "",
"story_types" => [type_feature.id.to_s],
"task_type" => task_type.id.to_s })
project.types << task_type
end
describe "Class methods" do
describe "#backlogs" do
describe "WITH one sprint " \
"WITH the sprint having 1 story" do
before do
story1
end
it { expect(Story.backlogs(project, [version.id])[version.id]).to contain_exactly(story1) }
end
describe "WITH two sprints " \
"WITH two stories " \
"WITH one story per sprint " \
"WITH querying for the two sprints" do
before do
version2
story1
story2.version_id = version2.id
story2.save!
end
it { expect(Story.backlogs(project, [version.id, version2.id])[version.id]).to contain_exactly(story1) }
it { expect(Story.backlogs(project, [version.id, version2.id])[version2.id]).to contain_exactly(story2) }
end
describe "WITH two sprints " \
"WITH two stories " \
"WITH one story per sprint " \
"WITH querying one sprints" do
before do
version2
story1
story2.version_id = version2.id
story2.save!
end
it { expect(Story.backlogs(project, [version.id])[version.id]).to contain_exactly(story1) }
it { expect(Story.backlogs(project, [version.id])[version2.id]).to be_empty }
end
describe "WITH two sprints " \
"WITH two stories " \
"WITH one story per sprint " \
"WITH querying for the two sprints " \
"WITH one sprint being in another project" do
before do
story1
other_project = create(:project)
version2.update! project_id: other_project.id
story2.version_id = version2.id
story2.project = other_project
# reset memoized versions to reflect changes above
story2.instance_variable_set(:@assignable_versions, nil)
story2.save!
end
it { expect(Story.backlogs(project, [version.id, version2.id])[version.id]).to contain_exactly(story1) }
it { expect(Story.backlogs(project, [version.id, version2.id])[version2.id]).to be_empty }
end
describe "WITH one sprint " \
"WITH the sprint having one story in this project and one story in another project" do
before do
version.sharing = "system"
version.save!
another_project = create(:project)
story1
story2.project = another_project
story2.save!
end
it { expect(Story.backlogs(project, [version.id])[version.id]).to contain_exactly(story1) }
end
describe "WITH one sprint " \
"WITH the sprint having two storys " \
"WITH one being the child of the other" do
before do
story1.parent_id = story2.id
story1.save
end
it { expect(Story.backlogs(project, [version.id])[version.id]).to contain_exactly(story1, story2) }
end
describe "WITH one sprint " \
"WITH the sprint having one story " \
"WITH the story having a child task" do
before do
task.parent_id = story1.id
task.save
end
it { expect(Story.backlogs(project, [version.id])[version.id]).to contain_exactly(story1) }
end
describe "WITH one sprint " \
"WITH the sprint having one story and one task " \
"WITH the two having no connection" do
before do
task
story1
end
it { expect(Story.backlogs(project, [version.id])[version.id]).to contain_exactly(story1) }
end
end
end
end
@@ -39,12 +39,6 @@ RSpec.describe Task do
type: task_type)
end
before do
allow(Setting)
.to receive(:plugin_openproject_backlogs)
.and_return({ "task_type" => task_type.id.to_s })
end
describe "having custom journables", with_settings: { journal_aggregation_time_minutes: 0 } do
let(:user) { create(:user) }
let(:role) do
@@ -1,252 +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 Version do
it { is_expected.to have_many :version_settings }
describe "#used_as_backlog?" do
let(:project) { create(:project) }
let(:version) { create(:version, project:) }
context "when backlogs is not enabled" do
before do
project.enabled_module_names = project.enabled_module_names - ["backlogs"]
end
it "returns false" do
expect(version.used_as_backlog?(project)).to be false
end
end
context "when backlogs is enabled" do
before do
project.enabled_module_names = project.enabled_module_names + ["backlogs"]
end
context "when no version_settings exist" do
it "returns false" do
expect(version.used_as_backlog?(project)).to be false
end
end
context "when version_settings exist with display_right" do
before do
create(:version_setting, version:, project:, display: VersionSetting::DISPLAY_RIGHT)
end
it "returns true" do
expect(version.used_as_backlog?(project)).to be true
end
end
context "when version_settings exist with display_left" do
before do
create(:version_setting, version:, project:, display: VersionSetting::DISPLAY_LEFT)
end
it "returns false" do
expect(version.used_as_backlog?(project)).to be false
end
end
context "when version_settings exist with display_none" do
before do
create(:version_setting, version:, project:, display: VersionSetting::DISPLAY_NONE)
end
it "returns false" do
expect(version.used_as_backlog?(project)).to be false
end
end
context "when multiple version_settings exist for different projects" do
let(:other_project) { create(:project) }
before do
project.enabled_module_names = project.enabled_module_names + ["backlogs"]
other_project.enabled_module_names = other_project.enabled_module_names + ["backlogs"]
create(:version_setting, version:, project:, display: VersionSetting::DISPLAY_RIGHT)
create(:version_setting, version:, project: other_project, display: VersionSetting::DISPLAY_LEFT)
end
it "returns true for the project with display_right" do
expect(version.used_as_backlog?(project)).to be true
end
it "returns false for the project with display_left" do
expect(version.used_as_backlog?(other_project)).to be false
end
end
context "when project parameter is not provided" do
context "and version has a project" do
before do
project.enabled_module_names = project.enabled_module_names + ["backlogs"]
create(:version_setting, version:, project:, display: VersionSetting::DISPLAY_RIGHT)
end
it "uses the version's project" do
expect(version.used_as_backlog?).to be true
end
end
end
end
end
describe "rebuild positions" do
def build_work_package(options = {})
build(:work_package, options.reverse_merge(version_id: version.id,
priority_id: priority.id,
project_id: project.id,
status_id: status.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, name: "Project 1", types: [epic_type, story_type, task_type, other_type]) }
let(:epic_type) { create(:type, name: "Epic") }
let(:story_type) { create(:type, name: "Story") }
let(:task_type) { create(:type, name: "Task") }
let(:other_type) { create(:type, name: "Other") }
let(:version) { create(:version, project_id: project.id, name: "Version") }
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
# We had problems while writing these specs, that some elements kept
# creeping 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" => [epic_type.id, story_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 = [epic_type, story_type, task_type, other_type]
version
end
it "moves an work_package to a project where backlogs is disabled while using versions" do
project2 = create(:project, name: "Project 2", types: [epic_type, story_type, task_type, other_type])
project2.enabled_module_names = project2.enabled_module_names - ["backlogs"]
project2.save!
project2.reload
work_package1 = create(:work_package, type_id: task_type.id, status_id: status.id, project_id: project.id)
work_package2 = create(:work_package, parent_id: work_package1.id, type_id: task_type.id, status_id: status.id,
project_id: project.id)
work_package3 = create(:work_package, parent_id: work_package2.id, type_id: task_type.id, status_id: status.id,
project_id: project.id)
work_package1.reload
work_package1.version_id = version.id
work_package1.save!
work_package1.reload
work_package2.reload
work_package3.reload
move_to_project(work_package3, project2)
work_package1.reload
work_package2.reload
work_package3.reload
move_to_project(work_package2, project2)
work_package1.reload
work_package2.reload
work_package3.reload
expect(work_package3.project).to eq(project2)
expect(work_package2.project).to eq(project2)
expect(work_package1.project).to eq(project)
expect(work_package3.version_id).to be_nil
expect(work_package2.version_id).to be_nil
expect(work_package1.version_id).to eq(version.id)
end
it "rebuilds positions" do
e1 = create_work_package(type_id: epic_type.id)
s2 = create_work_package(type_id: story_type.id)
s3 = create_work_package(type_id: story_type.id)
s4 = create_work_package(type_id: story_type.id)
s5 = create_work_package(type_id: story_type.id)
t3 = create_work_package(type_id: task_type.id)
o9 = create_work_package(type_id: other_type.id)
[e1, s2, s3, s4, s5].each(&:move_to_bottom)
# Messing around with positions
s3.update_column(:position, nil)
s4.update_column(:position, nil)
t3.update_column(:position, 3)
o9.update_column(:position, 9)
version.rebuild_story_positions(project)
work_packages = version
.work_packages
.where(project_id: project)
.order(Arel.sql("COALESCE(position, 0) ASC, id ASC"))
expect(work_packages.map(&:position)).to eq([nil, nil, 1, 2, 3, 4, 5])
expect(work_packages.map(&:subject)).to eq([t3, o9, e1, s2, s5, s3, s4].map(&:subject))
# Makes sure, that all work_package subjects are uniq, so that the above
# assertion works as expected
expect(work_packages.map(&:subject).uniq.size).to eq(7)
end
end
end

Some files were not shown because too many files have changed in this diff Show More