276 Commits

Author SHA1 Message Date
Kabiru Mwenja aa12a0a1ce [rubocop] Layout/HeredocIndentation: Use 2 spaces for indentation in a heredoc (#23296)
Rubocop fixes

1. Layout/HeredocIndentation: Use 2 spaces for indentation in a heredoc.
[Layout/HeredocIndentation]
2. Use 2 spaces for indentation in a heredoc.
[Layout/HeredocIndentation]
2026-05-21 00:20:41 +03:00
Kabiru Mwenja e597aa4216 Fix NameError: uninitialized constant Projects::Identifier::CLASSIC_IDENTIFIER_FORMAT (#23293) 2026-05-20 22:02:41 +03:00
Tomas Hykel 1ce03faa03 feat: Improve progress reporting during identifier conversion 2026-05-20 20:27:43 +02:00
Tomas Hykel 506198d55d chore: Consolidate classic project ID generation 2026-05-18 14:15:20 +02:00
Kabiru Mwenja fd9a5bea77 Merge pull request #23197 from opf/refactor/extract-semantic-project-identifier-format
Extract semantic project identifier regex into a composed constant
2026-05-13 17:49:56 +03:00
Kabiru Mwenja 8eeefa6e76 Merge pull request #22982 from opf/refactor/extract-projects-identifier-validator 2026-05-06 11:40:46 +03:00
Kabiru Mwenja 8c71858d33 Remove redundant comment so as not to give the wrong impression
There already exists a test that would capture the regression, hence no
need for the comment.

Ref: https://github.com/opf/openproject/pull/22982/changes#r3194055478
2026-05-06 11:39:16 +03:00
Kabiru Mwenja 41c2c09f9e Return wp_id => identifier assignments from reserve_semantic_id_block!
The bulk SQL UPDATE persists identifier and sequence_number directly,
but callers holding live records have no cheap way to learn what was
written without reloading. Returning the {wp_id => "PROJ-N"} mapping
lets callers (next commit: WorkPackages::UpdateService) refresh
in-memory state without N round trips. Empty input returns an empty
hash for symmetric Hash-typed callers.
2026-04-30 17:44:27 +03:00
Kabiru Mwenja 47fe076905 Move validator into Projects namespace
- Validator moves to app/validators/projects/identifier_validator.rb
  as Projects::IdentifierValidator. The path-style symbol on the
  validates declaration ('projects/identifier') resolves to it.
  Distinguishes domain-specific validators from globally reusable
  ones (UrlValidator, JsonValidator, etc).

- Spec moves to spec/validators/projects/identifier_validator_spec.rb.

- Trim comments that explained framework conventions (validator
  lookup, declaration order). Keep comments only where the WHY is
  non-obvious to a Rails-fluent reader.
2026-04-29 11:30:50 +03:00
Kabiru Mwenja 97a266273c Extract project identifier validation into dedicated validator
Move five private validation methods out of the Projects::Identifier
concern into a top-level ActiveModel::EachValidator. The concern goes
from carrying ~50 lines of validation logic to declaring:

    validates :identifier, project_identifier: true,
                           if: :identifier_changed?

Format rules, reserved-keyword check, and historical-reservation check
all live in ProjectIdentifierValidator. Mode dispatch (classic vs
semantic vs :semantic_conversion context) is internal to the validator
and easy to swap for a registry if a third format ever appears.

Picks up the follow-up suggested in #22931 (comment 4326208630).

Notes:

- Validator is top-level (ProjectIdentifierValidator) not namespaced,
  matching the existing convention of UrlValidator / JsonValidator /
  SecureContextUriValidator and so Rails' validator lookup for
  `validates :identifier, project_identifier: true` resolves directly.

- Format constants (RESERVED_IDENTIFIERS, *_MAX_LENGTH) stay on the
  concern since they're shared with acts_as_url's blacklist/limit, the
  routing constraint, and suggesters — moving them to the validator
  would couple unrelated subsystems to a validator namespace.

- Validator declaration order in the concern is significant: presence
  + uniqueness must run before `project_identifier: true` so the
  historical-reservation check can short-circuit when uniqueness has
  already flagged :taken.

Pure-validator behaviour (format checks, reserved keyword,
:semantic_conversion context, historical reservation) moves to
spec/validators/project_identifier_validator_spec.rb. Integration
tests (FriendlyId :history wiring, :saving_custom_fields,
.suggest_identifier, .identifier_slugs scopes) stay in the model spec.
2026-04-29 08:54:30 +03:00
Kabiru Mwenja defd525e04 Rename slug history scopes for clarity
Address review feedback: `historical_slugs` was misleading because
FriendlyId's :history module records a row on every save, so the
relation contains current identifiers as well as old ones. The
follow-up `truly_historical` filter read as a tautology, which was
the tell-tale sign of the misnaming.

Renames:
- `historical_slugs` -> `identifier_slugs`
- `HistoricalSlugScopes` -> `IdentifierSlugScopes`
- `truly_historical` -> `historically_reserved`
- `matching(value)` -> `for_identifier(value)`

`for_identifier` is preferred over `reserving` / `with_reservation_of`
because the slug record itself doesn't reserve anything; the slug
value is what blocks reuse, and `for_identifier` plainly names the
case-insensitive lookup without importing a "reservation" concept
that isn't otherwise modelled.
2026-04-29 07:56:21 +03:00
Kabiru Mwenja edce0f9025 revert "Project.historical_slugs" rename for FriendlyId consistency
FriendlyId's `:history` module already adds a `project.slugs` association.
Splitting the class-level name to `historical_identifiers` while the
instance-level stays `slugs` creates a vocabulary inconsistency that's
worse than the original "slugs vs identifiers" friction.

The reviewer's concern about newcomers confusing the two is addressed
with an inline comment near the method, not by partially renaming.

The scope module is renamed back from `HistoricalIdentifierScopes` to
`HistoricalSlugScopes` for the same consistency reason. The
`ProblematicIdentifiers#historical_identifiers` instance method
(unrelated, pre-existing) is unchanged.
2026-04-28 08:26:48 +03:00
Kabiru Mwenja a25bbb8cac add excluding_project scope
Replaces the conditional `.then { |q| project ? q.where.not(...) : q }`
in `ClassicIdentifierSuggestionGenerator#taken_identifiers` with a
named scope. The scope handles the nil case so the call site stays
unconditional and reads as the verb it is: "historical identifiers,
excluding this project's own history."
2026-04-28 08:26:47 +03:00
Kabiru Mwenja 3b237c10a5 add raw_values to HistoricalIdentifierScopes
Completes the symmetric trio: upcased_values / downcased_values / raw_values.
Named `raw_values` rather than `values` because the latter collides with
`ActiveRecord::Relation#values` (an internal method Rails depends on).
2026-04-28 08:26:47 +03:00
Kabiru Mwenja a46385325c rename Project.historical_slugs to Project.historical_identifiers
`slug` is FriendlyId terminology; `identifier` is the project domain
term. Reviewers unfamiliar with FriendlyId may not connect the two.
Renaming at the call site removes that translation step.

Updated four call sites and the corresponding spec.
2026-04-28 08:26:46 +03:00
Kabiru Mwenja 4a86bd7274 refactor: encapsulate Project slug history with relation scopes
Add `Projects::Identifier::HistoricalIdentifierScopes`, extended onto the
relation returned by `Project.historical_slugs`. Replace four inlined
SQL fragments with domain-named scopes:

* `truly_historical` — slugs no longer used as any active project's identifier
* `matching(value)` — case-insensitive equality
* `upcased_values` / `downcased_values` — SQL-side case-folded value lists

Call sites updated:

* `Projects::Identifier#identifier_used_by_other_project_in_past?`
* `ProblematicIdentifiers.reserved_identifiers` and `#historical_identifiers`
* `ClassicIdentifierSuggestionGenerator#taken_identifiers`

After this change, `FriendlyId::Slug` is mentioned in exactly one line —
the body of `Project.historical_slugs`.

Note: `#taken_identifiers` shifts from Ruby-side `String#downcase` to
SQL-side `LOWER()`. For ASCII-only project identifiers the two are
equivalent.
2026-04-28 08:26:45 +03:00
Tomas Hykel 269b18198c refactor: Add Project.historical_slugs 2026-04-27 20:01:56 +02:00
Tomas Hykel 89d3a2c3f0 fix: Correctly avoid problematic identifiers in semantic identifier generation 2026-04-27 16:01:52 +02:00
Tomas Hykel 6658f431a7 fix: Properly validate saved projects during semantic conversion 2026-04-27 15:35:49 +02:00
Tomas Hykel 0001feb9a8 refactor: Improve readability of project identifier validators 2026-04-27 15:22:18 +02:00
Tom Hykel fb31d1da98 Merge pull request #22857 from opf/fix/71645-robust-classic-id-handling
Improve classic project identifier generation
2026-04-23 10:27:50 +02:00
as-op 7f69fab503 allow admins to export the projects they are seeing, also archived ones 2026-04-22 10:32:34 +02:00
as-op 6d90712287 [#73841] projects filter does not work with project list export
https://community.openproject.org/wp/73841
2026-04-22 10:16:25 +02:00
Tomas Hykel b00c6c877b revert the rename 2026-04-22 00:25:51 +02:00
Tomas Hykel 0180bcd18f [#71645] Improve classic project identifier generation 2026-04-22 00:16:22 +02:00
Tomas Hykel 9b38c082ab [#73613] Improve peformance of bulk semantic ID allocation 2026-04-21 22:26:25 +02:00
Tomas Hykel 600499f4e1 [#71645] Revert instance to classic identifiers 2026-04-21 21:09:22 +02:00
Tomas Hykel 56f130d9f2 [#71645] Convert instance to semantic identifiers 2026-04-21 19:34:37 +02:00
Tomas Hykel 3a7fad89df [#73711] refactor: Rename the project identifier namespace 2026-04-16 21:40:38 +02: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 a14e1de2d5 appease rubocop 2026-04-09 16:17:42 +02:00
Tomas Hykel 2337a25b14 fix: Correctly auto-generate semantic identifiers on project copy 2026-04-09 16:03:17 +02:00
Alexander Brandon Coles af06b51b6b Merge branch 'dev' into merge-release/17.3-20260402133137 2026-04-02 15:46:07 +02:00
Pavel Balashou 7433851ab2 [#72809] Provide more details when project with identifier exists in OpenProject
https://community.openproject.org/work_packages/72809

- Extract the error message to I18n.
- Change the message to have only one option to resolve the issue(Change identifier in Jira).
- In case it is a historic identifier then provide this value in the message.
2026-04-02 13:05:44 +02:00
Tomas Hykel c4debc8aaa [#73523] Implement WorkPackage semantic ID allocation system 2026-04-01 17:25:19 +02:00
OpenProject Actions CI 6559ec3342 Merge branch 'release/17.3' into dev 2026-04-01 09:41:14 +00:00
Kabiru Mwenja 5e3dd6a1d1 fix(documents): strip invisible characters from document titles
Documents created with zero-width Unicode characters (e.g. U+200B)
in their titles become unclickable on the index page, making them
hard to manage or delete.

Introduce RemoveInvisibleCharacters normalizer, replacing the former
RemoveAsciiControlCharacters. It strips both ASCII control characters
and Unicode zero-width characters, with each category defined as a
named constant for clarity. Apply it to Document#title and update
existing callers (Project#identifier, CustomField#name).

Add a shared RSpec example "strips invisible characters" to verify
normalization consistently across all three models.
2026-03-31 18:18:41 +03:00
Oliver Günther e709bed089 Use active_admin? instead of admin? 2026-03-31 10:24:04 +02:00
Kabiru Mwenja d3734a7485 Rename work_packages_identifier setting values to "classic" and "semantic"
The setting previously used "numeric" and "alphanumeric" as its allowed
values. Rename them to "classic" and "semantic" to better align with the
product terminology for the work package identifier modes.

Includes a migration to update any stored setting values in the database,
updated constants and helper methods on Setting::WorkPackageIdentifier,
and all corresponding references across models, components, forms,
frontend controllers, locales, and specs.
2026-03-28 10:01:57 +03:00
Kabiru Mwenja 6fc8a68ca0 Collect all alphanumeric identifier errors instead of returning early
Remove the early return in identifier_alphanumeric_format so users see
all validation errors at once (e.g. "123!" reports both
must_start_with_letter and no_special_characters). Simplify the special
characters regex since the leading-letter check is already separate.

Update API specs to use test identifiers that trigger only one
validation error, matching the new non-short-circuiting behavior.
2026-03-26 18:01:12 +03:00
Kabiru Mwenja d801728b41 Extract identifier_numeric_format for symmetry with alphanumeric validator
The numeric (legacy) format validation was inline as a `validates :format`
declaration, while the alphanumeric validator was already a dedicated method.
Extract it into `identifier_numeric_format` so both mode-specific validators
follow the same pattern, and fix a misleading comment on the shared validators.
2026-03-25 12:30:52 +03:00
Kabiru Mwenja 1648e5331c Merge branch 'dev' into open-point/73149-how-do-we-handle-project-identifiers-case-insensitive-storage 2026-03-25 12:20:38 +03:00
Tomas Hykel 52bc6a6977 implement skip_if logic for OpActiveRecord 2026-03-24 12:48:23 +01:00
Kabiru Mwenja 2318522c17 Revert acts_as_url overrides (force_downcase, post_process)
These were built on the normalizes premise which has been removed.
In alphanumeric mode, SuggestionGenerator handles identifier generation
rather than acts_as_url. The API PR (22417) will handle the proper
wiring between modes.
2026-03-23 17:22:43 +03:00
Kabiru Mwenja bfeee22232 Remove case-transforming normalizes and parse_friendly_id override
Per code review: the model should not auto-transform identifier casing
on every read/write. Format validators already enforce correct casing
(lowercase for numeric, uppercase for alphanumeric), and the background
job will handle migrating existing identifiers.

Restores LOWER() in identifier_not_historically_reserved since without
normalizes, slugs and identifiers may differ in case.
2026-03-23 17:22:42 +03:00
Kabiru Mwenja 87750c50d2 Revert slug query to plain equality in identifier_not_historically_reserved
LOWER(slug) = LOWER(?) is unnecessary because normalizes :identifier
ensures consistent casing before FriendlyId records the slug. Plain
equality uses the existing composite index on (slug, sluggable_type).
2026-03-23 17:22:41 +03:00
Tomas Hykel 04717ce615 address PR feedback 2026-03-23 15:01:23 +01:00
Tomas Hykel a99c620219 readd a comment 2026-03-20 19:01:44 +01:00
Kabiru Mwenja e369227c39 Add case-insensitive project identifier storage
Introduce case-insensitive handling for project identifiers:

- Add `normalizes :identifier` to automatically upcase (alphanumeric
  mode) or downcase (numeric mode) identifiers on assignment
- Add `parse_friendly_id` to normalize FriendlyId lookups for
  case-insensitive finder queries
- Switch uniqueness validation to `case_sensitive: false`
- Replace inline `exclusion:` validator with explicit
  `identifier_not_reserved` that checks case-insensitively
- Consolidate alphanumeric format validators into a single
  `identifier_alphanumeric_format` method with early return to
  prevent cascading error messages
- Use case-insensitive LOWER() comparison in historical identifier
  reservation check
- Add `post_process` support to the OpActiveRecord acts_as_url
  adapter with an allowlist of safe transforms (upcase/downcase)
- Add migration to replace the unique index on projects.identifier
  with a case-insensitive LOWER(identifier) index
- Update table definition to match the new index

Includes corresponding test additions for normalization, case-
insensitive uniqueness, reserved identifier rejection, and the
create_spec fixture fix for alphanumeric mode.
2026-03-20 20:27:04 +03:00