434 Commits

Author SHA1 Message Date
Tomas Hykel a30f5d5169 [STC-729] Adapt Excel and CSV exports for semantic identifiers
https://community.openproject.org/wp/STC-729
2026-06-11 20:58:47 +02:00
OpenProject Actions CI 523ef39065 Merge branch 'release/17.5' into dev 2026-06-01 14:42:29 +00:00
Oliver Günther 1f3da064ac Escape CSV formula cells by default 2026-06-01 13:44:29 +02:00
Kabiru Mwenja 14ef6f901d Use semantic identifier for server-rendered search result links
Work package results on the search page build their link through the
acts_as_event url proc, which passed the numeric primary key instead of
the work package's display id. In semantic mode this rendered
/work_packages/<id> even though the row showed the semantic identifier,
unlike Rails URL helpers that already resolve the object via to_param.
Pass display_id so the link follows the same convention everywhere.
2026-05-29 19:12:27 +03:00
Kabiru Mwenja c607b36b8e Rename with_hash_prefix to format_display_id 2026-05-26 22:10:07 +03:00
Kabiru Mwenja 7c9d15e506 Render WP identifiers per current mode in plain-text mailer notes
The `mentioned` and `watcher_changed` text-mailer bodies surfaced raw
journal markdown — numeric `#42` references stayed numeric in semantic
mode, and `<mention>` envelopes leaked as HTML source.

Introduces `:plain_text` as a sibling format inside the existing Plain
module. The filter chain mirrors the markdown pipeline (markdown,
sanitization, mention, pattern-matcher) and finishes with a new
`PlainTextOutputFilter` that collapses the DOM to text. The
`WorkPackages` link handler and `MentionFilter` get plain-text branches
keyed off `context[:plain_text]` so identifier resolution stays in one
place across rich and plain channels.

Closes https://community.openproject.org/wp/74762
2026-05-26 14:18:17 +03:00
Tomas Hykel 948fa43321 chore: Remove feature flag for project-based work package identifiers 2026-05-25 17:45:06 +02:00
Judith Roth ca1cff55af Merge pull request #23093 from opf/feature/74366-adapt-pdf-export-for-semantic-identifiers
[#74366] Adapt PDF exports for semantic identifiers
2026-05-18 13:00:33 +02:00
Alexander 6350700c99 Prevent WorkPackage::SemanticIdentifier::UnsupportedLookup with empty ID (#23213)
---------

Co-authored-by: Kabiru Mwenja <k.mwenja@openproject.com>
2026-05-15 08:36:10 +03:00
Kabiru Mwenja c24e3cfab0 Accept scalars, varargs, and arrays in where_display_id_in
Splat with a depth-1 flatten lets callers pass scalars, varargs, or a
pre-built array interchangeably. `Array(values)` would have been
misleading here — `Array([[1, "a"], 2])` leaves the inner array
intact and `map(&:to_s)` stringifies it as `"[1, \"a\"]"`, which
then misclassifies through the semantic-id branch. Bounded `flatten(1)`
absorbs the (scalar) and ([scalar, scalar]) shapes that production
code already uses while leaving deeper nesting alone, so the same
pathology produces no match rather than silently working.

Refs https://github.com/opf/openproject/pull/23202#discussion_r3235337043
2026-05-13 18:26:23 +03:00
Kabiru Mwenja 4b164150ac Pin ID_ROUTE_CONSTRAINT against silent widening
The route constraint composes the numeric and semantic shapes from
SEMANTIC_ID_PATTERN, which itself composes the project-identifier shape
from `Projects::Identifier::SEMANTIC_FORMAT`. A future change to either
upstream pattern would shift what URLs Rails accepts without touching
routes.rb. Three anchored assertions lock the boundary in place — the
constant is used only as a route constraint, so anchored matching mirrors
the actual call site.
2026-05-13 17:51:44 +03:00
Kabiru Mwenja 0a5b2b8646 Make numeric_id? the exact complement of semantic_id? for Strings
`semantic_id?` already strips surrounding whitespace before its round-trip
check, so `" 123 "` was classified as not-semantic. `numeric_id?` did
not strip, so the same value was also not-numeric — leaving it
"neither" and unreachable through normal routing.

Stripping in `numeric_id?` and expressing the String branch as
`!semantic_id?(value)` removes the duplicated round-trip logic and gives
the routing predicates a single source of truth: any String classified
not-semantic is necessarily numeric. Integer/nil/other types are
unaffected — they fall on their own branches.

Routing-table spec gains a `" 123 "` row to lock the symmetry in.
2026-05-13 17:51:43 +03:00
Kabiru Mwenja c67978a7d1 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.

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 cost-query filter switches to the predicate in this slice; the
text-formatting and PDF callers convert in a follow-up.
2026-05-13 17:51:43 +03:00
Judith Roth b26b1ef0d7 [#74366] Adapt Contract PDF export for semantic identifiers
https://community.openproject.org/wp/74366

And also improve rendering of linked work packages for this.
Before, the macros were not expanded, but only the hashes and
ID output directly.
2026-05-07 21:06:06 +02:00
Kabiru Mwenja e59937858c Add WorkPackage.where_display_id_in batch finder
The plural counterpart to find_by_display_id resolves a mix of
numeric and semantic identifiers in a single composite scope so
callers (notably ApplicationController#find_work_packages, used by
bulk WP routes) do not have to branch on the input shape or partition
into numeric/semantic themselves. Mirrors scope_for_semantic_identifier
in using `.or` to union the primary-key, current-identifier, and
historical-alias arms; unknown values resolve to no match rather
than poisoning the rest of the set.
2026-04-30 17:44:01 +03:00
Kabiru Mwenja 4dfdd6ec5d Drop numeric pins on auxilary links 2026-04-30 11:08:48 +03:00
Kabiru Mwenja 131f595ea4 Remove unnecessary guard for simpler safe navigation 2026-04-30 08:27:30 +03:00
Kabiru Mwenja 6e2259fc49 Override WorkPackage#to_param for semantic-id URLs
Rails URL helpers — `work_package_path(@wp)`, `polymorphic_path(@wp)`,
`form_for(@wp)` — now produce `/work_packages/PROJ-7` automatically in
semantic mode, without callers having to thread `wp.display_id` through
every path-helper invocation.

In classic mode `display_id` returns the integer primary key, so this is
behaviourally identical to the inherited `id&.to_s`. The override gates
transitively on the existing `semantic_work_package_ids` flag via
`display_id`'s mode check — no new flag is introduced.

API v3 deliberately bypasses this by passing `id:` kwargs explicitly in
the representer, so HAL self-links remain numeric and stable for API
consumers.
2026-04-30 08:27:26 +03:00
Kabiru Mwenja 7192ff276c Merge branch 'dev' into implementation/73922-use-formattedid-in-autocompleter-and-global-search-results 2026-04-23 17:57:19 +03:00
Tomas Hykel 0f9ea744c6 [#73614] Improve validation of semantic identifiers for WPs 2026-04-22 16:34:07 +02:00
Kabiru Mwenja 865956a29b Merge branch 'implementation/73797-use-displayid-in-work-package-urls' into implementation/73922-use-formattedid-in-autocompleter-and-global-search-results 2026-04-21 21:25:24 +03:00
Tomas Hykel 56f130d9f2 [#71645] Convert instance to semantic identifiers 2026-04-21 19:34:37 +02:00
Kabiru Mwenja b1025e6a0d Align formatted_id with PR 22788
Adopts PR 22788's exact method body, comment, and spec text for
WorkPackage#formatted_id so the two PRs produce byte-identical diffs
on the overlapping hunks. Git's 3-way merge auto-resolves when either
PR lands first — no rebase conflict either way.

Functionally equivalent to the previous implementation; the is_a?(String)
guard is a no-op since display_id never returns anything exotic in
practice, but it keeps the two branches textually in sync.
2026-04-21 20:02:43 +03:00
Kabiru Mwenja 143b3b2da9 Fix autocompleter label for pre-populated work packages
The reporting filter widget pre-populates opce-autocompleter with
plain hashes ({id:, name:}). When the autocompleter template switched
from `#{item.id}` to `{{ item.formattedId }}`, those hashes had no
formattedId key and the ID prefix disappeared from the selected-value
label, breaking work_package_costlog_spec and time_entry_activity_spec.

Extend Widget::Filters::WorkPackage#map_filter_values to emit
formattedId, displayId, and subject alongside id/name so the
autocompleter template has the data it needs.

Centralise the formatting logic in a new WorkPackage#formatted_id
model method, mirroring the frontend formatWorkPackageId helper:
semantic identifiers pass through (PROJ-42); numeric ids are
prefixed with # (#42).
2026-04-21 19:59:02 +03: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 270da251e0 Raise UnsupportedLookup from multi-arg find for consistency
find_by already raises WorkPackage::SemanticIdentifier::UnsupportedLookup
when a semantic identifier is passed through id:/identifier:. Align the
multi-argument find guard so callers can rescue a single specific error
across both entry points, while the ArgumentError parent class keeps
legacy rescuers working.
2026-04-21 12:39:04 +03:00
Kabiru Mwenja e545bd4b79 Pin find_by multi-keyword guard with explicit test
Document that find_by(subject: x, id: "PROJ-1") raises
UnsupportedLookup — the args.length == 1 check in the guard
distinguishes hash-conditions form from the SQL-string form, not
hash arity.
2026-04-20 17:24:08 +03:00
Kabiru Mwenja 79d9ff78d3 Add exists? visibility scoping tests
The shared scope for semantic identifier lookups needs to compose
cleanly with the visible scope (uses join+allowed_to) for both
current identifiers and historical aliases. Pin that behavior so
structural regressions in the OR construction surface early.
2026-04-20 14:16:24 +03:00
Kabiru Mwenja c64254f316 Guard find_by array values against semantic identifiers
find_by(id: [...]) is a legitimate AR pattern for matching the first
record in a set. Previously we only guarded scalar values, so
find_by(id: ["PROJ-1", 1]) silently fell through to a SQL
WHERE id IN ('PROJ-1', 1) that would error or coerce unexpectedly
on Postgres.

Extend the guard to scan array elements and raise UnsupportedLookup
on the first semantic one found, keeping the error message focused
on the offending value.
2026-04-20 14:15:55 +03:00
Kabiru Mwenja 5aa3a70ddc Introduce UnsupportedLookup for find_by semantic-id guard
Use a dedicated error class so callers that need to rescue this
specifically can do so without catching unrelated ArgumentErrors.
It still inherits from ArgumentError, so existing rescue clauses
continue to work.
2026-04-20 14:14:24 +03:00
Kabiru Mwenja 01fd71b1f0 Point multi-arg find error to primary-key lookup
The convention is that low-level code (queries, filters, services)
uses primary keys and only frontend-facing code uses display-id
resolution. The previous message only pointed at find_by_display_id,
which encouraged the wrong tool for batch lookups.

Also mention both the raising and nil-returning variants so callers
can pick the right semantics.
2026-04-20 14:13:23 +03:00
Kabiru Mwenja 08fe973a24 Allow multi-argument find for numeric IDs, reject for semantic IDs
Internal callers like FilterForWpMixin#value_objects pass arrays of
numeric IDs to find. Blanket-rejecting array/multi-arg calls broke
those paths.

Fall through to standard AR find for all-numeric arrays and multi-arg
calls, but continue raising ArgumentError if any argument is a
semantic identifier — batch resolution across the alias table would
be ambiguous and is out of scope.
2026-04-20 13:27:12 +03:00
Kabiru Mwenja df9535bdcf Disallow multi-argument find and improve find_by guard readability
WorkPackage.find(['DP-1']) returned a single record instead of an
array, breaking Rails' convention that array-in means array-out.
Rather than fixing that edge case, simplify by disallowing all
multi-argument and array-argument find calls — nobody uses them
and the complexity isn't worth maintaining.

Also extract the hash.assoc chain in reject_semantic_id_in_find_by!
into an explicit pair variable for clearer two-step logic.
2026-04-17 19:54:30 +03:00
Kabiru Mwenja 7824f5908c Handle string-keyed hashes in find_by semantic ID guard
AR sometimes passes string keys ("id") instead of symbol keys (:id)
to find_by. Check both forms when detecting semantic identifiers.
2026-04-17 19:05:36 +03:00
Kabiru Mwenja 462a0f1c78 Replace transparent find_by resolution with ArgumentError guard
find_by(id:) and find_by!(id:) now raise ArgumentError when passed
a semantic identifier string, directing developers to use find() or
the new find_by_display_id() method instead. This avoids silently
altering ActiveRecord's find_by semantics and ensures misuse is
caught in development even when semantic mode is not enabled locally.

Renames find_by_id_or_identifier to find_by_display_id (public API)
and migrates all app callers that receive user-facing strings to use
the new method.
2026-04-17 18:17:15 +03:00
Kabiru Mwenja 61111f02b8 Fix display_id to fall back to numeric id when identifier is nil
In semantic mode, work packages created before the feature was enabled
have a nil identifier column. Previously display_id returned nil for
these, causing the Ruby representer to serialize displayId as null
while the SQL representer used COALESCE to return the numeric id.

Use identifier.presence to fall back to the numeric id, aligning both
representer paths.
2026-04-15 18:59:44 +03:00
Kabiru Mwenja 8d13c2ac93 Fix grammar in multi-argument error and clarify API route param
- "find are not yet supported" → "find is not yet supported"
- Add semantic identifier example to API route param description
2026-04-15 16:42:18 +03:00
Kabiru Mwenja db88a2da7e Guard multi-argument find with ArgumentError for semantic IDs
Rather than implementing full multi-argument semantic find support
(which adds significant complexity for a use case with no current
callers), raise ArgumentError with a clear message when semantic
identifiers are passed in multi-arg find calls.

This makes the limitation explicit and discoverable. Full multi-arg
support is being evaluated separately.
2026-04-15 15:54:49 +03:00
Kabiru Mwenja a898486c3a Override find_by/find_by! for semantic ID resolution
Add find_by and find_by! overrides to FinderMethods so that
find_by(id: "PROJ-42") transparently resolves semantic identifiers,
matching the existing behavior of find and exists?.

Only intercepts calls where id: is the sole keyword — all other
find_by usage (e.g. find_by(subject: ...)) passes through unchanged.

Revert ShowComponent, SplitViewComponent, and WorkPackagesController
from the explicit find_by_id_or_identifier back to standard
find_by(id:) now that the override handles semantic resolution.

Make find_by_id_or_identifier and find_by_id_or_identifier! private
since they are now internal implementation details with no external
callers. Move their test coverage into the find_by/find_by! specs.
2026-04-15 15:54:48 +03:00
Kabiru Mwenja 75ae132201 Replace friendly_id? with semantic_id? for identifier dispatch
Replace FriendlyId's Object#friendly_id? monkey-patch with a private
semantic_id? method that owns the full dispatch decision: type check,
whitespace stripping, and numeric detection in one place.

This fixes a bug where numeric strings with whitespace (e.g. " 456 "
from comma-split input) were misclassified as semantic identifiers
because " 456".to_i.to_s != " 456".
2026-04-14 17:38:13 +03:00
Kabiru Mwenja 63e9854836 Make find/exists? resolve semantic work package identifiers
Extract FinderMethods module that transparently resolves both numeric and
semantic identifiers (e.g. "PROJ-42") using FriendlyId's Object#friendly_id?
for dispatch. The module is included in both the WorkPackage class and
extended onto every relation, so scoped queries like
WorkPackage.visible(user).find("PROJ-42") work seamlessly.

- Override find to resolve semantic IDs via identifier column + alias table
- Override exists? with the same resolution chain
- Refactor find_by_id_or_identifier to use friendly_id? instead of semantic_id?
- Update API route to accept string IDs (type: Integer → type: String)
- Update controller and ViewComponent finders to use find_by_id_or_identifier
- Pass display_id from Rails views to Angular custom elements
2026-04-14 17:38:12 +03:00
Kabiru Mwenja 5bbc4e7563 Rename semanticId to displayId, make always present
Replace the conditional `semanticId` API field with `displayId` which is
always present in work package responses. In semantic mode it returns the
project-based identifier (e.g. "PROJ-42"), in classic mode it returns the
numeric ID as a string. This gives API consumers (frontend, mobile) a
single field to read without conditional logic.

- Add `WorkPackage#display_id` method that encapsulates the mode check
- Update both representers (JSON and SQL) to render `displayId` unconditionally
- Update OpenAPI schema documentation
2026-04-13 14:04:39 +03:00
Tomas Hykel c4debc8aaa [#73523] Implement WorkPackage semantic ID allocation system 2026-04-01 17:25:19 +02:00
ulferts 1240b066c3 work package creation and update including backlogs properties 2026-03-11 13:24:32 +01:00
Ivan Kuchin 1b73390da1 user parameters instead of lets for shared acts_as_customizable test parametrization 2026-03-09 17:05:36 +01:00
Ivan Kuchin 683f4b3c25 add test of admin_only_custom_fields_allowed? to shared acts as customizable examples 2026-03-09 17:05:35 +01:00
Ivan Kuchin bfae781b13 add test of can_have_custom_comments? to shared acts as customizable examples 2026-03-09 17:05:35 +01:00
Tobias Dillmann eeff279e41 [#71248] Add draft for sharing Sprints 2026-02-11 17:15:10 +01:00
Tobias Dillmann dd8f5e8d6a [#71248] Basic Sprint specs 2026-02-11 17:15:10 +01:00
Kabiru Mwenja 17c7dcfff5 Fix internal comments authorization [OP#70979]
The API v3 was allowing reading/writing internal comments when both the
enterprise token doesn't allow it and when the project setting is disabled.

Reading (GET):
- Added EnterpriseToken check to internal_visible scope

Writing (POST):
- Added project.enabled_internal_comments check to CreateNoteContract
2026-01-28 21:04:59 +03:00