8597 Commits

Author SHA1 Message Date
Kabiru Mwenja 6053f642c2 Apply S4 review feedback: drop wps_by_id from fold_in_alias_keys
`lookup` is already keyed by `wp.id.to_s`, so the second index built
inside `fold_in_alias_keys` was redundant. The alias-lookup branch can
read straight from `lookup` using the plucked work_package_id.
2026-05-15 08:24:18 +03:00
Kabiru Mwenja 480fa51a1a Cap WP identifier preload IN-list at 500
The doc-level preload that backs the macro pipeline reads identifiers
straight out of user-pasted CKEditor content. Without a bound, a single
multi-megabyte comment could push thousands of values into one
`WHERE id IN (...)` and an alias `WHERE identifier IN (...)`, both built
in memory and shipped in one query.

`MAX_PRELOAD_IDENTIFIERS = 500` caps the per-render Set; references past
the cap render via the link handler's cache-miss fallback (numeric → bare
`#N` link, semantic → literal text).

Spec stubs the constant to 2 and asserts the WP IN-list never exceeds it.
2026-05-15 08:24:17 +03:00
Kabiru Mwenja bc0b59f721 Specialise WP link handler with hash_trigger? predicate
Replace duplicated `%w(# ## ###).include?(matcher.sep)` and
`["##", "###"].include?(matcher.sep)` checks with named predicates on
the WorkPackages link handler: HASH_TRIGGERS for the `#`/`##`/`###`
family, hash_trigger? to gate the handler, and quickinfo?/detailed?
to distinguish the three macro shapes. The PDF-export subclass picks
both up via inheritance.

"Trigger" replaces "separator" in the WP context: in `#1234` the `#`
is a sigil that triggers mention recognition, not something that
separates parts of the reference. The matcher's generic `sep`
vocabulary stays — it still fits the `version#3` and `version:1.0.0`
cases that other handlers own.
2026-05-15 08:24:17 +03:00
Kabiru Mwenja f9406ff1c7 Trim verbose contextual comments on WP identifier branch
Cuts comments that restate code mechanics, narrate the journey, or
pile on implementation detail. Genuine WHYs (perf rationale of the
to_i.to_s round-trip, the alias second-query reason, leading-zero
rejection, classic-mode preload skip) all stayed.
2026-05-15 08:24:16 +03:00
Kabiru Mwenja a3e4d6bf1e Reject semantic-shaped ids in HashSeparator#applicable?
The matcher regex now accepts both numeric and semantic identifier shapes
(`\d+|PROJ-\d+`) on a single alternation branch shared by the bare WP
handler and the prefixed-resource handler. Without this guard,
`version#PROJ-1` reaches HashSeparator with `oid = "PROJ-1".to_i = 0`,
which is `present?`, so it issues `Version.find_by(id: 0)` — a guaranteed
miss against the wrong table. The same shape applies to every prefix
HashSeparator handles (document, message, meeting, project, user, group,
view), turning a single pasted comment into one wasted SELECT per prefix.

Gating `applicable?` on `numeric_id?(matcher.identifier)` short-circuits
the handler before any DB lookup; the regex still matches and the text
falls through to literal output. Regression test asserts zero
`FROM "versions"` SELECTs for `version#PROJ-1`.
2026-05-15 08:24:16 +03:00
Kabiru Mwenja 5977abf594 Encapsulate canonical-numeric guard as numeric_id?
The `value == value.to_i.to_s` round-trip check that filters leading-
zero ID forms ("0123") was duplicated across the WP link handler, the
PDF export macro, and the cost-query filter, and the matching guard in
`ResourceLinksMatcher#extract_work_package_identifier` was a no-op
(the regex's `\d+|[A-Z][A-Z0-9_]*-\d+` branch already gates the shape).

A new `WorkPackage::SemanticIdentifier.numeric_id?(value)` predicate
captures the canonical-numeric check at one site. It pairs with
`semantic_id?` as the WP-finder shape gate; the two answer different
questions (shape vs routing) and so are kept independent rather than
expressed as one another's negation.

The redundant `extract_work_package_identifier` guard is dropped along
with its misleading "rejecting leading-zero forms" comment — rejection
actually happens downstream in the link handler, where the new
predicate now reads as the contract it always was.
2026-05-15 08:24:16 +03:00
Kabiru Mwenja 1c15bd7777 Replace history/jargon framing in code comments
Comments shouldn't read as commit-history narration or as reviewer
shorthand. Each one now describes the present invariant in plain words:

- Drop "Pre-PR-E behaviour" / "matching pre-PR behaviour" / "matches the
  pre-PR" — the spec and inline comments now state the current rule.
- Drop "load-bearing", "already in this codebase", "we fall back" — say
  what the code does, not how a reviewer would describe it.
- Reword the `semantic_id?` rationale to a forward-looking constraint
  (`don't tighten it`) instead of a comparative description of why this
  module owns it.
2026-05-15 08:24:15 +03:00
Kabiru Mwenja df326642c8 Use named captures in ResourceLinksMatcher.regexp
The regex now declares each capture group by name, so `parse_match`
reads as a flat name-to-name map rather than "group 7 || 9 || 11"
arithmetic. Adding or removing a branch later doesn't ripple through
group numbers — historically that was the bug-window in this method.

Also bring the class-level docstring up to date with `##N`/`###N`
quickinfo macros (which were missing from the docs entirely) and the
new `#PROJ-N` semantic-id forms.
2026-05-15 08:24:14 +03:00
Kabiru Mwenja 8bddd2d24b Accept semantic work-package identifiers in text macros
`#1234` text macros render `formatted_id`/`display_id` already, but the
*input* side still requires the numeric primary key. Authors typing or
pasting `#PROJ-1` (or `##PROJ-1` / `###PROJ-1`) in a comment, WP
description, or meeting body would see literal text rather than a
resolved link.

Three changes that move together:

1. `ResourceLinksMatcher.regexp` — the hash-separator branch now accepts
   either the numeric shape `\d+` or the semantic shape
   `[A-Z][A-Z0-9_]*-\d+`, mirroring `WorkPackage::SemanticIdentifier::
   ID_ROUTE_CONSTRAINT`. The revision branch (`r\d+`) stays numeric-only
   via a separate alternation. `parse_match` is the single site that
   maps the new regex group indices to semantic field names; everything
   else flows from there.

2. `LinkHandlers::WorkPackages#call` — splits into a numeric path
   (preserving the leading-zero rejection from before) and a semantic
   path. Semantic-shape input only links when `semantic_mode_active?` is
   true; classic instances render literal text. Plain `#PROJ-N` requires
   a cache hit (literal-text fallback when missing); `##PROJ-N` /
   `###PROJ-N` quickinfo elements emit unconditionally with `data-id`
   set to the user-facing identifier — APIv3 already resolves either
   shape, and the frontend Angular component handles missing WPs.
   Hover-card URLs now also speak `display_id` so the URL matches the
   user-facing identifier (the route accepts both shapes — see
   `HoverCardComponent#initialize`).

3. The preload cache extends to string keys via the
   `WorkPackage.where_display_id_in` batch finder added in #23016.
   `with_preloaded_resources` runs one WP SELECT for the common case
   (numerics + current semantic identifiers); historical alias
   references add a second targeted alias-table pluck, so an
   alias-heavy doc costs at most two round-trips per render.

Specs cover: `#PROJ-N` resolves with formatted_id / display_id href in
semantic mode; classic mode leaves it as literal text and issues zero
WP SELECTs; `##/###` quickinfo carries `display_id` in `data-id`; mixed
numeric+semantic resolves in 1 SELECT; alias references resolve in 2
round-trips; `#GHOST-99` falls through cleanly; nested `format_text`
calls preserve outer save/restore semantics.
2026-05-15 08:24:14 +03:00
Kabiru Mwenja 5518ccfe6b Extract PreformattedBlocks; share parse_match; trim WP preload columns
Address follow-up polish on PR 22976 review:

- Extract `OpenProject::TextFormatting::PreformattedBlocks` (BLOCKS set +
  ancestor? helper) so PatternMatcherFilter and ResourceLinksMatcher share
  a single source of truth for the `<pre>`/`<code>` ancestry skip.
- Lift `parse_match(match)` so `process_match` and `extract_work_package_id`
  consume the same regex group → semantic name mapping.
- `with_preloaded_resources` captures `previous` unconditionally so the
  `ensure` block no longer needs a `defined?(previous)` guard.
- Preload `WorkPackage.where(id: ids).select(:id, :identifier)` only —
  `display_id`/`formatted_id` don't read other columns.
- N+1 spec switches from `have_a_query_limit(1)` to a SQL-notification
  subscriber filtered to `FROM "work_packages"` SELECTs, avoiding false
  positives from incidental Setting/User/Project queries.
2026-05-15 08:24:13 +03:00
Kabiru Mwenja 071edb6007 Address review feedback on PR 22976
Three changes from the rails-code-reviewer agent + the GitHub Copilot
reviewer:

1. Switch from `thread_mattr_accessor` to `RequestStore.store` for the
   per-render WorkPackage lookup. Aligns with the existing `Cache`,
   `Setting`, `CustomStyle`, and `WorkPackage#available_custom_field_key`
   conventions in the codebase. Cleanup-on-request-end via
   `RequestStore::Middleware` is defense in depth — the filter's explicit
   save/restore is what actually scopes the cache to a single render.

2. Save/restore around the per-node loop, exposed via a single yielding
   API on the matcher: `with_preloaded_resources(doc, context) { … }`.
   The previous lookup is captured on entry and restored on exit, so a
   nested `format_text` (custom-field formatter, recursive markdown
   render, etc.) cannot clobber the outer render's lookup. The flat
   `preload_for_doc`/`cleanup_after_doc` pair is replaced; the filter
   recursively wraps the loop in each matcher's hook.

3. Wrap the lookup behind two class methods — `work_package_for(id)` for
   the link handler, `with_preloaded_resources` for the filter — so the
   RequestStore key stays a private constant. Future storage swaps don't
   ripple to callers.

Plus the GitHub Copilot review's two findings:

- Skip the preload entirely in classic mode (`semantic_mode_active?`
  guard). `display_id` and `formatted_id` collapse to the numeric form,
  so the link handler renders the legacy shape from `wp_id` alone — no
  DB load required, restoring pre-PR query-free behaviour. New spec
  "classic mode is query-free" is the regression guard.

- `extract_work_package_id` now requires `prefix.nil?` so prefixed
  matches like `version#3` and `message#12` don't contribute to the
  preload set, mirroring `LinkHandlers::WorkPackages#applicable?`.

New specs:

- save/restore semantics: nested `with_preloaded_resources` calls
  preserve the outer lookup and clear after the outer block exits.
- classic mode: `format_text("#1 #2 #3")` issues zero `work_packages`
  SELECTs.

Refs https://github.com/opf/openproject/pull/22976#pullrequestreview-4194628380
2026-05-15 08:24:13 +03:00
Kabiru Mwenja 3cbe554414 Use formatted_id and displayId for #N text macros
In semantic mode, plain `#N` references inside formatted text rendered
`<a href="/work_packages/N">#N</a>`. The link should carry the
human-readable identifier on both the label and the href, matching how
the `##N` quickinfo macro already renders via Angular.

Two pieces:

1. `PatternMatcherFilter` gains an opt-in pre/cleanup hook around the
   per-node loop. Matchers that own per-render lookup caches (e.g. a
   batched WP load) implement `preload_for_doc` and `cleanup_after_doc`
   to populate and drop them.

2. `ResourceLinksMatcher` implements those hooks: it scans every text
   node for `#N` matches, runs a single batched `WorkPackage.where(id:
   ids)`, and exposes the result via a thread-isolated class attribute.
   The `WorkPackages` link handler reads from it to choose
   `wp.formatted_id` for the label and `wp.display_id` for the href.
   Falls back to the legacy `#N` shape when the WP isn't loadable
   (deleted, out of scope, or no preload ran).

Visibility filtering is intentionally not introduced — the matcher
links regardless of viewer permissions on the referenced WP, preserving
pre-existing behaviour. Out of scope for this ticket.

Refs https://community.openproject.org/work_packages/74315
2026-05-15 08:24:12 +03:00
Kabiru Mwenja 392ba04d1d Use formatted_id in journal cause formatter and link_to_work_package
The activity feed renders an automatic entry whenever a parent / child /
predecessor / related work package change cascades dates onto the
current WP. Both render paths spoke numeric ids:

- The plain-text branch (used for the API JSON) hardcoded `##{id}`.
- The HTML branch went through `link_to_work_package`, which built the
  visible link label as `Type ##{id}: subject`.

Both now use `formatted_id`, which produces `PROJ-7` in semantic mode
and `#42` in classic — same shape, mode-aware. The link href was
already mode-correct via `to_param`; only the visible label needed the
swap.
2026-05-13 17:41:48 +03:00
Oliver Günther 928da22493 Bumped version to 17.4.1
[ci skip]
2026-05-13 08:46:32 +02:00
Oliver Günther e5ce2359f6 Bumped version to 17.3.3
[ci skip]
2026-05-13 07:59:38 +02:00
Alexander Brandon Coles 9d4881216b Merge remote-tracking branch 'opf/dev' into HEAD
# Conflicts:
#	frontend/src/assets/sass/backlogs/_master_backlog.sass
#	modules/backlogs/config/locales/crowdin/ru.yml
#	modules/wikis/config/locales/crowdin/ru.yml
#	modules/wikis/config/locales/crowdin/uk.yml
#	modules/wikis/config/locales/crowdin/zh-CN.yml
2026-05-08 10:35:12 +02:00
Oliver Günther 7ae5604869 Merge pull request #23070 from opf/fix/relation-visible-scope
The performance decreased because of a combination of calls that were supposed to increase performance. 

We have a mechanism in place which automatically eager loads models needed in the element representers when a collection of them is rendered. This is to avoid N+1 queries of course. But, if eager loading is  combined with e.g., a LIMIT, which we do because we paginate, rails automatically falls back to issuing two instead of just one SQL statement. Which makes sense as otherwise LEFT JOINS might mess with the result set. 

But Rails does so in a somewhat simple fashion. It uses the first query to get the DISTINCT ids. The second is used to load the values (without a limit). But instead of removing all WHERE statements in the second SQL statement and then apply just the one for the ids, it keeps the original WHERE statement and applies the one for the ids on top. The problem with that is that the database trips on that (I didn't check the why) and uses a less than optimal query plan. 

That was the problem here as well. The first query remained reasonable quick (300ms) but the second one took 25s. 

The fix is to split the two statements by hand in the representer whenever eager loading is defined. The first query has all the filters but no eager loading and fetches the ids. The second takes the ids, and with eager loading included loads the data. Et voila, second query takes 10ms.   

That at least works for relations, work_packages and projects. But there are other representers that also seem to have custom behaviour. I'm looking into whether they can be easily adapted.
2026-05-07 15:44:13 +02:00
Klaus Zanders b3d51774b3 Merge pull request #22968 from opf/resource-management-module
Introduce Resource Management Module
2026-05-06 17:07:07 +02:00
OpenProject Actions CI 0778811719 Merge branch 'release/17.4' into dev 2026-05-06 12:47:03 +00:00
Oliver Günther c660802146 Merge remote-tracking branch 'origin/release/17.3' into release/17.4 2026-05-06 09:19:25 +02:00
Jan Sandbrink 5c31dadceb Merge pull request #22946 from opf/extract-wp-page-links
Create reverse page links from internal wiki to WorkPackages
2026-05-06 08:10:36 +02:00
Klaus Zanders 56e3d6214d Do not show Resource Management permissions in Role Editor 2026-05-05 15:30:51 +02:00
Oliver Günther 5bf27bb868 Merge remote-tracking branch 'origin/release/17.4' into dev 2026-05-05 12:29:43 +02:00
Andrej 49f2465efe Merge pull request #22998 from opf/bug/74536-errors-with-include-project-work-package-list-filter-with-a-portfolio
include filter working with portfolios and programs
2026-05-05 12:27:38 +02:00
Alexander Brandon Coles 83573af155 Merge dev into release merge branch
Resolve Backlogs sprint conflicts by porting the release assignability
changes onto the renamed Sprint model.
2026-05-05 08:51:13 +01:00
Jan Sandbrink 0b06198e6f Improve readability of patch_with_namespace
Using Ruby's splat operator to make clear how the module name is constructed,
avoiding the use of args[0..-2], which (IMO) is not easy to read.
2026-05-05 08:35:45 +02:00
Oliver Günther 80d8571992 Tighter css sanitization rules
So far, we've been using the relaxed sanitization rule set. We only need some styles for the table display in CKEditor,
other styles should not be allowed.
2026-05-04 15:12:31 +02:00
ulferts 1449f12197 bump grape & mustermann 2026-04-30 15:24:34 +02:00
David F 68f3d335fa Add optional hidden sentinel field for Primer’s checkbox groups. wp/74398 2026-04-30 11:10:28 +02:00
ulferts 222639f8d4 include filter working with portfolios and programs 2026-04-30 10:18:09 +02:00
Kabiru Mwenja 0d79f5358c Bust WP representer JSON cache when identifier mode flips (#22960)
Include identifier mode in WP representer cache key
2026-04-28 14:22:56 +03:00
Klaus Zanders a9a6898153 Remove inline disables for DynamicFindBy 2026-04-27 09:22:18 +02:00
Oliver Günther ab301f7de4 Merge pull request #22875 from opf/fix/validate-conditions-on-custom-action-execute
Validate conditions on execution of custom action
2026-04-27 08:41:04 +02:00
OpenProject Actions CI 9569225e56 Merge branch 'release/17.4' into dev 2026-04-25 04:23:14 +00:00
Oliver Günther 8eca1925ec Respect activation limit in user unlocking
https://community.openproject.org/work_packages/74373
2026-04-24 09:19:06 +02:00
Kabiru Mwenja 5d3abc6c80 Merge pull request #22858 from opf/implementation/74200-use-displayid-in-work-package-breadcrumbs
[#74200] Use displayId in work package breadcrumbs
2026-04-23 18:00:56 +03:00
Oliver Günther 6877e29351 Validate current user password confirmation when changing passwords through API
https://community.openproject.org/work_packages/74335
2026-04-23 14:19:50 +02:00
Oliver Günther 971fe2a45d Bumped version to 17.5.0
[ci skip]
2026-04-23 08:35:04 +02:00
OpenProject Actions CI ca54954220 Merge branch 'release/17.3' into dev 2026-04-23 04:33:10 +00:00
Oliver Günther 8ca79798db Validate conditions on execution of custom action
https://community.openproject.org/projects/openproject/work_packages/74294/activity
2026-04-22 14:12:42 +02:00
Oliver Günther 02ae6a9119 Remove password min-rules in favor of clearly listed/checked password rules
https://community.openproject.org/work_packages/73461
2026-04-22 13:43:42 +02:00
Oliver Günther 7135dfc2c4 Add additional validations for path validation for posix on repositories 2026-04-22 11:23:37 +02:00
Kabiru Mwenja b8471484e0 Include identifier in Hierarchy eager loader's children SELECT
The ancestors/children representer change calls `child.display_id`,
which consults `identifier` in semantic mode. The Hierarchy eager
loader preloads children with a minimal `SELECT id, subject, project_id,
parent_id` for performance, so `identifier` was missing and
`ActiveModel::MissingAttributeError` fired the moment a query endpoint
rendered a work package with visible children in semantic mode.

Add `identifier` to the SELECT. It's one extra short text column per
child row.
2026-04-22 08:50:32 +03:00
Kabiru Mwenja 3e0f738c2c Expose displayId on work package ancestor and children HAL links
In semantic mode, the work package breadcrumb renders numeric IDs
instead of the semantic identifier because ancestor HAL resources are
built from `_links.ancestors[]` entries that only carry `href` and
`title`. With no top-level `displayId` in `$source`, the frontend getter
falls through to the numeric id parsed from the href.

Emit `displayId` alongside `href`/`title` on each ancestor and child
link in the representer, and have the `displayId` getter fall back to
the self link's `displayId` so resources built from a link payload
alone still surface the semantic identifier.
2026-04-22 08:18:54 +03:00
Pavel Balashou 10f2ed7efe Merge pull request #22842 from opf/jira-import-ssrf
Use ssrf filtering in Jira Import.
2026-04-21 15:26:40 +02:00
Tobias Dillmann a42eb98f87 [#73104] Move api resource link out of the model 2026-04-21 13:55:12 +02:00
Tobias Dillmann 4d3205636e [#73104] Remove backlogs module pollution from core
We delegate the resource name to the object itself, that way we can
override it in our record and no knowledge about the backlogs module is
necessary in the core.
2026-04-21 13:55:12 +02:00
Tobias Dillmann 281e44762f [#73104] Allow resource linking for sprints
This is necessary to make the group_by work
2026-04-21 13:55:12 +02:00
Kabiru Mwenja 234a870060 Merge pull request #22704 from opf/feature/73716-adapt-work-package-show-view-for-project-based-semantic-work-package-identifiers
Adapt work package show view for semantic identifiers
2026-04-21 13:52:23 +03:00
Kabiru Mwenja 79d4e67a0d Merge pull request #22718 from opf/feature/73756-adapt-routes-for-project-based-semantic-work-package-identifiers
Make find/exists? resolve semantic work package identifiers
2026-04-21 13:13:54 +03:00