Skip to content

[SE-642] Markdown rendering of work packages - #26113

Open
myabc wants to merge 10 commits into
devfrom
feature/se-642-wp-markdown-rendering
Open

myabc wants to merge 10 commits into
devfrom
feature/se-642-wp-markdown-rendering

Conversation

@myabc

@myabc myabc commented Oct 16, 2026 •

Copy link
Copy Markdown
Contributor

Ticket

https://community.openproject.org/wp/SE-642

What are you trying to accomplish?

Bots reading a work package get Angular-rendered HTML or HAL JSON to resolve link by link. This PR adds a text/markdown document shaped like the page.

  • GET /work_packages/:id.md (also project-scoped, /wp/:id.md, and Accept: text/markdown).
  • Form Configuration groups in order, embedded queries as tables, long-text fields and the description as raw Markdown.
  • The HTML page links it with <link rel="alternate" type="text/markdown">, the API v3 work package exposes it as _links.markdown (numeric href, like the other auxiliary links), and the work package menu gets a "View Markdown" entry next to "Generate PDF" that opens /wp/<display id>.md.
# Feature SE-642 - Markdown rendering for work packages

Project: OpenProject
Status: New
URL: https://community.openproject.org/work_packages/SE-642

## Details

| Attribute | Value |
| --- | --- |
| Type | Feature |
| Date | 2026-10-01 - 2026-11-01 |

## Children

| ID | Subject | Status |
| --- | --- | --- |
| SE-650 | Add route | New |

## Description

Markdown has become the language of AI …

Screenshots

Menu Rendered (with browser plugin) 401
Screenshot 2026-10-16 at 20 40 45 Screenshot 2026-10-16 at 20 45 52 Screenshot 2026-10-16 at 20 44 54

What approach did you choose and why?

Note

The endpoint accepts both a browser session and an API key on purpose: people open the Markdown version in the browser, bots fetch it with X-OpenProject-API-Key. One URL serves both consumers.

  • Extracts the Form Configuration walk from the single work package PDF export into WorkPackage::Exports::FormConfiguration; PDF and the new WorkPackage::Exports::Markdown exporter render the same structure.
  • The walker now applies the page's project-level attribute constraints, a deliberate PDF change in its own commit: attributes of disabled modules and all-hidden groups disappear from the PDF too.
  • A new parent visibility check hides parents the user cannot see, in Markdown and PDF.
  • Plain values: ISO dates, Yes/No, display ids, bare URLs; hours, percentages and costs reuse the PDF unit formatters. Labels are translated. Query cells are blanked per row via allowed_to_view_attribute?, since Query#results filters rows, not attributes.
  • Bots authenticate with X-OpenProject-API-Key. api_request? is untouched; show accepts a key only for the negotiated text/markdown format. find_current_user tries the key before the session cookie; keyed requests skip the session TTL and never start or refresh a session. Header SSO still requires its mandatory header, but never logs in once the key authenticated.
  • Unauthenticated md requests answer 401 with WWW-Authenticate and a one-line Markdown hint instead of a blank page. For the auth owners: that 401 branch and the key-before-session precedence are global, though only the two accept_api_key_auth actions resolve a user from a key. ?key= works, undocumented, now redacted from logs.
  • Macro output in descriptions keeps the PDF formatter's shape for now.

Potential follow-ups (to be tracked separately)

  • Activities and comments in the document. need a @opf/communicator team opinion!
  • Building the document from a Markdown AST (Markly or Commonmarker nodes) once the structure grows. Building strings works well enough for the time-being.
  • Pre-existing PDF export gap: its related-work-package and list tables apply no per-attribute visibility. Potential bug – consult with @as-op
  • Pre-existing PDF export gap: raw request params are passed as exporter options. Potential bug – consult with @as-op

AI involvement

Collaborative – AI generated a substantial part of the code; I reviewed and understand every line.

Merge checklist

  • Added/updated tests
  • Added/updated documentation (docs/api/README.md)
  • Manual check of /wp/SE-642.md in Chrome, Firefox and Safari (browsers differ on inline text/markdown)

@myabc myabc changed the title Feature/se 642 wp markdown rendering [SE-642] Markdown rendering of work packages Oct 16, 2026
Comment thread app/models/work_package/exports/markdown.rb Fixed
myabc added 2 commits October 16, 2026 18:53
Moves the walk over a work package's Form Configuration out of the
single-WP PDF export into WorkPackage::Exports::FormConfiguration,
which returns format-agnostic attribute and query groups. The PDF
export and the upcoming Markdown export share it.

PDF output stays the same, except that an attribute group whose
attributes are all hidden from the user is no longer rendered and no
longer listed in the artefact table of contents.

https://community.openproject.org/wp/SE-642
The work package page hides attributes whose module is disabled in
the project, such as Story Points without the Backlogs module. The
single work package PDF export ignored these constraints and printed
them regardless.

The Form Configuration walker now iterates the group's active
members for the work package's project and resolves custom fields
through the work package's available custom fields, so both exports
share the same rule. Groups whose attributes are all constrained away
are dropped, while groups configured without attributes keep printing
their title.

https://community.openproject.org/wp/SE-642
@myabc
myabc force-pushed the feature/se-642-wp-markdown-rendering branch from 673736e to 6a5eec7 Compare October 16, 2026 17:59
@myabc
myabc force-pushed the feature/se-642-wp-markdown-rendering branch from 6a5eec7 to af92ada Compare October 16, 2026 19:04
list WorkPackage, WorkPackage::PDFExport::WorkPackageListToPdf

single WorkPackage, WorkPackage::PDFExport::WorkPackageToPdf
single WorkPackage, WorkPackage::Exports::Markdown

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

naming? – not currently symmetrical.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatives considered:

  1. WorkPackage::Exports::Markdown (matches CSV; one-file exporter stays in the shared namespace). This is what I went with.
  2. WorkPackage::Exports::WorkPackageToMarkdown, mirroring the PDF class name only.
  3. WorkPackage::MarkdownExport::WorkPackageToMarkdown, mirroring the PDF layout fully; a namespace for one class.

this might be a question for you, @as-op

@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./spec/features/admin/departments_spec.rb[1:2:3]
🤖 Ask Copilot to investigate

Copy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer.

@copilot The following spec(s) are flaky in CI (first seen on PR #26113, linked for reference only):

- `rspec ./spec/features/admin/departments_spec.rb[1:2:3]`

Treat this as a standalone task, unrelated to PR #26113. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #26113 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @myabc to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @myabc, and request a review from @myabc.
On every commit, set @myabc as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

@github-actions github-actions Bot added the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Oct 16, 2026
@myabc
myabc marked this pull request as ready for review October 16, 2026 19:18
Rails.application.config.filter_parameters += %i[
passw mail secret token _key crypt salt certificate otp ssn cvv cvc
]
Rails.application.config.filter_parameters += [/\Akey\z/]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check!

@myabc
myabc force-pushed the feature/se-642-wp-markdown-rendering branch from af92ada to 67976ad Compare October 16, 2026 19:28
myabc added 5 commits October 16, 2026 20:31
Renders a work package as text/markdown at GET /work_packages/:id.md,
also reachable with Accept: text/markdown and the /wp/:id.md short
URL. The document follows the type's form configuration: a title
heading, a Project/Status/URL header block, one section per attribute
group and the description last. Attribute groups render as two-column
tables, long-text custom fields as subheadings with their raw
Markdown, and embedded queries as tables of the related work packages
using all of the query's columns, including a group-by column. Each
query cell is blanked when the user may not view that attribute on
the related work package. Attribute macros are expanded in long-text
fields and the description. Structural labels and headings come from
the attribute and label translations.

Values come from :md formatters: ISO dates and times (a lone start or
due date keeps its position), Yes/No booleans, display ids and bare
link URLs; hours, percentages and costs reuse the PDF unit formatters.
Table cells escape backslashes and pipes and collapse CR, LF and CRLF.

The controller serves the document inline with a filename
disposition, the /wp redirect keeps an alphanumeric format suffix and
drops any other, and the HTML work package page advertises the
Markdown version with a link rel="alternate" tag.

https://community.openproject.org/wp/SE-642
Registers a visibility check for the parent attribute so exports only
print a parent the current user may view. Without it, a query table
row, a form group or a workPackageValue:parent macro printed the
subject of a parent that lives in a project the user cannot see, in
both the Markdown and the PDF export.

Grants the PDF spec user access to the parent's project, since that
spec asserted the leaked parent.

https://community.openproject.org/wp/SE-642
Lets bots fetch GET /work_packages/:id.md (or Accept: text/markdown)
with an API key in the key param or the X-OpenProject-API-Key header.

Adds an api_key_auth_format? predicate that gates current_api_key_user
(api_request? by default; WorkPackagesController overrides it to the md
format only, so a key never authenticates an HTML response chosen by
content negotiation) and accept_api_key_auth :show on
WorkPackagesController. find_current_user now tries the API key before
the session, autologin and RSS key, so a valid key wins over a stale
browser cookie. Requests authenticated by key are exempt from the
session TTL check and never refresh session activity, so they neither
start nor extend a session, while browser sessions on .md keep normal
expiry. With header-based SSO, a mandatory header is still required, but
once the key authenticated the request the header is not used to log in:
no session is written and a header naming another user is ignored in
favour of the key. A key-authenticated request never takes the SSO
in-progress shortcut, so a retained failure cookie cannot bypass a
mandatory header. Unauthenticated md requests answer 401 with
WWW-Authenticate and a one-line Markdown hint.

api_request? is unchanged. Removes the duplicate session_expired? in
ApplicationController that shadowed the SessionLifetime concern. The
API-key flag is set only for the user find_current_user accepts, so
the session-expiry exemption cannot outlive a rejected key user. The
key request parameter is redacted from logs.

https://community.openproject.org/wp/SE-642
Instantiates the Markdown exporter with the work package only. Passing
the request params as exporter options let a current_user query
parameter replace the exporter's user with a string, which crashed
macro expansion.

Restores the attribute visibility examples that the parent check
commit dropped from the attributes spec and adds the parent examples
next to them.

https://community.openproject.org/wp/SE-642
Adds a section to the API overview describing the text/markdown
rendering of a work package. It covers the URL forms, the document
structure, visibility rules, authentication with a session or the
X-OpenProject-API-Key header, and the alternate link advertised on
the HTML page.

https://community.openproject.org/wp/SE-642
@myabc
myabc force-pushed the feature/se-642-wp-markdown-rendering branch from 67976ad to 21554d4 Compare October 16, 2026 19:33
@myabc
myabc requested a review from judithroth October 16, 2026 19:35
@github-actions github-actions Bot removed the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Oct 16, 2026
myabc added 2 commits October 16, 2026 20:56
Lets API clients discover the Markdown document without knowing the
URL convention. The link needs only view_work_packages, so it carries
no export gate, and its href stays numeric like the auxiliary links.

https://community.openproject.org/wp/SE-642
Lets users open the Markdown document from the menu that already offers
PDF and Atom. Both menu resolvers build the URL from the display id so
semantic instances land on /work_packages/<id>.md.

https://community.openproject.org/wp/SE-642
@github-actions github-actions Bot added the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Oct 16, 2026
Cuprite's send_keys left-clicks the receiving element before typing
unless it already holds the selection. Sending Escape to body therefore
clicked the viewport centre while the context menu was open. The new
View Markdown entry moved the Atom item onto that point, so the BIM
card view spec navigated to the Atom feed and lost its cards.

Sends Escape through the driver's keyboard instead, on Cuprite and on
Selenium.

https://community.openproject.org/wp/SE-642
@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Deploying openproject with ⚡ PullPreview

Field Value
Latest commit 5affec8
Job deploy
Status ✅ Deploy successful
Preview URL https://pr-26113-se-642-wp-markdo-ip-188-245-56-209.my.opf.run:443

View logs

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. feature needs review Passion time pullpreview

Development

Successfully merging this pull request may close these issues.

2 participants