465 Commits

Author SHA1 Message Date
Kabiru Mwenja f94d7b0bfc Resolve the changes-filter predecessor once in a journals CTE
The :only_changes filter re-seeked each journal's predecessor in every EXISTS
branch (~7 LATERAL lookups per row). A CTE now shadows the journals table,
exposing predecessor_id/predecessor_data_id once per row, and each branch reads
those columns instead. On a 703-journal work package this cuts the COUNT from
~1.13M to ~35K shared buffers.
2026-06-09 10:25:21 +03:00
Kabiru Mwenja 32a8bc43cc Surface the predecessor lateral alias at each call site
The LATERAL subquery is aliased `predecessor` where it is joined rather
than inside the helper, so the relation each EXISTS clause references is
visible without reading the helper.
2026-06-09 08:36:32 +03:00
Kabiru Mwenja 879cb108c9 Seek changes-filter predecessor via LATERAL instead of a version scan
The :only_changes activity filter identified each journal's predecessor
with `version = (SELECT MAX(version) WHERE version < current)`. That
predicate cannot use the (journable_type, journable_id, version) index,
so Postgres scanned every journal of the journable and filtered by
version — turning a per-page filter into an O(history) sweep run twice
(pagy's count plus the page query). A LATERAL `ORDER BY version DESC
LIMIT 1` seeks the predecessor through that index in a single row,
preserving gap-tolerant matching on `< version`.
2026-06-08 10:18:05 +03:00
Kabiru Mwenja fd22629702 Code Maintenance/STC-462: Tidy the activity-tab paginator and centralise filter modes (#23552)
https://community.openproject.org/wp/STC-462

Two readability passes over the work package activity tab, no behaviour change. The paginator's private methods are reordered to follow their call order so the file reads top-down from `#call`, and the three activity filter modes (`:all`, `:only_comments`, `:only_changes`) — until now bare symbols duplicated across the controller, paginator, journal components and the hidden form — move into a single `WorkPackages::ActivitiesTab::Filters` module so the modes have one source of truth and can't drift apart. The diff reaches beyond the paginator into the controller, several components and a form, since that's where the symbols were scattered.
2026-06-05 16:50:08 +03:00
Kabiru Mwenja 2dcc7dd5d1 Remove dead paginate entry point and clarify resolved-anchor recording
The Paginator.paginate class method bypassed the instance, discarding the
resolved_anchor state the controller reads after .call; it had no callers, so
drop it and keep the single new(...).call entry point. Extract the activity
anchor side effect into record_resolved_anchor so the intent is explicit at the
call site, and pin the server contract with a request spec asserting an
unresolvable activity anchor omits the resolved-comment value.
2026-06-05 14:38:02 +03:00
Kabiru Mwenja 5fdf57df69 Defer activity-tab sequence_version to anchor resolution
The work package activity tab computed a per-journal sequence_version on
every render — a ROW_NUMBER() window function over a LATERAL join — only to
stamp the legacy data-anchor-activity-id that #activity-N deep links rely on.
Nothing mints those links anymore; copy and share links use
#comment-<journal id>, which needs no extra query.

The activity number is now resolved on demand. Only a request carrying
?anchor=activity-N runs the window function, mapping the number to a journal
id the paginator exposes as resolved_anchor. The view hands that to the
client, which rewrites #activity-N to the canonical #comment-<id> and scrolls
using the comment anchor already present in the DOM. Default renders no longer
touch the window function.

References WP #68063.
2026-06-05 14:26:04 +03:00
Kabiru Mwenja 376f5fcdc3 Code Maintenance/STC-462: Move work package activity tab from in-memory to database-level pagination (#23434) 2026-06-05 14:21:47 +03:00
Oliver Günther 11dc79a74d Merge remote-tracking branch 'origin/release/17.5' into dev 2026-06-02 16:22:39 +02:00
Oliver Günther 9993792dbf Merge remote-tracking branch 'origin/release/17.4' into release/17.5 2026-06-02 16:22:18 +02:00
Oliver Günther 2a6412d5ae Merge remote-tracking branch 'origin/release/17.3' into release/17.4 2026-06-02 16:22:05 +02:00
Oliver Günther bef69b6aaf Parse query params as a separate options hash, not kwargs 2026-06-02 16:21:14 +02:00
OpenProject Actions CI 49b802ca02 Merge branch 'release/17.5' into dev 2026-05-28 08:51:09 +00:00
Kabiru Mwenja 6e3068531f Write primary-key WP refs in auto-generated journal notes
The earlier write-time canonicalization stored the rendered
display_id ("#PROJ-7") in the journal note, which would rot under
project-identifier renames and leave dangling semantic strings if
semantic mode were rolled back. Restore the PK shape ("#42") and
let the formatter pipeline turn it into the user-facing identifier
at render time, where the resolver already handles both modes.

Both spec contexts now assert the same PK shape; the mode-specific
rendering of "#N" lives in the formatter specs.
2026-05-26 14:19:25 +03:00
Kabiru Mwenja 878048f8e8 Resolve WP labels across visibility boundaries in text macros
The macro preload was visibility-scoped — references to work packages the
recipient cannot see would fall through to the literal `#43` shape, even
when the same reference rendered as `DCP-1` for an author with full view
permission. Notification recipients saw misleading numeric ids for cross-
project references in journal notes.

Splits label resolution from link gating:

- `ResourceLinksMatcher.build_lookup` now does an unscoped fetch for the
  primary identifier and a separate visibility-scoped id pluck. The link
  handler reads `visible_to_current_user?` to decide between a navigable
  anchor and a plain-text label.
- `UpdateAncestorsService#set_journal_note` writes `#display_id` so new
  notes carry the semantic shape at the source; render-time resolution
  heals legacy `#N` content for users with view permission.

Tradeoff: a recipient without view permission now sees the WP's semantic
identifier (e.g. `DCP-1`) as plain text rather than `#43`. The reference's
existence was already disclosed by the stored journal text; the project
identifier is the only new piece of information surfaced, and is not
treated as a secret elsewhere in the system (URLs, exports, API).
2026-05-26 14:18:48 +03:00
Alexander Brandon Coles 1ba2dd2775 [#75314] Replace deprecated Pagy max_pages option
Pagy 43.4.3 deprecated `:max_pages` and recommends capping records
before pagination instead. Caps the combined array in `base_journals`.

https://community.openproject.org/wp/75314
2026-05-26 11:56:20 +02:00
Kabiru Mwenja 59a631e983 Scope dirty-state reset to identifier and sequence_number
clear_changes_information would also clobber any unrelated dirty
attributes the work package happened to be carrying at this point.
The intent is narrower: only the two attributes we just synced from
the raw-SQL UPDATE should be marked clean. Use clear_attribute_changes
with the explicit attribute list instead.
2026-05-05 15:55:28 +03:00
Kabiru Mwenja 8602aff838 Use clear_changes_information after raw-SQL identifier assignment
clear_changes_information is the established pattern in this codebase
for syncing in-memory dirty state after a side-channel persist (queries,
historic-attribute eager loading, progress/date-picker controllers).
changes_applied has the same effect but no other call site, so prefer
the local convention.
2026-04-30 18:14:24 +03:00
Kabiru Mwenja 8338a8158a Apply WP identifier assignments in-memory after move
Previous fix reloaded each moved work package after the bulk SQL
UPDATE so HAL representers and the move-and-follow redirect saw the
freshly allocated semantic id. That cost one round trip per WP, which
matters when a parent moves with descendants. Now that
reserve_semantic_id_block! returns the {wp_id => identifier}
assignments directly, apply them with assign_attributes +
changes_applied — same observable result, zero extra queries
regardless of bulk size.
2026-04-30 17:44:38 +03:00
Kabiru Mwenja a54362020f Refresh in-memory identifier after WP move re-allocation
Project#reserve_semantic_id_block! rewrites work_packages.identifier
and sequence_number via a raw SQL UPDATE, leaving the in-memory
records with the nil identifier set by SetAttributesService when the
project changed. Callers that read the WP straight out of the
ServiceResult (HAL action links, the move-and-follow redirect in
WorkPackages::BulkJob#redirect_path) then saw an empty display_id and
fell back to numeric URLs. Reload after the bulk allocation so the
in-memory state matches the database.
2026-04-30 16:56:27 +03:00
Tomas Hykel c85a69c476 [#74192] Clear semantic identifier when moving a work package 2026-04-21 22:35:47 +02:00
Tomas Hykel 9b38c082ab [#73613] Improve peformance of bulk semantic ID allocation 2026-04-21 22:26:25 +02:00
Tomas Hykel 3a7fad89df [#73711] refactor: Rename the project identifier namespace 2026-04-16 21:40:38 +02:00
Tomas Hykel cc475e6c2e [#71645] Improve interface of WP identifier setting controller 2026-04-16 20:35:34 +02:00
Kabiru Mwenja 58cde218bb Rename private reserved_identifiers to historical_identifiers
The class method .reserved_identifiers returns all slugs (current +
historical) for use as an exclusion set. The private instance method
only returns historical slugs no longer active. Having both share
the same name with different semantics is a maintenance trap.

Rename the private method to historical_identifiers so exclusion_set
reads naturally: historical_identifiers | in_use_identifiers.
2026-04-13 13:59:36 +03:00
Kabiru Mwenja 10fd218ffd Use FriendlyId slug table directly for exclusion set
The friendly_id_slugs table already contains both current and historical
project identifiers (backfilled by InitializeHistoricIdentifiers migration,
maintained by FriendlyId history module on create/update). Query it
directly via ProblematicIdentifiers.reserved_identifiers instead of
merging Project.pluck with a filtered slug query.
2026-04-13 09:09:40 +03:00
Kabiru Mwenja f1c76116a5 Simplify semantic identifier generation
Remove SemanticIdentifierGenerator wrapper class and move collision-aware
logic directly into Projects::Identifier.suggest_identifier. This gives
both the model callback and the frontend suggestion controller automatic
collision detection.

Promote reserved_identifiers to a class method on ProblematicIdentifiers
since it has no instance state dependency.
2026-04-13 08:41:59 +03:00
Tomas Hykel a58386ae76 Revert "expose reserved_identifiers via class variable"
This reverts commit 83499c71c8.
2026-04-09 16:15:13 +02:00
Tomas Hykel 83499c71c8 expose reserved_identifiers via class variable 2026-04-09 16:13:56 +02:00
Tomas Hykel 2337a25b14 fix: Correctly auto-generate semantic identifiers on project copy 2026-04-09 16:03:17 +02:00
Tomas Hykel c4debc8aaa [#73523] Implement WorkPackage semantic ID allocation system 2026-04-01 17:25:19 +02:00
Kabiru Mwenja 2b4478da68 Merge pull request #22406 from opf/open-point/73149-how-do-we-handle-project-identifiers-case-insensitive-storage
Add case-insensitive uniqueness enforcement for project identifier
2026-03-27 13:26:38 +03:00
Jan Sandbrink f9d8bc6614 Introduce SubclassResponsibility error
This error is intended for cases when a method is
intentionally not implemented, because the module/class defining
it expects a subclass (or class including the module) to implement
the method.

This is intended to distinguish it from other cases, such as:
* feature not implemented yet
* edge case of a method call not yet supported

Notably it avoids the misuse of the Ruby-defined NotImplementedError,
which is only intended for much more specific scenarios:

> Raised when a feature is not implemented on the current platform. For example, methods depending on the fsync or fork system calls may raise this exception [...]

Also see https://docs.ruby-lang.org/en/master/NotImplementedError.html
2026-03-27 08:14:56 +01:00
Kabiru Mwenja 4812974de9 Upcase exclusion set for case-insensitive slug matching
The suggestion generator produces uppercase candidates and checks
exclusions with case-sensitive `include?`. Historical slugs stored in
lowercase (e.g. "proj") would not block the generator from suggesting
"PROJ", even though `identifier_not_historically_reserved` would reject
it on save via LOWER(). Upcasing the exclusion set at the consumption
point keeps ProblematicIdentifiers#exclusion_set casing-neutral for
other consumers while ensuring the preview never proposes identifiers
that collide with historical slugs.
2026-03-25 12:16:10 +03:00
Kabiru Mwenja f0ff8030ec Cleanup tests and add lossy rollback note
Unify test helpers into a single create_project_with_raw_identifier
method that better communicates intent (bypasses validations to set
an exact identifier). Add project.reload after update_all so the
in-memory object stays consistent with the database.

Also documents that the migration rollback is intentionally lossy —
deduplicated identifiers keep their suffixes under the restored
case-sensitive index.
2026-03-24 08:10:18 +03:00
Kabiru Mwenja 2803d2d280 Use case-insensitive comparison for reserved identifier exclusion
After the background autofix job converts identifiers from lowercase
to uppercase, historical slugs in friendly_id_slugs will differ in
case from current project identifiers. Without LOWER() in the NOT IN
subquery, those slugs are incorrectly included in the reserved set,
causing the suggestion generator to over-reserve.

Example: a project created in numeric mode gets identifier "proj" and
FriendlyId records slug "proj". The autofix job later uppercases it to
"PROJ". Now the case-sensitive `slug NOT IN (SELECT identifier ...)`
sees "proj" != "PROJ" and incorrectly treats "proj" as reserved,
preventing the suggestion generator from offering it.
2026-03-24 08:10:17 +03:00
Kabiru Mwenja 5c7bed193e Apply underscore fix to ProblematicIdentifiers after rebase
The underscore fix (allowing _ in identifiers per spec) needs to target
ProblematicIdentifiers instead of PreviewQuery after the extraction
refactor on the target branch.
2026-03-23 17:24:55 +03:00
Kabiru Mwenja 6c8421345a Simplify exclusion_set to plain Set, remove ExclusionSet class
Drop the DB-backed ExclusionSet in favour of a simple Set from pluck.
This is a one-off admin migration — the brief memory cost of loading
all non-problematic identifiers is not worth the added complexity of
a custom duck-typed wrapper.

Also adds performance notes documenting index considerations for the
regex scope conditions and the eager-load trade-off.
2026-03-20 23:06:26 +03:00
Kabiru Mwenja 30f9c7c633 Wire up reserved identifiers with FriendlyId slug history
Replace the TODO stub in ProblematicIdentifiers#reserved_identifiers
with a real query against the friendly_id_slugs table. Historical slugs
(identifiers a project used in the past but has since changed) are now
excluded from suggestion generation and classified as :reserved in
error_reason output.

The query excludes slugs that match a current active project identifier
(those are already covered by the in_use exclusion path).
2026-03-20 22:57:07 +03:00
Kabiru Mwenja 69818ad10c Extract ProblematicIdentifiers from PreviewQuery
Separate scope-building, identifier classification, and exclusion set
logic into a reusable class that both PreviewQuery (admin UI) and the
future ApplyHandlesJob (batch migration) can compose from.

Key changes:
- ProblematicIdentifiers owns FORMAT_RULES, problematic scope, error
  classification, and a dual-mode exclusion set (DB-backed ExclusionSet
  for preview, preloaded Set for batch)
- PreviewQuery becomes a thin orchestrator delegating to
  ProblematicIdentifiers
- Add deterministic ordering (.order(:id)) to preview results
- Rename :not_uppercase → :not_fully_uppercased to match scope method
  naming, update corresponding en.yml translation key
- ExclusionSet uses raw SQL to bypass Rails normalizes on :identifier
  which would transform query parameters based on current setting mode
2026-03-20 22:55:54 +03:00
Kabiru Mwenja 91362fb680 Improve PreviewQuery error detection and update MCP descriptions
Expand PreviewQuery to detect additional alphanumeric identifier
format violations: case-mismatch, underscores, leading digits, and
purely numerical identifiers. Restructure error classification with
FORMAT_RULES for clarity and add corresponding locale strings.

Update MCP tool descriptions for search_projects, search_programs,
and search_portfolios to reflect case-insensitive identifier
matching, with updated test expectations.
2026-03-20 21:35:08 +03:00
Kabiru Mwenja 480db19b19 Simplify ProjectIdentifierSuggestionGenerator interface (#22439)
Replace the separate `reserved_identifiers` and `in_use_identifiers` keyword arguments with a single `exclude` parameter.

The generator immediately merged these two sets on every call path -- the distinction only matters to `PreviewQuery` for error classification, which it already handles independently. Collapsing them into one parameter removes unnecessary coupling.
2026-03-20 20:25:37 +03:00
Kabiru Mwenja d4a8ca19cc Inline set union, remove combined_identifiers wrapper 2026-03-16 11:59:33 +03:00
Kabiru Mwenja f8d1663827 Use it syntax and method chaining for single-expression blocks 2026-03-16 11:58:20 +03:00
Kabiru Mwenja 25e5c41208 Reduce allocations: string slicing, single-pass filter, simpler set merge
- Replace scan+join with gsub for stripping non-alphanumeric chars
- Use string slicing instead of .chars arrays throughout multi-word
  pipeline, eliminating intermediate array allocations per word
- Merge two-pass filter_map+select into single filter_map
- Replace index.with_index enumerator with each_index.find
- Simplify combined_identifiers from splat+reduce to set union
2026-03-16 11:55:42 +03:00
Kabiru Mwenja 61e20a0784 Improve readability: split dense one-liner, clarify mutation and contracts 2026-03-13 18:37:02 +03:00
Kabiru Mwenja 68ef621217 Simplify multi-word pipeline, harden digit-stripping, add ordering test
- Flatten multi-word candidate generation from 5 methods to 4 by
  removing unnecessary indirection layers
- Apply ensure_starts_with_letter in numeric_suffix_fallback so
  fallback identifiers also satisfy the starts-with-letter constraint
- Add test verifying batch mode assigns identifiers in array order
2026-03-13 18:24:14 +03:00
Kabiru Mwenja 0053b140a9 Consolidate identifier length constants into IDENTIFIER_LENGTH hash
Group the four related length constants (min, max, base, single_word)
into a single frozen hash for better locality and fewer top-level names.
2026-03-13 18:06:14 +03:00
Kabiru Mwenja f2b23e0d15 Fix identifier generator to match semantic identifier spec
- Replace numeric suffix collision strategy with progressive acronym
  widening ("SC" → "STC" → "STCO" instead of "SC" → "SC2" → "SC3")
- Allow underscores in identifiers (fix regex in PreviewQuery)
- Enforce identifiers must start with a letter (strip leading digits)
- Use DEFAULT_IDENTIFIER_BASE_LENGTH (5) for initial generation with
  MAX_IDENTIFIER_LENGTH (10) as expansion ceiling for collisions
- Enforce MIN_IDENTIFIER_LENGTH (2) for generated identifiers
- Expand single-word identifiers on collision ("BAN" → "BANA" → "BANAN")
2026-03-13 18:01:11 +03:00
Kabiru Mwenja e09288dd6a Address code review: rename column, remove padding, split constants, extract error_reason 2026-03-13 17:07:03 +03:00
Kabiru Mwenja e9900dfe2d Rename ProjectHandleSuggestionGenerator to ProjectIdentifierSuggestionGenerator
Aligns the class name and all internal terminology with the domain
language: "handle" → "identifier" throughout. Renames the file, class,
constants (HANDLE_MAX_LENGTH → IDENTIFIER_MAX_LENGTH, FALLBACK_HANDLE →
FALLBACK_IDENTIFIER), public API (suggest_handle → suggest_identifier,
suggested_handle hash key → suggested_identifier), keyword arguments
(in_use_handles → in_use_identifiers, reserved_handles →
reserved_identifiers), and private helper methods accordingly. All
call-sites and specs updated to match.
2026-03-13 16:38:00 +03:00