mirror of
https://github.com/opf/openproject.git
synced 2026-06-14 03:30:14 +00:00
Merge branch 'release/17.3' into dev
This commit is contained in:
@@ -37,7 +37,6 @@ class News::BaseContract < ModelContract
|
||||
News
|
||||
end
|
||||
|
||||
attribute :project
|
||||
attribute :title
|
||||
attribute :summary
|
||||
attribute :description
|
||||
|
||||
@@ -29,4 +29,5 @@
|
||||
#++
|
||||
|
||||
class News::CreateContract < News::BaseContract
|
||||
attribute :project
|
||||
end
|
||||
|
||||
@@ -72,6 +72,8 @@ module WorkPackages
|
||||
|
||||
validate :user_allowed_to_edit
|
||||
|
||||
validate :user_allowed_to_move_from_source_project
|
||||
|
||||
validate :can_move_to_milestone
|
||||
|
||||
validate :user_allowed_to_change_parent
|
||||
@@ -90,6 +92,21 @@ module WorkPackages
|
||||
end
|
||||
end
|
||||
|
||||
# When moving a work package to a different project, require
|
||||
# :move_work_packages in the source project (the project the work
|
||||
# package currently belongs to). The per-attribute permission check
|
||||
# (reduce_by_writable_permissions) evaluates :move_work_packages
|
||||
# against the target project; this validation covers the source side.
|
||||
def user_allowed_to_move_from_source_project
|
||||
return unless model.project_id_changed?
|
||||
|
||||
with_unchanged_project_id do
|
||||
unless user.allowed_in_project?(:move_work_packages, model.project)
|
||||
errors.add :project_id, :error_readonly
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def user_allowed_to_access
|
||||
unless ::WorkPackage.visible(@user).exists?(model.id)
|
||||
errors.add :base, :error_not_found
|
||||
|
||||
@@ -285,7 +285,8 @@ module ApplicationHelper
|
||||
|
||||
def body_data_attributes(local_assigns)
|
||||
{
|
||||
controller: "application auto-theme-switcher hover-card-trigger beforeunload external-links highlight-target-element",
|
||||
controller: ["application auto-theme-switcher hover-card-trigger beforeunload external-links highlight-target-element",
|
||||
stimulus_body_controller].compact.join(" "),
|
||||
relative_url_root: root_path,
|
||||
overflowing_identifier: ".__overflowing_body",
|
||||
external_links_enabled_value: Setting.capture_external_links?,
|
||||
|
||||
@@ -39,5 +39,13 @@ module StimulusHelper
|
||||
@stimulus_content_data || {}
|
||||
end
|
||||
|
||||
def body_controller(name)
|
||||
@stimulus_body_controller = name
|
||||
end
|
||||
|
||||
def stimulus_body_controller
|
||||
@stimulus_body_controller
|
||||
end
|
||||
|
||||
# rubocop:enable Rails/HelperInstanceVariable
|
||||
end
|
||||
|
||||
@@ -59,7 +59,7 @@ In OpenProject, artificial intelligence (AI) refers to the integration of AI sys
|
||||
|
||||
OpenProject approaches AI with a clear focus on control, transparency, and data protection. AI is designed to support users in their work, not to replace human decision-making. Users remain in full control at all times and can decide whether and how AI features are used.
|
||||
|
||||
With features such as the [MCP Server](https://www.openproject.org/blog/openproject-17-2-0-release/), OpenProject enables secure connections between project data and external AI systems. This allows organizations to benefit from AI while maintaining full control over how their data is accessed and used.
|
||||
With features such as the [MCP Server](../system-admin-guide/integrations/mcp-server/), OpenProject enables secure connections between project data and external AI systems. This allows organizations to benefit from AI while maintaining full control over how their data is accessed and used.
|
||||
|
||||
### Attribute help texts
|
||||
|
||||
|
||||
@@ -7,10 +7,12 @@ keywords: configure backlogs, backlogs settings, story type, task type, burn cha
|
||||
---
|
||||
# Backlogs configuration
|
||||
|
||||
Backlogs settings let you tailor OpenProject’s Scrum features to match how your team plans and tracks work. By configuring story and task types, burn charts, and sprint wiki templates, you can ensure your backlogs and boards show the right work items and support consistent sprint planning and documentation.
|
||||
Backlogs settings let you tailor OpenProject’s Scrum features to match how your team plans and tracks work. Backlogs support teams in structuring, prioritizing, and refining work for upcoming sprints, helping ensure a clear and consistent approach to sprint planning and execution.
|
||||
|
||||
## Backlog administration settings are evolving
|
||||
|
||||
We are currently redesigning the Backlogs module. Administration settings for sprints and backlogs will be visible here in the near future. Project-level settings remain available.
|
||||
|
||||
|
||||
> [!NOTE]
|
||||
>
|
||||
> We are currently redesigning the Backlogs module. Administration settings for sprints and backlogs will be visible here in the near future.
|
||||
>
|
||||
> Project-level settings remain available.
|
||||
|
||||
@@ -13,12 +13,12 @@ keywords: backlogs, scrum, backlog, agile, sprint, sprint bucket
|
||||
|
||||
Working in agile project teams is becoming increasingly important, and with OpenProject, it is easier than ever.
|
||||
|
||||
OpenProject supports your work with the Agile and Scrum methodology by providing a variety of improved functionalities. You can now create and manage sprints, record and prioritize user stories in sprints and the backlog, use sprint boards or burndown-charts, print story cards, and much more. For more information, please refer to the OpenProject [agile and scrum features](https://www.openproject.org/collaboration-software-features/agile-project-management/) page.
|
||||
OpenProject supports your work with the Agile and Scrum methodology by providing a variety of improved functionalities. You can now create and manage sprints, record and prioritize work packages in sprints and the backlog, use automated sprint boards or burndown-charts, and much more. For more information, please refer to the OpenProject [agile and scrum features](https://www.openproject.org/collaboration-software-features/agile-project-management/) page.
|
||||
|
||||
<div class="glossary">
|
||||
|
||||
|
||||
A **Backlog** is defined as a plugin that allows you to use the backlogs feature in OpenProject. In order to use backlogs in a project, the Backlogs module has to be activated in the project settings.
|
||||
A **Backlog** is defined as a module that allows you to use the backlogs feature in OpenProject. In order to use backlogs in a project, the Backlogs module has to be activated in the project settings.
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -23,20 +23,43 @@ Press the **Save** button to apply your changes.
|
||||
|
||||
## Sharing sprints
|
||||
|
||||
Sharing is a project-level setting that allows you to choose whether sprints should be shared across projects or not.
|
||||
Sharing is a **project-level setting** that allows you to choose whether sprints should be shared across projects or not.
|
||||
|
||||
> [!NOTE]
|
||||
> This is not a sprint-level setting as is currently the case with versions.
|
||||
|
||||
Sharing sprints allows teams working across multiple projects to plan and track work in a coordinated way. Instead of managing separate, disconnected sprints in each project, you can define a sprint once and reuse it across projects. This is especially useful for cross-team Scrum setups, scaled agile environments, or when multiple teams contribute to the same increment.
|
||||
|
||||
Depending on the selected option, a project can either provide sprints to others, use shared sprints, or remain independent:
|
||||
|
||||
**Don't share:** This is the default setting for projects. Sprints can be created in this project and are available and visible only within this project. None of the created sprints are shared with any other project or sub-projects.
|
||||
|
||||
**Share sprints:** Sprints can be created in this project and shared with either **all projects** or **subprojects**:
|
||||
|
||||
**All projects:** Selecting this option means the sprints created are available to all projects within the instance. It also means that other projects will not be able to use this option.
|
||||
- **All projects:** Selecting this option means the sprints created are available to all projects within the instance. It also means that no other project can share sprints with all projects.
|
||||
|
||||
**Subprojects:** Sprints created in this project will be available to all subprojects of the current project.
|
||||
- **Subprojects:** Sprints created in this project will be available to all subprojects of the current project.
|
||||
|
||||
**Receive shared sprints:** No sprints can be created within this project. Instead, only sprints shared by other projects can be used.
|
||||
**Receive shared sprints:** No sprints can be created within this project. Instead, only sprints shared by another project can be used.
|
||||
|
||||

|
||||
|
||||
### What is shared
|
||||
|
||||
When sprints are shared, the sprint itself is shared across projects. This includes:
|
||||
|
||||
- Sprint name
|
||||
- Start and finish dates
|
||||
- Sprint status (e.g. planning, active, completed)
|
||||
|
||||
This ensures that all participating projects work with the same sprint definition and timeline.
|
||||
|
||||
### What is not shared
|
||||
|
||||
- Work packages remain in their respective projects
|
||||
- Backlogs and their structure remain project-specific
|
||||
- Permissions and visibility are still managed per project
|
||||
|
||||
Even when using shared sprints, each project keeps its own work items and configuration.
|
||||
|
||||
Read more on [how to work with Backlogs in OpenProject](../../../backlogs-scrum/).
|
||||
|
||||
@@ -31,16 +31,10 @@
|
||||
import { ApplicationController } from 'stimulus-use';
|
||||
|
||||
export default class KeepScrollPositionController extends ApplicationController {
|
||||
static values = {
|
||||
url: String,
|
||||
};
|
||||
|
||||
static targets = ['triggerButton'];
|
||||
|
||||
declare triggerButtonTarget:HTMLLinkElement;
|
||||
|
||||
declare urlValue:string;
|
||||
|
||||
connect() {
|
||||
super.connect();
|
||||
|
||||
@@ -80,6 +74,6 @@ export default class KeepScrollPositionController extends ApplicationController
|
||||
}
|
||||
|
||||
private scrollPositionKey():string {
|
||||
return `${this.urlValue}/scrollPosition`;
|
||||
return `${window.location.pathname}/scrollPosition`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,6 +345,7 @@ module OpenProject
|
||||
# Target path with optional peg revision
|
||||
# http://svnbook.red-bean.com/en/1.7/svn.advanced.pegrevs.html
|
||||
def target(path = "", peg: nil)
|
||||
validate_path!(path)
|
||||
path = super(path)
|
||||
|
||||
if peg
|
||||
@@ -354,6 +355,27 @@ module OpenProject
|
||||
end
|
||||
end
|
||||
|
||||
def validate_path!(path)
|
||||
decoded = path.to_s
|
||||
|
||||
# Unescape repeatedly to catch nested encoding such as %252e%252e.
|
||||
loop do
|
||||
unescaped = URI::DEFAULT_PARSER.unescape(decoded)
|
||||
break if unescaped == decoded
|
||||
|
||||
decoded = unescaped
|
||||
end
|
||||
|
||||
invalid_characters = [path.to_s, decoded].any? do |value|
|
||||
value.include?("\0") || value.include?("\n") || value.include?("\r")
|
||||
end
|
||||
|
||||
traverses_parent = decoded.split("/").include?("..")
|
||||
return unless invalid_characters || traverses_parent
|
||||
|
||||
raise Exceptions::CommandFailed.new(client_command, "Invalid repository path")
|
||||
end
|
||||
|
||||
##
|
||||
# Builds the full git arguments from the parameters
|
||||
# and calls the given block with in, out, err, thread
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<%=
|
||||
helpers.content_controller "poll-for-changes keep-scroll-position",
|
||||
helpers.content_controller "poll-for-changes",
|
||||
poll_for_changes_url_value: check_for_updates_project_meeting_path(@meeting.project, @meeting),
|
||||
poll_for_changes_interval_value: check_for_updates_interval,
|
||||
keep_scroll_position_url_value: meeting_path(@meeting)
|
||||
poll_for_changes_interval_value: check_for_updates_interval
|
||||
helpers.body_controller "keep-scroll-position"
|
||||
|
||||
component_wrapper do
|
||||
render(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<% helpers.content_controller "keep-scroll-position",
|
||||
keep_scroll_position_url_value: polymorphic_path([@project, :meetings]) %>
|
||||
<% helpers.content_controller "keep-scroll-position" %>
|
||||
|
||||
<%= render(Primer::OpenProject::PageHeader.new) do |header|
|
||||
header.with_title { page_title }
|
||||
|
||||
+1
-2
@@ -1,6 +1,5 @@
|
||||
<%=
|
||||
helpers.content_controller "keep-scroll-position",
|
||||
keep_scroll_position_url_value: polymorphic_path([@project, @meeting])
|
||||
helpers.content_controller "keep-scroll-position"
|
||||
|
||||
component_wrapper do
|
||||
render(Primer::OpenProject::PageHeader.new) do |header|
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
# 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/meetings/show"
|
||||
require_relative "../support/pages/meetings/index"
|
||||
require_relative "../support/pages/recurring_meeting/show"
|
||||
|
||||
RSpec.describe "Meeting scroll position",
|
||||
:js do
|
||||
shared_let(:project) { create(:project, enabled_module_names: %w[meetings]) }
|
||||
shared_let(:user) { create(:admin) }
|
||||
|
||||
current_user { user }
|
||||
|
||||
describe "is restored after clicking 'Reload' in the flash banner" do
|
||||
let(:meeting) { create(:meeting, project:, author: user) }
|
||||
let(:show_page) { Pages::Meetings::Show.new(meeting) }
|
||||
|
||||
before do
|
||||
25.times { create(:meeting_agenda_item, meeting:, author: user) }
|
||||
|
||||
# Disable automatic polling so we can trigger it manually
|
||||
allow_any_instance_of(Meetings::HeaderComponent) # rubocop:disable RSpec/AnyInstance
|
||||
.to receive(:check_for_updates_interval)
|
||||
.and_return(0)
|
||||
end
|
||||
|
||||
it do
|
||||
flash_component = ".op-primer-flash--item"
|
||||
|
||||
show_page.visit!
|
||||
first_window = current_window
|
||||
second_window = open_new_window
|
||||
|
||||
scroll_position = within_window(first_window) do
|
||||
expect(page).to have_test_selector("meeting-page-header")
|
||||
last_item = all("[data-test-selector='op-meeting-agenda-title']").last
|
||||
page.execute_script("arguments[0].scrollIntoView({ block: 'center', behavior: 'instant' });", last_item)
|
||||
page.evaluate_script("document.getElementById('content-body').scrollTop")
|
||||
end
|
||||
|
||||
within_window(second_window) do
|
||||
show_page.visit!
|
||||
|
||||
retry_block do
|
||||
show_page.add_agenda_item do
|
||||
fill_in "Title", with: "New item triggering update"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
within_window(first_window) do
|
||||
show_page.trigger_change_poll
|
||||
expect(page).to have_css(flash_component)
|
||||
expect(page).to have_text(I18n.t(:notice_meeting_updated))
|
||||
|
||||
click_on I18n.t("label_meeting_reload")
|
||||
|
||||
expect(page).to have_test_selector("meeting-page-header")
|
||||
|
||||
retry_block do
|
||||
restored_position = page.evaluate_script("document.getElementById('content-body').scrollTop")
|
||||
expect(restored_position).to be_within(25).of(scroll_position)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "is restored after clicking 'Show more' in the meetings index page footer" do
|
||||
let(:index_page) { Pages::Meetings::Index.new(project:) }
|
||||
|
||||
# Freeze time at 8 AM so "today" meetings scheduled for 10 AM are always in the future
|
||||
around do |example|
|
||||
freeze_time_at = Time.zone.today.beginning_of_day + 8.hours
|
||||
travel_to(freeze_time_at) { example.run }
|
||||
end
|
||||
|
||||
before do
|
||||
30.times { create(:meeting, :author_participates, project:, author: user, start_time: Time.zone.today + 10.hours) }
|
||||
8.times { create(:meeting, :author_participates, project:, author: user, start_time: 2.weeks.from_now) }
|
||||
end
|
||||
|
||||
it do
|
||||
index_page.visit!
|
||||
expect(page).to have_text(I18n.t(:label_recurring_meeting_show_more))
|
||||
|
||||
trigger_button = find("[data-keep-scroll-position-target='triggerButton']")
|
||||
page.execute_script("arguments[0].scrollIntoView({ block: 'center', behavior: 'instant' });", trigger_button)
|
||||
scroll_position = page.evaluate_script("document.getElementById('content-body').scrollTop")
|
||||
|
||||
trigger_button.click
|
||||
|
||||
expect(page).to have_no_text(I18n.t(:label_recurring_meeting_show_more))
|
||||
|
||||
retry_block do
|
||||
restored_position = page.evaluate_script("document.getElementById('content-body').scrollTop")
|
||||
expect(restored_position).to be_within(25).of(scroll_position)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "is restored after clicking 'Show more' in the recurring meeting show page footer" do
|
||||
let(:recurring_meeting) do
|
||||
create(:recurring_meeting,
|
||||
project:,
|
||||
author: user,
|
||||
start_time: Date.tomorrow + 10.hours,
|
||||
frequency: "daily",
|
||||
end_after: "iterations",
|
||||
iterations: 31)
|
||||
end
|
||||
let(:show_page) { Pages::RecurringMeeting::Show.new(recurring_meeting) }
|
||||
|
||||
before do
|
||||
25.times do |i|
|
||||
meeting = create(:meeting,
|
||||
project:,
|
||||
author: user,
|
||||
recurring_meeting:,
|
||||
start_time: (i + 4).weeks.from_now + 10.hours)
|
||||
create(:scheduled_meeting,
|
||||
meeting:,
|
||||
recurring_meeting:,
|
||||
start_time: meeting.start_time)
|
||||
end
|
||||
end
|
||||
|
||||
it do
|
||||
show_page.visit!
|
||||
expect(page).to have_text(I18n.t(:label_recurring_meeting_show_more))
|
||||
|
||||
trigger_button = find("[data-keep-scroll-position-target='triggerButton']")
|
||||
page.execute_script("arguments[0].scrollIntoView({ block: 'center', behavior: 'instant' });", trigger_button)
|
||||
scroll_position = page.evaluate_script("document.getElementById('content-body').scrollTop")
|
||||
|
||||
trigger_button.click
|
||||
|
||||
expect(page).to have_no_text(I18n.t(:label_recurring_meeting_show_more))
|
||||
|
||||
retry_block do
|
||||
restored_position = page.evaluate_script("document.getElementById('content-body').scrollTop")
|
||||
expect(restored_position).to be_within(25).of(scroll_position)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -98,24 +98,60 @@ RSpec.describe WorkPackages::UpdateContract do
|
||||
|
||||
describe "project_id" do
|
||||
let(:target_project) { persisted_other_project }
|
||||
let(:source_permissions) { %i[view_work_packages edit_work_packages move_work_packages] }
|
||||
let(:target_permissions) { [:move_work_packages] }
|
||||
|
||||
before do
|
||||
mock_permissions_for(user) do |mock|
|
||||
mock.allow_in_project *permissions, project: persisted_project
|
||||
mock.allow_in_project *source_permissions, project: persisted_project
|
||||
mock.allow_in_project *target_permissions, project: target_project
|
||||
end
|
||||
|
||||
work_package.project = target_project
|
||||
end
|
||||
|
||||
it_behaves_like "contract is valid"
|
||||
context "with move_work_packages in both source and target" do
|
||||
it_behaves_like "contract is valid"
|
||||
end
|
||||
|
||||
context "if the user lacks the permissions" do
|
||||
context "if the user lacks move_work_packages in the target project" do
|
||||
let(:target_permissions) { [] }
|
||||
|
||||
it_behaves_like "contract is invalid", project_id: :error_readonly
|
||||
end
|
||||
|
||||
context "if the user lacks move_work_packages in the source project" do
|
||||
let(:source_permissions) { %i[view_work_packages edit_work_packages] }
|
||||
|
||||
it_behaves_like "contract is invalid", project_id: :error_readonly
|
||||
end
|
||||
|
||||
context "when modifying attributes while moving (authorization bypass prevention)" do
|
||||
before do
|
||||
work_package.subject = "modified-subject"
|
||||
end
|
||||
|
||||
context "with edit_work_packages in target project" do
|
||||
let(:target_permissions) { %i[move_work_packages edit_work_packages] }
|
||||
|
||||
it_behaves_like "contract is valid"
|
||||
end
|
||||
|
||||
context "without edit_work_packages in target project" do
|
||||
let(:target_permissions) { [:move_work_packages] }
|
||||
|
||||
it_behaves_like "contract is invalid", subject: :error_readonly
|
||||
end
|
||||
|
||||
context "without move_work_packages in source project" do
|
||||
let(:source_permissions) { %i[view_work_packages change_work_package_status] }
|
||||
let(:target_permissions) { %i[move_work_packages edit_work_packages] }
|
||||
|
||||
it "blocks the move even when the target project grants all permissions" do
|
||||
expect(validated_contract.errors.symbols_for(:project_id)).to include(:error_readonly)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "remaining_hours" do
|
||||
|
||||
@@ -128,6 +128,32 @@ RSpec.describe OpenProject::SCM::Adapters::Subversion do
|
||||
end
|
||||
end
|
||||
|
||||
describe "path validation" do
|
||||
shared_examples "rejects encoded parent traversal" do |method_name, *args|
|
||||
it "rejects encoded parent traversal in ##{method_name}" do
|
||||
allow(adapter).to receive(:capture_svn)
|
||||
allow(adapter).to receive(:popen3)
|
||||
|
||||
expect { adapter.public_send(method_name, "%252e%252e/other_repo/credentials.txt", *args) }
|
||||
.to raise_error(OpenProject::SCM::Exceptions::CommandFailed) { |error|
|
||||
expect(error.message).to eq("Invalid repository path")
|
||||
}
|
||||
|
||||
expect(adapter).not_to have_received(:capture_svn)
|
||||
expect(adapter).not_to have_received(:popen3)
|
||||
end
|
||||
end
|
||||
|
||||
include_examples "rejects encoded parent traversal", :entries
|
||||
include_examples "rejects encoded parent traversal", :cat
|
||||
include_examples "rejects encoded parent traversal", :diff, 2, 1
|
||||
include_examples "rejects encoded parent traversal", :annotate
|
||||
|
||||
it "allows dots that are not parent traversal" do
|
||||
expect(adapter.send(:target, "subversion_test/file..txt")).to eq("#{url}/subversion_test/file..txt")
|
||||
end
|
||||
end
|
||||
|
||||
describe "empty repository" do
|
||||
include_context "with tmpdir"
|
||||
let(:root_url) { tmpdir }
|
||||
@@ -178,7 +204,7 @@ RSpec.describe OpenProject::SCM::Adapters::Subversion do
|
||||
|
||||
out, process = Open3.capture2e("svn", "info", url)
|
||||
expect(process.exitstatus).to eq(0)
|
||||
expect(out).to include("Repository UUID")
|
||||
expect(out).to match(/UUID.*[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)
|
||||
end
|
||||
|
||||
it "is available" do
|
||||
@@ -389,7 +415,9 @@ RSpec.describe OpenProject::SCM::Adapters::Subversion do
|
||||
|
||||
it "provides the selected diff for the given range" do
|
||||
diff = adapter.diff("subversion_test/helloworld.c", 8, 6).map(&:chomp)
|
||||
expect(diff).to eq(<<~DIFF.split("\n"))
|
||||
normalized_diff = diff.map { |line| line.gsub(/\((?i:revision) (\d+)\)/, "(revision \\1)") }
|
||||
|
||||
expect(normalized_diff).to eq(<<~DIFF.split("\n"))
|
||||
Index: helloworld.c
|
||||
===================================================================
|
||||
--- helloworld.c (revision 6)
|
||||
|
||||
@@ -66,6 +66,33 @@ RSpec.describe API::V3::News::NewsAPI,
|
||||
it_behaves_like "updates the news"
|
||||
end
|
||||
|
||||
describe "user with only view_news on source project and manage_news on different project" do
|
||||
let(:attacker_project) { create(:project, enabled_module_names: %w[news]) }
|
||||
let(:user) do
|
||||
create(:user,
|
||||
member_with_permissions: {
|
||||
project => %i[view_news],
|
||||
attacker_project => %i[view_news manage_news]
|
||||
})
|
||||
end
|
||||
let(:parameters) do
|
||||
{
|
||||
_links: {
|
||||
project: {
|
||||
href: api_v3_paths.project(attacker_project.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
it "does not move the news to another project" do
|
||||
expect(last_response.status).to eq(422)
|
||||
expect(news.reload.project).to eq(project)
|
||||
end
|
||||
|
||||
it_behaves_like "read-only violation", "project", News
|
||||
end
|
||||
|
||||
describe "unauthorized user" do
|
||||
let(:user) { build(:user) }
|
||||
|
||||
|
||||
@@ -416,6 +416,7 @@ RSpec.describe "API v3 Work package resource",
|
||||
let(:target_project) do
|
||||
create(:project, public: false)
|
||||
end
|
||||
let(:permissions) { super() + [:move_work_packages] }
|
||||
let(:project_link) { api_v3_paths.project target_project.id }
|
||||
let(:project_parameter) { { _links: { project: { href: project_link } } } }
|
||||
let(:params) { valid_params.merge(project_parameter) }
|
||||
|
||||
@@ -10,11 +10,15 @@ module FormFields
|
||||
# Capybara's native .click on a checkbox can update the DOM property directly
|
||||
# without dispatching a browser click event, so Stimulus event handlers won't fire.
|
||||
# Using execute_script with element.click() fires a real browser event.
|
||||
# We first wait for the element via Capybara's find (which retries until it appears)
|
||||
# to avoid null reference errors when the DOM is still being updated by Turbo.
|
||||
def check
|
||||
page.find(selector)
|
||||
page.execute_script("document.querySelector(\"#{selector}\").click()")
|
||||
end
|
||||
|
||||
def uncheck
|
||||
page.find(selector)
|
||||
page.execute_script("document.querySelector(\"#{selector}\").click()")
|
||||
end
|
||||
|
||||
|
||||
@@ -104,9 +104,8 @@ module Pages
|
||||
def open_inplace_edit_field_for_custom_field(custom_field)
|
||||
scroll_to_element(page.find("[data-test-selector='project-custom-field-#{custom_field.id}']"))
|
||||
field = Components::Common::InplaceEditField.new(project, custom_field.attribute_name.to_sym)
|
||||
field.open_field
|
||||
|
||||
wait_for_network_idle
|
||||
wait_for_turbo_stream { field.open_field }
|
||||
|
||||
field
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user