diff --git a/static/images/integrations/bot_avatars/openproject.png b/static/images/integrations/bot_avatars/openproject.png
new file mode 100644
index 0000000000..a13d2b1c8d
Binary files /dev/null and b/static/images/integrations/bot_avatars/openproject.png differ
diff --git a/static/images/integrations/logos/openproject.svg b/static/images/integrations/logos/openproject.svg
new file mode 100644
index 0000000000..5ff601a0ce
--- /dev/null
+++ b/static/images/integrations/logos/openproject.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/static/images/integrations/openproject/001.png b/static/images/integrations/openproject/001.png
new file mode 100644
index 0000000000..70d6bd70c4
Binary files /dev/null and b/static/images/integrations/openproject/001.png differ
diff --git a/zerver/lib/integrations.py b/zerver/lib/integrations.py
index 2684039a28..a90a75897b 100644
--- a/zerver/lib/integrations.py
+++ b/zerver/lib/integrations.py
@@ -528,6 +528,7 @@ WEBHOOK_INTEGRATIONS: list[WebhookIntegration] = [
WebhookIntegration("netlify", ["continuous-integration", "deployment"]),
WebhookIntegration("newrelic", ["monitoring"], display_name="New Relic"),
WebhookIntegration("opencollective", ["financial"], display_name="Open Collective"),
+ WebhookIntegration("openproject", ["project-management"], display_name="OpenProject"),
WebhookIntegration("opensearch", ["monitoring"], display_name="OpenSearch"),
WebhookIntegration("opsgenie", ["meta-integration", "monitoring"]),
WebhookIntegration("pagerduty", ["monitoring"], display_name="PagerDuty"),
@@ -774,6 +775,7 @@ DOC_SCREENSHOT_CONFIG: dict[str, list[BaseScreenshotConfig]] = {
"netlify": [ScreenshotConfig("deploy_building.json")],
"newrelic": [ScreenshotConfig("incident_activated_new_default_payload.json")],
"opencollective": [ScreenshotConfig("one_time_donation.json")],
+ "openproject": [ScreenshotConfig("project_created__without_parent.json")],
"opensearch": [ScreenshotConfig("example_template.txt")],
"opsgenie": [ScreenshotConfig("addrecipient.json")],
"pagerduty": [ScreenshotConfig("trigger_v2.json")],
diff --git a/zerver/webhooks/openproject/__init__.py b/zerver/webhooks/openproject/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/zerver/webhooks/openproject/doc.md b/zerver/webhooks/openproject/doc.md
new file mode 100644
index 0000000000..8dca7601e9
--- /dev/null
+++ b/zerver/webhooks/openproject/doc.md
@@ -0,0 +1,35 @@
+# Zulip OpenProject integration
+
+Get Zulip notifications for your OpenProject work packages and projects!
+
+{start_tabs}
+
+1. {!create-an-incoming-webhook.md!}
+
+1. {!generate-webhook-url-basic.md!}
+
+1. From your OpenProject organization, click on your user profile icon.
+ Select **Administration** from the dropdown menu, and navigate to
+ **API and Webhooks**. Select the **Webhooks** tab from the left panel,
+ and click on **+ Webhook**.
+
+1. Enter a name of your choice for the webhook, such as `Zulip`. Set
+ **Payload URL** to the URL generated above, and ensure the webhook is
+ enabled.
+
+1. Select the events and projects you want to receive notifications for,
+ and click **Create**.
+
+{end_tabs}
+
+{!congrats.md!}
+
+
+
+### Related documentation
+
+* [**OpenProject webhook guide**][1]
+
+{!webhooks-url-specification.md!}
+
+[1]: https://www.openproject.org/docs/system-admin-guide/api-and-webhooks/#webhooks
diff --git a/zerver/webhooks/openproject/fixtures/attachment_created.json b/zerver/webhooks/openproject/fixtures/attachment_created.json
new file mode 100644
index 0000000000..d34293c942
--- /dev/null
+++ b/zerver/webhooks/openproject/fixtures/attachment_created.json
@@ -0,0 +1,343 @@
+{
+ "action": "attachment:created",
+ "attachment": {
+ "_type": "Attachment",
+ "id": 3,
+ "fileName": "a.out",
+ "fileSize": 20144,
+ "description": {
+ "format": "plain",
+ "raw": "",
+ "html": ""
+ },
+ "status": "uploaded",
+ "contentType": "application/x-pie-executable",
+ "digest": {
+ "algorithm": "md5",
+ "hash": "f9d1a2d6c97e33acc10b858001bc0ea1"
+ },
+ "createdAt": "2025-01-01T21:27:50.230Z",
+ "_embedded": {
+ "author": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2025-01-01T18:28:20.852Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "container": {
+ "_type": "WorkPackage",
+ "id": 39,
+ "lockVersion": 1,
+ "subject": "task2",
+ "description": {
+ "format": "markdown",
+ "raw": "x vxcv x\n\n ",
+ "html": "
x vxcv x
\n "
+ },
+ "scheduleManually": false,
+ "startDate": null,
+ "dueDate": null,
+ "derivedStartDate": null,
+ "derivedDueDate": null,
+ "estimatedTime": null,
+ "derivedEstimatedTime": null,
+ "derivedRemainingTime": null,
+ "duration": null,
+ "ignoreNonWorkingDays": false,
+ "spentTime": "PT0S",
+ "percentageDone": null,
+ "derivedPercentageDone": null,
+ "createdAt": "2025-01-01T20:44:41.255Z",
+ "updatedAt": "2025-01-01T21:27:53.926Z",
+ "readonly": false,
+ "laborCosts": "0.00 EUR",
+ "materialCosts": "0.00 EUR",
+ "overallCosts": "0.00 EUR",
+ "_links": {
+ "attachments": {
+ "href": "/api/v3/work_packages/39/attachments"
+ },
+ "prepareAttachment": {
+ "href": "/api/v3/work_packages/39/attachments/prepare",
+ "method": "post"
+ },
+ "addAttachment": {
+ "href": "/api/v3/work_packages/39/attachments",
+ "method": "post"
+ },
+ "fileLinks": {
+ "href": "/api/v3/work_packages/39/file_links"
+ },
+ "addFileLink": {
+ "href": "/api/v3/work_packages/39/file_links",
+ "method": "post"
+ },
+ "self": {
+ "href": "/api/v3/work_packages/39",
+ "title": "task2"
+ },
+ "update": {
+ "href": "/api/v3/work_packages/39/form",
+ "method": "post"
+ },
+ "schema": {
+ "href": "/api/v3/work_packages/schemas/4-1"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/work_packages/39",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/work_packages/39",
+ "method": "delete"
+ },
+ "logTime": {
+ "href": "/api/v3/time_entries",
+ "title": "Log time on work package 'task2'"
+ },
+ "move": {
+ "href": "/work_packages/39/move/new",
+ "type": "text/html",
+ "title": "Move work package 'task2'"
+ },
+ "copy": {
+ "href": "/work_packages/39/copy",
+ "type": "text/html",
+ "title": "Copy work package 'task2'"
+ },
+ "pdf": {
+ "href": "/work_packages/39.pdf",
+ "type": "application/pdf",
+ "title": "Export as PDF"
+ },
+ "atom": {
+ "href": "/work_packages/39.atom",
+ "type": "application/rss+xml",
+ "title": "Atom feed"
+ },
+ "availableRelationCandidates": {
+ "href": "/api/v3/work_packages/39/available_relation_candidates",
+ "title": "Potential work packages to relate to"
+ },
+ "customFields": {
+ "href": "/projects/project-2/settings/custom_fields",
+ "type": "text/html",
+ "title": "Custom fields"
+ },
+ "configureForm": {
+ "href": "/types/1/edit?tab=form_configuration",
+ "type": "text/html",
+ "title": "Configure form"
+ },
+ "activities": {
+ "href": "/api/v3/work_packages/39/activities"
+ },
+ "availableWatchers": {
+ "href": "/api/v3/work_packages/39/available_watchers"
+ },
+ "relations": {
+ "href": "/api/v3/work_packages/39/relations"
+ },
+ "revisions": {
+ "href": "/api/v3/work_packages/39/revisions"
+ },
+ "watchers": {
+ "href": "/api/v3/work_packages/39/watchers"
+ },
+ "addWatcher": {
+ "href": "/api/v3/work_packages/39/watchers",
+ "method": "post",
+ "payload": {
+ "user": {
+ "href": "/api/v3/users/{user_id}"
+ }
+ },
+ "templated": true
+ },
+ "removeWatcher": {
+ "href": "/api/v3/work_packages/39/watchers/{user_id}",
+ "method": "delete",
+ "templated": true
+ },
+ "addRelation": {
+ "href": "/api/v3/work_packages/39/relations",
+ "method": "post",
+ "title": "Add relation"
+ },
+ "addChild": {
+ "href": "/api/v3/projects/project-2/work_packages",
+ "method": "post",
+ "title": "Add child of task2"
+ },
+ "changeParent": {
+ "href": "/api/v3/work_packages/39",
+ "method": "patch",
+ "title": "Change parent of task2"
+ },
+ "addComment": {
+ "href": "/api/v3/work_packages/39/activities",
+ "method": "post",
+ "title": "Add comment"
+ },
+ "previewMarkup": {
+ "href": "/api/v3/render/markdown?context=/api/v3/work_packages/39",
+ "method": "post"
+ },
+ "timeEntries": {
+ "href": "/api/v3/time_entries?filters=%5B%7B%22work_package_id%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%2239%22%5D%7D%7D%5D",
+ "title": "Time entries"
+ },
+ "ancestors": [],
+ "category": {
+ "href": null
+ },
+ "type": {
+ "href": "/api/v3/types/1",
+ "title": "Task"
+ },
+ "priority": {
+ "href": "/api/v3/priorities/8",
+ "title": "Normal"
+ },
+ "project": {
+ "href": "/api/v3/projects/4",
+ "title": "Project 2"
+ },
+ "status": {
+ "href": "/api/v3/statuses/1",
+ "title": "New"
+ },
+ "author": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "responsible": {
+ "href": null
+ },
+ "assignee": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "version": {
+ "href": null
+ },
+ "parent": {
+ "href": null,
+ "title": null
+ },
+ "customActions": [],
+ "logCosts": {
+ "href": "/work_packages/39/cost_entries/new",
+ "type": "text/html",
+ "title": "Log costs on task2"
+ },
+ "showCosts": {
+ "href": "/projects/4/cost_reports?fields%5B%5D=WorkPackageId&operators%5BWorkPackageId%5D=%3D&set_filter=1&values%5BWorkPackageId%5D=39",
+ "type": "text/html",
+ "title": "Show cost entries"
+ },
+ "costsByType": {
+ "href": "/api/v3/work_packages/39/summarized_costs_by_type"
+ },
+ "meetings": {
+ "href": "/work_packages/39/tabs/meetings",
+ "title": "meetings"
+ },
+ "github": {
+ "href": "/work_packages/39/tabs/github",
+ "title": "github"
+ },
+ "github_pull_requests": {
+ "href": "/api/v3/work_packages/39/github_pull_requests",
+ "title": "GitHub pull requests"
+ },
+ "gitlab": {
+ "href": "/work_packages/39/tabs/gitlab",
+ "title": "gitlab"
+ },
+ "gitlab_merge_requests": {
+ "href": "/api/v3/work_packages/39/gitlab_merge_requests",
+ "title": "Gitlab merge requests"
+ },
+ "gitlab_issues": {
+ "href": "/api/v3/work_packages/39/gitlab_issues",
+ "title": "Gitlab Issues"
+ },
+ "convertBCF": {
+ "href": "/api/bcf/2.1/projects/project-2/topics",
+ "title": "Convert to BCF",
+ "payload": {
+ "reference_links": [
+ "/api/v3/work_packages/39"
+ ]
+ },
+ "method": "post"
+ }
+ }
+ }
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/attachments/3",
+ "title": "a.out"
+ },
+ "author": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "container": {
+ "href": "/api/v3/work_packages/39",
+ "title": "task2"
+ },
+ "staticDownloadLocation": {
+ "href": "/api/v3/attachments/3/content"
+ },
+ "downloadLocation": {
+ "href": "https://saas-openproject-aws-de-trials2-20241119125120412800000002.s3.eu-central-1.amazonaws.com/1735649550_8932652_b7d27008_3959_4391_8bd2_083b23bbd9d0/attachment/file/3/a.out?response-content-disposition=attachment&X-Amz-Expires=600&X-Amz-Date=20250101T212754Z&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAUF2KCI5YK2B2YLH6%2F20250101%2Feu-central-1%2Fs3%2Faws4_request&X-Amz-SignedHeaders=host&X-Amz-Signature=ce1ce0ece048498ed44704da6d4eab8aabbe34b38a67780fea218628b91e4e46"
+ },
+ "delete": {
+ "href": "/api/v3/attachments/3",
+ "method": "delete"
+ }
+ }
+ }
+ }
diff --git a/zerver/webhooks/openproject/fixtures/project_created__with_parent.json b/zerver/webhooks/openproject/fixtures/project_created__with_parent.json
new file mode 100644
index 0000000000..4a25ef08bd
--- /dev/null
+++ b/zerver/webhooks/openproject/fixtures/project_created__with_parent.json
@@ -0,0 +1,163 @@
+{
+ "action": "project:created",
+ "project": {
+ "_type": "Project",
+ "id": 3,
+ "identifier": "ai backend",
+ "name": "AI Backend",
+ "active": true,
+ "public": false,
+ "description": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "createdAt": "2024-12-31T18:07:24.546Z",
+ "updatedAt": "2024-12-31T18:07:24.609Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "_embedded": {
+ "parent": {
+ "_type": "Project",
+ "id": 1,
+ "identifier": "demo-project",
+ "name": "Demo project",
+ "active": true,
+ "public": true,
+ "description": {
+ "format": "markdown",
+ "raw": "This is a short summary of the goals of this demo project.",
+ "html": "This is a short summary of the goals of this demo project.
"
+ },
+ "createdAt": "2024-12-31T12:52:45.287Z",
+ "updatedAt": "2024-12-31T18:07:24.609Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "All tasks are on schedule. The people involved know their tasks. The system is completely set up.",
+ "html": "All tasks are on schedule. The people involved know their tasks. The system is completely set up.
"
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "createWorkPackage": {
+ "href": "/api/v3/projects/1/work_packages/form",
+ "method": "post"
+ },
+ "createWorkPackageImmediately": {
+ "href": "/api/v3/projects/1/work_packages",
+ "method": "post"
+ },
+ "workPackages": {
+ "href": "/api/v3/projects/1/work_packages"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/1/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/1/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/1/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/1/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/1",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/1",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": null
+ },
+ "status": {
+ "href": "/api/v3/project_statuses/on_track",
+ "title": "On track"
+ }
+ }
+ }
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/3",
+ "title": "AI Backend"
+ },
+ "createWorkPackage": {
+ "href": "/api/v3/projects/3/work_packages/form",
+ "method": "post"
+ },
+ "createWorkPackageImmediately": {
+ "href": "/api/v3/projects/3/work_packages",
+ "method": "post"
+ },
+ "workPackages": {
+ "href": "/api/v3/projects/3/work_packages"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/3/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/3/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/3/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/3/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/3",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/3",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [
+ {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ }
+ ],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "status": {
+ "href": null
+ }
+ }
+ }
+}
+
diff --git a/zerver/webhooks/openproject/fixtures/project_created__without_parent.json b/zerver/webhooks/openproject/fixtures/project_created__without_parent.json
new file mode 100644
index 0000000000..8df4cdcba1
--- /dev/null
+++ b/zerver/webhooks/openproject/fixtures/project_created__without_parent.json
@@ -0,0 +1,84 @@
+{
+ "action": "project:created",
+ "project": {
+ "_type": "Project",
+ "id": 3,
+ "identifier": "ai backend",
+ "name": "AI Backend",
+ "active": true,
+ "public": false,
+ "description": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "createdAt": "2024-12-31T18:07:24.546Z",
+ "updatedAt": "2024-12-31T18:07:24.609Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/3",
+ "title": "AI Backend"
+ },
+ "createWorkPackage": {
+ "href": "/api/v3/projects/3/work_packages/form",
+ "method": "post"
+ },
+ "createWorkPackageImmediately": {
+ "href": "/api/v3/projects/3/work_packages",
+ "method": "post"
+ },
+ "workPackages": {
+ "href": "/api/v3/projects/3/work_packages"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/3/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/3/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/3/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/3/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/3",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/3",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [
+ {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ }
+ ],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "status": {
+ "href": null
+ }
+ }
+ }
+}
diff --git a/zerver/webhooks/openproject/fixtures/project_updated.json b/zerver/webhooks/openproject/fixtures/project_updated.json
new file mode 100644
index 0000000000..7458248cd9
--- /dev/null
+++ b/zerver/webhooks/openproject/fixtures/project_updated.json
@@ -0,0 +1,162 @@
+{
+ "action": "project:updated",
+ "project": {
+ "_type": "Project",
+ "id": 3,
+ "identifier": "ai backend",
+ "name": "AI Backend",
+ "active": true,
+ "public": false,
+ "description": {
+ "format": "markdown",
+ "raw": "dgrfcxbcfbcgf",
+ "html": "dgrfcxbcfbcgf
"
+ },
+ "createdAt": "2024-12-31T18:07:24.546Z",
+ "updatedAt": "2024-12-31T18:08:11.603Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "_embedded": {
+ "parent": {
+ "_type": "Project",
+ "id": 1,
+ "identifier": "demo-project",
+ "name": "Demo project",
+ "active": true,
+ "public": true,
+ "description": {
+ "format": "markdown",
+ "raw": "This is a short summary of the goals of this demo project.",
+ "html": "This is a short summary of the goals of this demo project.
"
+ },
+ "createdAt": "2024-12-31T12:52:45.287Z",
+ "updatedAt": "2024-12-31T18:07:24.609Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "All tasks are on schedule. The people involved know their tasks. The system is completely set up.",
+ "html": "All tasks are on schedule. The people involved know their tasks. The system is completely set up.
"
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "createWorkPackage": {
+ "href": "/api/v3/projects/1/work_packages/form",
+ "method": "post"
+ },
+ "createWorkPackageImmediately": {
+ "href": "/api/v3/projects/1/work_packages",
+ "method": "post"
+ },
+ "workPackages": {
+ "href": "/api/v3/projects/1/work_packages"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/1/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/1/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/1/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/1/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/1",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/1",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": null
+ },
+ "status": {
+ "href": "/api/v3/project_statuses/on_track",
+ "title": "On track"
+ }
+ }
+ }
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/3",
+ "title": "AI Backend"
+ },
+ "createWorkPackage": {
+ "href": "/api/v3/projects/3/work_packages/form",
+ "method": "post"
+ },
+ "createWorkPackageImmediately": {
+ "href": "/api/v3/projects/3/work_packages",
+ "method": "post"
+ },
+ "workPackages": {
+ "href": "/api/v3/projects/3/work_packages"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/3/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/3/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/3/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/3/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/3",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/3",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [
+ {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ }
+ ],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "status": {
+ "href": null
+ }
+ }
+ }
+}
diff --git a/zerver/webhooks/openproject/fixtures/time_entry_created__with_invalid_iso.json b/zerver/webhooks/openproject/fixtures/time_entry_created__with_invalid_iso.json
new file mode 100644
index 0000000000..cbace75efb
--- /dev/null
+++ b/zerver/webhooks/openproject/fixtures/time_entry_created__with_invalid_iso.json
@@ -0,0 +1,445 @@
+{
+ "action": "time_entry:created",
+ "time_entry": {
+ "_type": "TimeEntry",
+ "id": 2,
+ "ongoing": false,
+ "comment": {
+ "format": "plain",
+ "raw": "",
+ "html": ""
+ },
+ "spentOn": "2025-01-02",
+ "hours": "XX",
+ "createdAt": "2025-01-01T21:20:58.333Z",
+ "updatedAt": "2025-01-01T21:20:58.333Z",
+ "_embedded": {
+ "project": {
+ "_type": "Project",
+ "id": 3,
+ "identifier": "project1",
+ "name": "Project1",
+ "active": true,
+ "public": false,
+ "description": {
+ "format": "markdown",
+ "raw": "1. nucxdswssdfdsf\\_hb\\_xcddsfjsxczxchkxvcxvyuguygyujijdgrfcxbcfxbcgfjhkhksf",
+ "html": "\nnucxdswssdfdsf_hb_xcddsfjsxczxchkxvcxvyuguygyujijdgrfcxbcfxbcgfjhkhksf \n "
+ },
+ "createdAt": "2024-12-31T18:07:24.546Z",
+ "updatedAt": "2025-01-01T20:41:55.823Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "createWorkPackage": {
+ "href": "/api/v3/projects/3/work_packages/form",
+ "method": "post"
+ },
+ "createWorkPackageImmediately": {
+ "href": "/api/v3/projects/3/work_packages",
+ "method": "post"
+ },
+ "workPackages": {
+ "href": "/api/v3/projects/3/work_packages"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/3/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/3/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/3/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/3/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/3",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/3",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [
+ {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ }
+ ],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "status": {
+ "href": null
+ }
+ }
+ },
+ "workPackage": {
+ "_type": "WorkPackage",
+ "id": 40,
+ "lockVersion": 0,
+ "subject": "kl",
+ "description": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "scheduleManually": false,
+ "startDate": null,
+ "dueDate": null,
+ "derivedStartDate": null,
+ "derivedDueDate": null,
+ "estimatedTime": null,
+ "derivedEstimatedTime": null,
+ "derivedRemainingTime": null,
+ "duration": null,
+ "ignoreNonWorkingDays": false,
+ "spentTime": "PT0S",
+ "percentageDone": null,
+ "derivedPercentageDone": null,
+ "createdAt": "2025-01-01T20:45:21.718Z",
+ "updatedAt": "2025-01-01T20:45:21.759Z",
+ "readonly": false,
+ "laborCosts": "0.00 EUR",
+ "materialCosts": "0.00 EUR",
+ "overallCosts": "0.00 EUR",
+ "_links": {
+ "attachments": {
+ "href": "/api/v3/work_packages/40/attachments"
+ },
+ "prepareAttachment": {
+ "href": "/api/v3/work_packages/40/attachments/prepare",
+ "method": "post"
+ },
+ "addAttachment": {
+ "href": "/api/v3/work_packages/40/attachments",
+ "method": "post"
+ },
+ "fileLinks": {
+ "href": "/api/v3/work_packages/40/file_links"
+ },
+ "addFileLink": {
+ "href": "/api/v3/work_packages/40/file_links",
+ "method": "post"
+ },
+ "self": {
+ "href": "/api/v3/work_packages/40",
+ "title": "kl"
+ },
+ "update": {
+ "href": "/api/v3/work_packages/40/form",
+ "method": "post"
+ },
+ "schema": {
+ "href": "/api/v3/work_packages/schemas/3-1"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/work_packages/40",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/work_packages/40",
+ "method": "delete"
+ },
+ "logTime": {
+ "href": "/api/v3/time_entries",
+ "title": "Log time on work package 'kl'"
+ },
+ "move": {
+ "href": "/work_packages/40/move/new",
+ "type": "text/html",
+ "title": "Move work package 'kl'"
+ },
+ "copy": {
+ "href": "/work_packages/40/copy",
+ "type": "text/html",
+ "title": "Copy work package 'kl'"
+ },
+ "pdf": {
+ "href": "/work_packages/40.pdf",
+ "type": "application/pdf",
+ "title": "Export as PDF"
+ },
+ "atom": {
+ "href": "/work_packages/40.atom",
+ "type": "application/rss+xml",
+ "title": "Atom feed"
+ },
+ "availableRelationCandidates": {
+ "href": "/api/v3/work_packages/40/available_relation_candidates",
+ "title": "Potential work packages to relate to"
+ },
+ "customFields": {
+ "href": "/projects/project1/settings/custom_fields",
+ "type": "text/html",
+ "title": "Custom fields"
+ },
+ "configureForm": {
+ "href": "/types/1/edit?tab=form_configuration",
+ "type": "text/html",
+ "title": "Configure form"
+ },
+ "activities": {
+ "href": "/api/v3/work_packages/40/activities"
+ },
+ "availableWatchers": {
+ "href": "/api/v3/work_packages/40/available_watchers"
+ },
+ "relations": {
+ "href": "/api/v3/work_packages/40/relations"
+ },
+ "revisions": {
+ "href": "/api/v3/work_packages/40/revisions"
+ },
+ "watchers": {
+ "href": "/api/v3/work_packages/40/watchers"
+ },
+ "addWatcher": {
+ "href": "/api/v3/work_packages/40/watchers",
+ "method": "post",
+ "payload": {
+ "user": {
+ "href": "/api/v3/users/{user_id}"
+ }
+ },
+ "templated": true
+ },
+ "removeWatcher": {
+ "href": "/api/v3/work_packages/40/watchers/{user_id}",
+ "method": "delete",
+ "templated": true
+ },
+ "addRelation": {
+ "href": "/api/v3/work_packages/40/relations",
+ "method": "post",
+ "title": "Add relation"
+ },
+ "addChild": {
+ "href": "/api/v3/projects/project1/work_packages",
+ "method": "post",
+ "title": "Add child of kl"
+ },
+ "changeParent": {
+ "href": "/api/v3/work_packages/40",
+ "method": "patch",
+ "title": "Change parent of kl"
+ },
+ "addComment": {
+ "href": "/api/v3/work_packages/40/activities",
+ "method": "post",
+ "title": "Add comment"
+ },
+ "previewMarkup": {
+ "href": "/api/v3/render/markdown?context=/api/v3/work_packages/40",
+ "method": "post"
+ },
+ "timeEntries": {
+ "href": "/api/v3/time_entries?filters=%5B%7B%22work_package_id%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%2240%22%5D%7D%7D%5D",
+ "title": "Time entries"
+ },
+ "ancestors": [],
+ "category": {
+ "href": null
+ },
+ "type": {
+ "href": "/api/v3/types/1",
+ "title": "Task"
+ },
+ "priority": {
+ "href": "/api/v3/priorities/8",
+ "title": "Normal"
+ },
+ "project": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "status": {
+ "href": "/api/v3/statuses/1",
+ "title": "New"
+ },
+ "author": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "responsible": {
+ "href": null
+ },
+ "assignee": {
+ "href": null
+ },
+ "version": {
+ "href": null
+ },
+ "parent": {
+ "href": null,
+ "title": null
+ },
+ "customActions": [],
+ "logCosts": {
+ "href": "/work_packages/40/cost_entries/new",
+ "type": "text/html",
+ "title": "Log costs on kl"
+ },
+ "showCosts": {
+ "href": "/projects/3/cost_reports?fields%5B%5D=WorkPackageId&operators%5BWorkPackageId%5D=%3D&set_filter=1&values%5BWorkPackageId%5D=40",
+ "type": "text/html",
+ "title": "Show cost entries"
+ },
+ "costsByType": {
+ "href": "/api/v3/work_packages/40/summarized_costs_by_type"
+ },
+ "meetings": {
+ "href": "/work_packages/40/tabs/meetings",
+ "title": "meetings"
+ },
+ "github": {
+ "href": "/work_packages/40/tabs/github",
+ "title": "github"
+ },
+ "github_pull_requests": {
+ "href": "/api/v3/work_packages/40/github_pull_requests",
+ "title": "GitHub pull requests"
+ },
+ "gitlab": {
+ "href": "/work_packages/40/tabs/gitlab",
+ "title": "gitlab"
+ },
+ "gitlab_merge_requests": {
+ "href": "/api/v3/work_packages/40/gitlab_merge_requests",
+ "title": "Gitlab merge requests"
+ },
+ "gitlab_issues": {
+ "href": "/api/v3/work_packages/40/gitlab_issues",
+ "title": "Gitlab Issues"
+ },
+ "convertBCF": {
+ "href": "/api/bcf/2.1/projects/project1/topics",
+ "title": "Convert to BCF",
+ "payload": {
+ "reference_links": [
+ "/api/v3/work_packages/40"
+ ]
+ },
+ "method": "post"
+ }
+ }
+ },
+ "user": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2025-01-01T18:28:20.852Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "activity": {
+ "_type": "TimeEntriesActivity",
+ "id": 1,
+ "name": "Management",
+ "position": 1,
+ "default": true,
+ "_links": {
+ "self": {
+ "href": "/api/v3/time_entries/activities/1",
+ "title": "Management"
+ },
+ "projects": []
+ }
+ }
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/time_entries/2"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/time_entries/2",
+ "method": "patch"
+ },
+ "update": {
+ "href": "/api/v3/time_entries/2/form",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/time_entries/2",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/time_entries/schema"
+ },
+ "project": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "workPackage": {
+ "href": "/api/v3/work_packages/40",
+ "title": "kl"
+ },
+ "user": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "activity": {
+ "href": "/api/v3/time_entries/activities/1",
+ "title": "Management"
+ }
+ }
+ }
+}
diff --git a/zerver/webhooks/openproject/fixtures/time_entry_created__with_iso_hm.json b/zerver/webhooks/openproject/fixtures/time_entry_created__with_iso_hm.json
new file mode 100644
index 0000000000..58cffe7f7c
--- /dev/null
+++ b/zerver/webhooks/openproject/fixtures/time_entry_created__with_iso_hm.json
@@ -0,0 +1,445 @@
+{
+ "action": "time_entry:created",
+ "time_entry": {
+ "_type": "TimeEntry",
+ "id": 2,
+ "ongoing": false,
+ "comment": {
+ "format": "plain",
+ "raw": "",
+ "html": ""
+ },
+ "spentOn": "2025-01-02",
+ "hours": "PT07H42M",
+ "createdAt": "2025-01-01T21:20:58.333Z",
+ "updatedAt": "2025-01-01T21:20:58.333Z",
+ "_embedded": {
+ "project": {
+ "_type": "Project",
+ "id": 3,
+ "identifier": "project1",
+ "name": "Project1",
+ "active": true,
+ "public": false,
+ "description": {
+ "format": "markdown",
+ "raw": "1. nucxdswssdfdsf\\_hb\\_xcddsfjsxczxchkxvcxvyuguygyujijdgrfcxbcfxbcgfjhkhksf",
+ "html": "\nnucxdswssdfdsf_hb_xcddsfjsxczxchkxvcxvyuguygyujijdgrfcxbcfxbcgfjhkhksf \n "
+ },
+ "createdAt": "2024-12-31T18:07:24.546Z",
+ "updatedAt": "2025-01-01T20:41:55.823Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "createWorkPackage": {
+ "href": "/api/v3/projects/3/work_packages/form",
+ "method": "post"
+ },
+ "createWorkPackageImmediately": {
+ "href": "/api/v3/projects/3/work_packages",
+ "method": "post"
+ },
+ "workPackages": {
+ "href": "/api/v3/projects/3/work_packages"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/3/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/3/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/3/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/3/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/3",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/3",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [
+ {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ }
+ ],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "status": {
+ "href": null
+ }
+ }
+ },
+ "workPackage": {
+ "_type": "WorkPackage",
+ "id": 40,
+ "lockVersion": 0,
+ "subject": "kl",
+ "description": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "scheduleManually": false,
+ "startDate": null,
+ "dueDate": null,
+ "derivedStartDate": null,
+ "derivedDueDate": null,
+ "estimatedTime": null,
+ "derivedEstimatedTime": null,
+ "derivedRemainingTime": null,
+ "duration": null,
+ "ignoreNonWorkingDays": false,
+ "spentTime": "PT0S",
+ "percentageDone": null,
+ "derivedPercentageDone": null,
+ "createdAt": "2025-01-01T20:45:21.718Z",
+ "updatedAt": "2025-01-01T20:45:21.759Z",
+ "readonly": false,
+ "laborCosts": "0.00 EUR",
+ "materialCosts": "0.00 EUR",
+ "overallCosts": "0.00 EUR",
+ "_links": {
+ "attachments": {
+ "href": "/api/v3/work_packages/40/attachments"
+ },
+ "prepareAttachment": {
+ "href": "/api/v3/work_packages/40/attachments/prepare",
+ "method": "post"
+ },
+ "addAttachment": {
+ "href": "/api/v3/work_packages/40/attachments",
+ "method": "post"
+ },
+ "fileLinks": {
+ "href": "/api/v3/work_packages/40/file_links"
+ },
+ "addFileLink": {
+ "href": "/api/v3/work_packages/40/file_links",
+ "method": "post"
+ },
+ "self": {
+ "href": "/api/v3/work_packages/40",
+ "title": "kl"
+ },
+ "update": {
+ "href": "/api/v3/work_packages/40/form",
+ "method": "post"
+ },
+ "schema": {
+ "href": "/api/v3/work_packages/schemas/3-1"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/work_packages/40",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/work_packages/40",
+ "method": "delete"
+ },
+ "logTime": {
+ "href": "/api/v3/time_entries",
+ "title": "Log time on work package 'kl'"
+ },
+ "move": {
+ "href": "/work_packages/40/move/new",
+ "type": "text/html",
+ "title": "Move work package 'kl'"
+ },
+ "copy": {
+ "href": "/work_packages/40/copy",
+ "type": "text/html",
+ "title": "Copy work package 'kl'"
+ },
+ "pdf": {
+ "href": "/work_packages/40.pdf",
+ "type": "application/pdf",
+ "title": "Export as PDF"
+ },
+ "atom": {
+ "href": "/work_packages/40.atom",
+ "type": "application/rss+xml",
+ "title": "Atom feed"
+ },
+ "availableRelationCandidates": {
+ "href": "/api/v3/work_packages/40/available_relation_candidates",
+ "title": "Potential work packages to relate to"
+ },
+ "customFields": {
+ "href": "/projects/project1/settings/custom_fields",
+ "type": "text/html",
+ "title": "Custom fields"
+ },
+ "configureForm": {
+ "href": "/types/1/edit?tab=form_configuration",
+ "type": "text/html",
+ "title": "Configure form"
+ },
+ "activities": {
+ "href": "/api/v3/work_packages/40/activities"
+ },
+ "availableWatchers": {
+ "href": "/api/v3/work_packages/40/available_watchers"
+ },
+ "relations": {
+ "href": "/api/v3/work_packages/40/relations"
+ },
+ "revisions": {
+ "href": "/api/v3/work_packages/40/revisions"
+ },
+ "watchers": {
+ "href": "/api/v3/work_packages/40/watchers"
+ },
+ "addWatcher": {
+ "href": "/api/v3/work_packages/40/watchers",
+ "method": "post",
+ "payload": {
+ "user": {
+ "href": "/api/v3/users/{user_id}"
+ }
+ },
+ "templated": true
+ },
+ "removeWatcher": {
+ "href": "/api/v3/work_packages/40/watchers/{user_id}",
+ "method": "delete",
+ "templated": true
+ },
+ "addRelation": {
+ "href": "/api/v3/work_packages/40/relations",
+ "method": "post",
+ "title": "Add relation"
+ },
+ "addChild": {
+ "href": "/api/v3/projects/project1/work_packages",
+ "method": "post",
+ "title": "Add child of kl"
+ },
+ "changeParent": {
+ "href": "/api/v3/work_packages/40",
+ "method": "patch",
+ "title": "Change parent of kl"
+ },
+ "addComment": {
+ "href": "/api/v3/work_packages/40/activities",
+ "method": "post",
+ "title": "Add comment"
+ },
+ "previewMarkup": {
+ "href": "/api/v3/render/markdown?context=/api/v3/work_packages/40",
+ "method": "post"
+ },
+ "timeEntries": {
+ "href": "/api/v3/time_entries?filters=%5B%7B%22work_package_id%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%2240%22%5D%7D%7D%5D",
+ "title": "Time entries"
+ },
+ "ancestors": [],
+ "category": {
+ "href": null
+ },
+ "type": {
+ "href": "/api/v3/types/1",
+ "title": "Task"
+ },
+ "priority": {
+ "href": "/api/v3/priorities/8",
+ "title": "Normal"
+ },
+ "project": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "status": {
+ "href": "/api/v3/statuses/1",
+ "title": "New"
+ },
+ "author": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "responsible": {
+ "href": null
+ },
+ "assignee": {
+ "href": null
+ },
+ "version": {
+ "href": null
+ },
+ "parent": {
+ "href": null,
+ "title": null
+ },
+ "customActions": [],
+ "logCosts": {
+ "href": "/work_packages/40/cost_entries/new",
+ "type": "text/html",
+ "title": "Log costs on kl"
+ },
+ "showCosts": {
+ "href": "/projects/3/cost_reports?fields%5B%5D=WorkPackageId&operators%5BWorkPackageId%5D=%3D&set_filter=1&values%5BWorkPackageId%5D=40",
+ "type": "text/html",
+ "title": "Show cost entries"
+ },
+ "costsByType": {
+ "href": "/api/v3/work_packages/40/summarized_costs_by_type"
+ },
+ "meetings": {
+ "href": "/work_packages/40/tabs/meetings",
+ "title": "meetings"
+ },
+ "github": {
+ "href": "/work_packages/40/tabs/github",
+ "title": "github"
+ },
+ "github_pull_requests": {
+ "href": "/api/v3/work_packages/40/github_pull_requests",
+ "title": "GitHub pull requests"
+ },
+ "gitlab": {
+ "href": "/work_packages/40/tabs/gitlab",
+ "title": "gitlab"
+ },
+ "gitlab_merge_requests": {
+ "href": "/api/v3/work_packages/40/gitlab_merge_requests",
+ "title": "Gitlab merge requests"
+ },
+ "gitlab_issues": {
+ "href": "/api/v3/work_packages/40/gitlab_issues",
+ "title": "Gitlab Issues"
+ },
+ "convertBCF": {
+ "href": "/api/bcf/2.1/projects/project1/topics",
+ "title": "Convert to BCF",
+ "payload": {
+ "reference_links": [
+ "/api/v3/work_packages/40"
+ ]
+ },
+ "method": "post"
+ }
+ }
+ },
+ "user": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2025-01-01T18:28:20.852Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "activity": {
+ "_type": "TimeEntriesActivity",
+ "id": 1,
+ "name": "Management",
+ "position": 1,
+ "default": true,
+ "_links": {
+ "self": {
+ "href": "/api/v3/time_entries/activities/1",
+ "title": "Management"
+ },
+ "projects": []
+ }
+ }
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/time_entries/2"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/time_entries/2",
+ "method": "patch"
+ },
+ "update": {
+ "href": "/api/v3/time_entries/2/form",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/time_entries/2",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/time_entries/schema"
+ },
+ "project": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "workPackage": {
+ "href": "/api/v3/work_packages/40",
+ "title": "kl"
+ },
+ "user": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "activity": {
+ "href": "/api/v3/time_entries/activities/1",
+ "title": "Management"
+ }
+ }
+ }
+}
diff --git a/zerver/webhooks/openproject/fixtures/time_entry_created__with_workpackage.json b/zerver/webhooks/openproject/fixtures/time_entry_created__with_workpackage.json
new file mode 100644
index 0000000000..9007af28fa
--- /dev/null
+++ b/zerver/webhooks/openproject/fixtures/time_entry_created__with_workpackage.json
@@ -0,0 +1,445 @@
+{
+ "action": "time_entry:created",
+ "time_entry": {
+ "_type": "TimeEntry",
+ "id": 2,
+ "ongoing": false,
+ "comment": {
+ "format": "plain",
+ "raw": "",
+ "html": ""
+ },
+ "spentOn": "2025-01-02",
+ "hours": "PT1H",
+ "createdAt": "2025-01-01T21:20:58.333Z",
+ "updatedAt": "2025-01-01T21:20:58.333Z",
+ "_embedded": {
+ "project": {
+ "_type": "Project",
+ "id": 3,
+ "identifier": "project1",
+ "name": "Project1",
+ "active": true,
+ "public": false,
+ "description": {
+ "format": "markdown",
+ "raw": "1. nucxdswssdfdsf\\_hb\\_xcddsfjsxczxchkxvcxvyuguygyujijdgrfcxbcfxbcgfjhkhksf",
+ "html": "\nnucxdswssdfdsf_hb_xcddsfjsxczxchkxvcxvyuguygyujijdgrfcxbcfxbcgfjhkhksf \n "
+ },
+ "createdAt": "2024-12-31T18:07:24.546Z",
+ "updatedAt": "2025-01-01T20:41:55.823Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "createWorkPackage": {
+ "href": "/api/v3/projects/3/work_packages/form",
+ "method": "post"
+ },
+ "createWorkPackageImmediately": {
+ "href": "/api/v3/projects/3/work_packages",
+ "method": "post"
+ },
+ "workPackages": {
+ "href": "/api/v3/projects/3/work_packages"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/3/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/3/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/3/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/3/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/3",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/3",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [
+ {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ }
+ ],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "status": {
+ "href": null
+ }
+ }
+ },
+ "workPackage": {
+ "_type": "WorkPackage",
+ "id": 40,
+ "lockVersion": 0,
+ "subject": "kl",
+ "description": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "scheduleManually": false,
+ "startDate": null,
+ "dueDate": null,
+ "derivedStartDate": null,
+ "derivedDueDate": null,
+ "estimatedTime": null,
+ "derivedEstimatedTime": null,
+ "derivedRemainingTime": null,
+ "duration": null,
+ "ignoreNonWorkingDays": false,
+ "spentTime": "PT0S",
+ "percentageDone": null,
+ "derivedPercentageDone": null,
+ "createdAt": "2025-01-01T20:45:21.718Z",
+ "updatedAt": "2025-01-01T20:45:21.759Z",
+ "readonly": false,
+ "laborCosts": "0.00 EUR",
+ "materialCosts": "0.00 EUR",
+ "overallCosts": "0.00 EUR",
+ "_links": {
+ "attachments": {
+ "href": "/api/v3/work_packages/40/attachments"
+ },
+ "prepareAttachment": {
+ "href": "/api/v3/work_packages/40/attachments/prepare",
+ "method": "post"
+ },
+ "addAttachment": {
+ "href": "/api/v3/work_packages/40/attachments",
+ "method": "post"
+ },
+ "fileLinks": {
+ "href": "/api/v3/work_packages/40/file_links"
+ },
+ "addFileLink": {
+ "href": "/api/v3/work_packages/40/file_links",
+ "method": "post"
+ },
+ "self": {
+ "href": "/api/v3/work_packages/40",
+ "title": "kl"
+ },
+ "update": {
+ "href": "/api/v3/work_packages/40/form",
+ "method": "post"
+ },
+ "schema": {
+ "href": "/api/v3/work_packages/schemas/3-1"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/work_packages/40",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/work_packages/40",
+ "method": "delete"
+ },
+ "logTime": {
+ "href": "/api/v3/time_entries",
+ "title": "Log time on work package 'kl'"
+ },
+ "move": {
+ "href": "/work_packages/40/move/new",
+ "type": "text/html",
+ "title": "Move work package 'kl'"
+ },
+ "copy": {
+ "href": "/work_packages/40/copy",
+ "type": "text/html",
+ "title": "Copy work package 'kl'"
+ },
+ "pdf": {
+ "href": "/work_packages/40.pdf",
+ "type": "application/pdf",
+ "title": "Export as PDF"
+ },
+ "atom": {
+ "href": "/work_packages/40.atom",
+ "type": "application/rss+xml",
+ "title": "Atom feed"
+ },
+ "availableRelationCandidates": {
+ "href": "/api/v3/work_packages/40/available_relation_candidates",
+ "title": "Potential work packages to relate to"
+ },
+ "customFields": {
+ "href": "/projects/project1/settings/custom_fields",
+ "type": "text/html",
+ "title": "Custom fields"
+ },
+ "configureForm": {
+ "href": "/types/1/edit?tab=form_configuration",
+ "type": "text/html",
+ "title": "Configure form"
+ },
+ "activities": {
+ "href": "/api/v3/work_packages/40/activities"
+ },
+ "availableWatchers": {
+ "href": "/api/v3/work_packages/40/available_watchers"
+ },
+ "relations": {
+ "href": "/api/v3/work_packages/40/relations"
+ },
+ "revisions": {
+ "href": "/api/v3/work_packages/40/revisions"
+ },
+ "watchers": {
+ "href": "/api/v3/work_packages/40/watchers"
+ },
+ "addWatcher": {
+ "href": "/api/v3/work_packages/40/watchers",
+ "method": "post",
+ "payload": {
+ "user": {
+ "href": "/api/v3/users/{user_id}"
+ }
+ },
+ "templated": true
+ },
+ "removeWatcher": {
+ "href": "/api/v3/work_packages/40/watchers/{user_id}",
+ "method": "delete",
+ "templated": true
+ },
+ "addRelation": {
+ "href": "/api/v3/work_packages/40/relations",
+ "method": "post",
+ "title": "Add relation"
+ },
+ "addChild": {
+ "href": "/api/v3/projects/project1/work_packages",
+ "method": "post",
+ "title": "Add child of kl"
+ },
+ "changeParent": {
+ "href": "/api/v3/work_packages/40",
+ "method": "patch",
+ "title": "Change parent of kl"
+ },
+ "addComment": {
+ "href": "/api/v3/work_packages/40/activities",
+ "method": "post",
+ "title": "Add comment"
+ },
+ "previewMarkup": {
+ "href": "/api/v3/render/markdown?context=/api/v3/work_packages/40",
+ "method": "post"
+ },
+ "timeEntries": {
+ "href": "/api/v3/time_entries?filters=%5B%7B%22work_package_id%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%2240%22%5D%7D%7D%5D",
+ "title": "Time entries"
+ },
+ "ancestors": [],
+ "category": {
+ "href": null
+ },
+ "type": {
+ "href": "/api/v3/types/1",
+ "title": "Task"
+ },
+ "priority": {
+ "href": "/api/v3/priorities/8",
+ "title": "Normal"
+ },
+ "project": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "status": {
+ "href": "/api/v3/statuses/1",
+ "title": "New"
+ },
+ "author": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "responsible": {
+ "href": null
+ },
+ "assignee": {
+ "href": null
+ },
+ "version": {
+ "href": null
+ },
+ "parent": {
+ "href": null,
+ "title": null
+ },
+ "customActions": [],
+ "logCosts": {
+ "href": "/work_packages/40/cost_entries/new",
+ "type": "text/html",
+ "title": "Log costs on kl"
+ },
+ "showCosts": {
+ "href": "/projects/3/cost_reports?fields%5B%5D=WorkPackageId&operators%5BWorkPackageId%5D=%3D&set_filter=1&values%5BWorkPackageId%5D=40",
+ "type": "text/html",
+ "title": "Show cost entries"
+ },
+ "costsByType": {
+ "href": "/api/v3/work_packages/40/summarized_costs_by_type"
+ },
+ "meetings": {
+ "href": "/work_packages/40/tabs/meetings",
+ "title": "meetings"
+ },
+ "github": {
+ "href": "/work_packages/40/tabs/github",
+ "title": "github"
+ },
+ "github_pull_requests": {
+ "href": "/api/v3/work_packages/40/github_pull_requests",
+ "title": "GitHub pull requests"
+ },
+ "gitlab": {
+ "href": "/work_packages/40/tabs/gitlab",
+ "title": "gitlab"
+ },
+ "gitlab_merge_requests": {
+ "href": "/api/v3/work_packages/40/gitlab_merge_requests",
+ "title": "Gitlab merge requests"
+ },
+ "gitlab_issues": {
+ "href": "/api/v3/work_packages/40/gitlab_issues",
+ "title": "Gitlab Issues"
+ },
+ "convertBCF": {
+ "href": "/api/bcf/2.1/projects/project1/topics",
+ "title": "Convert to BCF",
+ "payload": {
+ "reference_links": [
+ "/api/v3/work_packages/40"
+ ]
+ },
+ "method": "post"
+ }
+ }
+ },
+ "user": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2025-01-01T18:28:20.852Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "activity": {
+ "_type": "TimeEntriesActivity",
+ "id": 1,
+ "name": "Management",
+ "position": 1,
+ "default": true,
+ "_links": {
+ "self": {
+ "href": "/api/v3/time_entries/activities/1",
+ "title": "Management"
+ },
+ "projects": []
+ }
+ }
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/time_entries/2"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/time_entries/2",
+ "method": "patch"
+ },
+ "update": {
+ "href": "/api/v3/time_entries/2/form",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/time_entries/2",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/time_entries/schema"
+ },
+ "project": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "workPackage": {
+ "href": "/api/v3/work_packages/40",
+ "title": "kl"
+ },
+ "user": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "activity": {
+ "href": "/api/v3/time_entries/activities/1",
+ "title": "Management"
+ }
+ }
+ }
+}
diff --git a/zerver/webhooks/openproject/fixtures/time_entry_created__without_workpackage.json b/zerver/webhooks/openproject/fixtures/time_entry_created__without_workpackage.json
new file mode 100644
index 0000000000..e1a25b3ab8
--- /dev/null
+++ b/zerver/webhooks/openproject/fixtures/time_entry_created__without_workpackage.json
@@ -0,0 +1,180 @@
+{
+ "action": "time_entry:created",
+ "time_entry": {
+ "_type": "TimeEntry",
+ "id": 2,
+ "ongoing": false,
+ "comment": {
+ "format": "plain",
+ "raw": "",
+ "html": ""
+ },
+ "spentOn": "2025-01-02",
+ "hours": "PT1H",
+ "createdAt": "2025-01-01T21:20:58.333Z",
+ "updatedAt": "2025-01-01T21:20:58.333Z",
+ "_embedded": {
+ "project": {
+ "_type": "Project",
+ "id": 3,
+ "identifier": "project1",
+ "name": "Project1",
+ "active": true,
+ "public": false,
+ "description": {
+ "format": "markdown",
+ "raw": "1. nucxdswssdfdsf\\_hb\\_xcddsfjsxczxchkxvcxvyuguygyujijdgrfcxbcfxbcgfjhkhksf",
+ "html": "\nnucxdswssdfdsf_hb_xcddsfjsxczxchkxvcxvyuguygyujijdgrfcxbcfxbcgfjhkhksf \n "
+ },
+ "createdAt": "2024-12-31T18:07:24.546Z",
+ "updatedAt": "2025-01-01T20:41:55.823Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/3/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/3/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/3/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/3/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/3",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/3",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [
+ {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ }
+ ],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%223%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "status": {
+ "href": null
+ }
+ }
+ },
+ "user": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2025-01-01T18:28:20.852Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "activity": {
+ "_type": "TimeEntriesActivity",
+ "id": 1,
+ "name": "Management",
+ "position": 1,
+ "default": true,
+ "_links": {
+ "self": {
+ "href": "/api/v3/time_entries/activities/1",
+ "title": "Management"
+ },
+ "projects": []
+ }
+ }
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/time_entries/2"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/time_entries/2",
+ "method": "patch"
+ },
+ "update": {
+ "href": "/api/v3/time_entries/2/form",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/time_entries/2",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/time_entries/schema"
+ },
+ "project": {
+ "href": "/api/v3/projects/3",
+ "title": "Project1"
+ },
+ "user": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "activity": {
+ "href": "/api/v3/time_entries/activities/1",
+ "title": "Management"
+ }
+ }
+ }
+}
diff --git a/zerver/webhooks/openproject/fixtures/work_package_created.json b/zerver/webhooks/openproject/fixtures/work_package_created.json
new file mode 100644
index 0000000000..3ad891c95a
--- /dev/null
+++ b/zerver/webhooks/openproject/fixtures/work_package_created.json
@@ -0,0 +1,551 @@
+{
+ "action": "work_package:created",
+ "work_package": {
+ "_type": "WorkPackage",
+ "id": 37,
+ "lockVersion": 0,
+ "subject": "Task1",
+ "description": {
+ "format": "markdown",
+ "raw": "",
+ "html": ""
+ },
+ "scheduleManually": false,
+ "startDate": null,
+ "dueDate": null,
+ "derivedStartDate": null,
+ "derivedDueDate": null,
+ "estimatedTime": null,
+ "derivedEstimatedTime": null,
+ "derivedRemainingTime": null,
+ "duration": null,
+ "ignoreNonWorkingDays": false,
+ "percentageDone": null,
+ "derivedPercentageDone": null,
+ "createdAt": "2024-12-31T13:09:24.901Z",
+ "updatedAt": "2024-12-31T13:09:24.969Z",
+ "readonly": false,
+ "_embedded": {
+ "attachments": {
+ "_type": "Collection",
+ "total": 0,
+ "count": 0,
+ "_embedded": {
+ "elements": []
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/work_packages/37/attachments"
+ }
+ }
+ },
+ "fileLinks": {
+ "_type": "Collection",
+ "total": 0,
+ "count": 0,
+ "pageSize": 30,
+ "offset": 1,
+ "_embedded": {
+ "elements": []
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/work_packages/37/file_links?offset=1&pageSize=30"
+ },
+ "jumpTo": {
+ "href": "/api/v3/work_packages/37/file_links?offset=%7Boffset%7D&pageSize=30",
+ "templated": true
+ },
+ "changeSize": {
+ "href": "/api/v3/work_packages/37/file_links?offset=1&pageSize=%7Bsize%7D",
+ "templated": true
+ }
+ }
+ },
+ "relations": {
+ "_type": "Collection",
+ "total": 0,
+ "count": 0,
+ "_embedded": {
+ "elements": []
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/work_packages/37/relations"
+ }
+ }
+ },
+ "type": {
+ "_type": "Type",
+ "id": 1,
+ "name": "Task",
+ "color": "#1A67A3",
+ "position": 1,
+ "isDefault": true,
+ "isMilestone": false,
+ "createdAt": "2024-12-31T12:52:41.042Z",
+ "updatedAt": "2024-12-31T12:52:41.042Z",
+ "_links": {
+ "self": {
+ "href": "/api/v3/types/1",
+ "title": "Task"
+ }
+ }
+ },
+ "priority": {
+ "_type": "Priority",
+ "id": 8,
+ "name": "Normal",
+ "position": 2,
+ "color": "#74C0FC",
+ "isDefault": true,
+ "isActive": true,
+ "_links": {
+ "self": {
+ "href": "/api/v3/priorities/8",
+ "title": "Normal"
+ }
+ }
+ },
+ "project": {
+ "_type": "Project",
+ "id": 1,
+ "identifier": "demo-project",
+ "name": "Demo project",
+ "active": true,
+ "public": true,
+ "description": {
+ "format": "markdown",
+ "raw": "This is a short summary of the goals of this demo project.",
+ "html": "This is a short summary of the goals of this demo project.
"
+ },
+ "createdAt": "2024-12-31T12:52:45.287Z",
+ "updatedAt": "2024-12-31T12:52:45.287Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "All tasks are on schedule. The people involved know their tasks. The system is completely set up.",
+ "html": "All tasks are on schedule. The people involved know their tasks. The system is completely set up.
"
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "createWorkPackage": {
+ "href": "/api/v3/projects/1/work_packages/form",
+ "method": "post"
+ },
+ "createWorkPackageImmediately": {
+ "href": "/api/v3/projects/1/work_packages",
+ "method": "post"
+ },
+ "workPackages": {
+ "href": "/api/v3/projects/1/work_packages"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/1/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/1/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/1/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/1/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/1",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/1",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": null
+ },
+ "status": {
+ "href": "/api/v3/project_statuses/on_track",
+ "title": "On track"
+ }
+ }
+ },
+ "status": {
+ "_type": "Status",
+ "id": 1,
+ "name": "New",
+ "isClosed": false,
+ "color": "#1098AD",
+ "isDefault": true,
+ "isReadonly": false,
+ "excludedFromTotals": false,
+ "defaultDoneRatio": 0,
+ "position": 1,
+ "_links": {
+ "self": {
+ "href": "/api/v3/statuses/1",
+ "title": "New"
+ }
+ }
+ },
+ "author": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2024-12-31T12:54:19.804Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "responsible": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2024-12-31T12:54:19.804Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "assignee": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2024-12-31T12:54:19.804Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "customActions": []
+ },
+ "_links": {
+ "attachments": {
+ "href": "/api/v3/work_packages/37/attachments"
+ },
+ "prepareAttachment": {
+ "href": "/api/v3/work_packages/37/attachments/prepare",
+ "method": "post"
+ },
+ "addAttachment": {
+ "href": "/api/v3/work_packages/37/attachments",
+ "method": "post"
+ },
+ "fileLinks": {
+ "href": "/api/v3/work_packages/37/file_links"
+ },
+ "addFileLink": {
+ "href": "/api/v3/work_packages/37/file_links",
+ "method": "post"
+ },
+ "self": {
+ "href": "/api/v3/work_packages/37",
+ "title": "Task1"
+ },
+ "update": {
+ "href": "/api/v3/work_packages/37/form",
+ "method": "post"
+ },
+ "schema": {
+ "href": "/api/v3/work_packages/schemas/1-1"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/work_packages/37",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/work_packages/37",
+ "method": "delete"
+ },
+ "logTime": {
+ "href": "/api/v3/time_entries",
+ "title": "Log time on work package 'Task1'"
+ },
+ "move": {
+ "href": "/work_packages/37/move/new",
+ "type": "text/html",
+ "title": "Move work package 'Task1'"
+ },
+ "copy": {
+ "href": "/work_packages/37/copy",
+ "type": "text/html",
+ "title": "Copy work package 'Task1'"
+ },
+ "pdf": {
+ "href": "/work_packages/37.pdf",
+ "type": "application/pdf",
+ "title": "Export as PDF"
+ },
+ "atom": {
+ "href": "/work_packages/37.atom",
+ "type": "application/rss+xml",
+ "title": "Atom feed"
+ },
+ "availableRelationCandidates": {
+ "href": "/api/v3/work_packages/37/available_relation_candidates",
+ "title": "Potential work packages to relate to"
+ },
+ "customFields": {
+ "href": "/projects/demo-project/settings/custom_fields",
+ "type": "text/html",
+ "title": "Custom fields"
+ },
+ "configureForm": {
+ "href": "/types/1/edit?tab=form_configuration",
+ "type": "text/html",
+ "title": "Configure form"
+ },
+ "activities": {
+ "href": "/api/v3/work_packages/37/activities"
+ },
+ "availableWatchers": {
+ "href": "/api/v3/work_packages/37/available_watchers"
+ },
+ "relations": {
+ "href": "/api/v3/work_packages/37/relations"
+ },
+ "revisions": {
+ "href": "/api/v3/work_packages/37/revisions"
+ },
+ "watchers": {
+ "href": "/api/v3/work_packages/37/watchers"
+ },
+ "addWatcher": {
+ "href": "/api/v3/work_packages/37/watchers",
+ "method": "post",
+ "payload": {
+ "user": {
+ "href": "/api/v3/users/{user_id}"
+ }
+ },
+ "templated": true
+ },
+ "removeWatcher": {
+ "href": "/api/v3/work_packages/37/watchers/{user_id}",
+ "method": "delete",
+ "templated": true
+ },
+ "addRelation": {
+ "href": "/api/v3/work_packages/37/relations",
+ "method": "post",
+ "title": "Add relation"
+ },
+ "addChild": {
+ "href": "/api/v3/projects/demo-project/work_packages",
+ "method": "post",
+ "title": "Add child of Task1"
+ },
+ "changeParent": {
+ "href": "/api/v3/work_packages/37",
+ "method": "patch",
+ "title": "Change parent of Task1"
+ },
+ "addComment": {
+ "href": "/api/v3/work_packages/37/activities",
+ "method": "post",
+ "title": "Add comment"
+ },
+ "previewMarkup": {
+ "href": "/api/v3/render/markdown?context=/api/v3/work_packages/37",
+ "method": "post"
+ },
+ "timeEntries": {
+ "href": "/api/v3/time_entries?filters=%5B%7B%22work_package_id%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%2237%22%5D%7D%7D%5D",
+ "title": "Time entries"
+ },
+ "ancestors": [],
+ "category": {
+ "href": null
+ },
+ "type": {
+ "href": "/api/v3/types/1",
+ "title": "Task"
+ },
+ "priority": {
+ "href": "/api/v3/priorities/8",
+ "title": "Normal"
+ },
+ "project": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "status": {
+ "href": "/api/v3/statuses/1",
+ "title": "New"
+ },
+ "author": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "responsible": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "assignee": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "version": {
+ "href": null
+ },
+ "parent": {
+ "href": null,
+ "title": null
+ },
+ "customActions": [],
+ "meetings": {
+ "href": "/work_packages/37/tabs/meetings",
+ "title": "meetings"
+ },
+ "github": {
+ "href": "/work_packages/37/tabs/github",
+ "title": "github"
+ },
+ "github_pull_requests": {
+ "href": "/api/v3/work_packages/37/github_pull_requests",
+ "title": "GitHub pull requests"
+ },
+ "gitlab": {
+ "href": "/work_packages/37/tabs/gitlab",
+ "title": "gitlab"
+ },
+ "gitlab_merge_requests": {
+ "href": "/api/v3/work_packages/37/gitlab_merge_requests",
+ "title": "Gitlab merge requests"
+ },
+ "gitlab_issues": {
+ "href": "/api/v3/work_packages/37/gitlab_issues",
+ "title": "Gitlab Issues"
+ },
+ "convertBCF": {
+ "href": "/api/bcf/2.1/projects/demo-project/topics",
+ "title": "Convert to BCF",
+ "payload": {
+ "reference_links": [
+ "/api/v3/work_packages/37"
+ ]
+ },
+ "method": "post"
+ }
+ }
+ }
+ }
diff --git a/zerver/webhooks/openproject/fixtures/work_package_updated.json b/zerver/webhooks/openproject/fixtures/work_package_updated.json
new file mode 100644
index 0000000000..933c4557da
--- /dev/null
+++ b/zerver/webhooks/openproject/fixtures/work_package_updated.json
@@ -0,0 +1,551 @@
+{
+ "action": "work_package:updated",
+ "work_package": {
+ "_type": "WorkPackage",
+ "id": 37,
+ "lockVersion": 1,
+ "subject": "Task1",
+ "description": {
+ "format": "markdown",
+ "raw": "dxvdsvds",
+ "html": "dxvdsvds
"
+ },
+ "scheduleManually": false,
+ "startDate": null,
+ "dueDate": null,
+ "derivedStartDate": null,
+ "derivedDueDate": null,
+ "estimatedTime": null,
+ "derivedEstimatedTime": null,
+ "derivedRemainingTime": null,
+ "duration": null,
+ "ignoreNonWorkingDays": false,
+ "percentageDone": null,
+ "derivedPercentageDone": null,
+ "createdAt": "2024-12-31T13:09:24.901Z",
+ "updatedAt": "2024-12-31T18:10:05.102Z",
+ "readonly": false,
+ "_embedded": {
+ "attachments": {
+ "_type": "Collection",
+ "total": 0,
+ "count": 0,
+ "_embedded": {
+ "elements": []
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/work_packages/37/attachments"
+ }
+ }
+ },
+ "fileLinks": {
+ "_type": "Collection",
+ "total": 0,
+ "count": 0,
+ "pageSize": 30,
+ "offset": 1,
+ "_embedded": {
+ "elements": []
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/work_packages/37/file_links?offset=1&pageSize=30"
+ },
+ "jumpTo": {
+ "href": "/api/v3/work_packages/37/file_links?offset=%7Boffset%7D&pageSize=30",
+ "templated": true
+ },
+ "changeSize": {
+ "href": "/api/v3/work_packages/37/file_links?offset=1&pageSize=%7Bsize%7D",
+ "templated": true
+ }
+ }
+ },
+ "relations": {
+ "_type": "Collection",
+ "total": 0,
+ "count": 0,
+ "_embedded": {
+ "elements": []
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/work_packages/37/relations"
+ }
+ }
+ },
+ "type": {
+ "_type": "Type",
+ "id": 1,
+ "name": "Task",
+ "color": "#1A67A3",
+ "position": 1,
+ "isDefault": true,
+ "isMilestone": false,
+ "createdAt": "2024-12-31T12:52:41.042Z",
+ "updatedAt": "2024-12-31T12:52:41.042Z",
+ "_links": {
+ "self": {
+ "href": "/api/v3/types/1",
+ "title": "Task"
+ }
+ }
+ },
+ "priority": {
+ "_type": "Priority",
+ "id": 8,
+ "name": "Normal",
+ "position": 2,
+ "color": "#74C0FC",
+ "isDefault": true,
+ "isActive": true,
+ "_links": {
+ "self": {
+ "href": "/api/v3/priorities/8",
+ "title": "Normal"
+ }
+ }
+ },
+ "project": {
+ "_type": "Project",
+ "id": 1,
+ "identifier": "demo-project",
+ "name": "Demo project",
+ "active": true,
+ "public": true,
+ "description": {
+ "format": "markdown",
+ "raw": "This is a short summary of the goals of this demo project.",
+ "html": "This is a short summary of the goals of this demo project.
"
+ },
+ "createdAt": "2024-12-31T12:52:45.287Z",
+ "updatedAt": "2024-12-31T18:07:24.609Z",
+ "statusExplanation": {
+ "format": "markdown",
+ "raw": "All tasks are on schedule. The people involved know their tasks. The system is completely set up.",
+ "html": "All tasks are on schedule. The people involved know their tasks. The system is completely set up.
"
+ },
+ "_links": {
+ "self": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "createWorkPackage": {
+ "href": "/api/v3/projects/1/work_packages/form",
+ "method": "post"
+ },
+ "createWorkPackageImmediately": {
+ "href": "/api/v3/projects/1/work_packages",
+ "method": "post"
+ },
+ "workPackages": {
+ "href": "/api/v3/projects/1/work_packages"
+ },
+ "storages": [],
+ "categories": {
+ "href": "/api/v3/projects/1/categories"
+ },
+ "versions": {
+ "href": "/api/v3/projects/1/versions"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22project%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D"
+ },
+ "types": {
+ "href": "/api/v3/projects/1/types"
+ },
+ "update": {
+ "href": "/api/v3/projects/1/form",
+ "method": "post"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/projects/1",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/projects/1",
+ "method": "delete"
+ },
+ "schema": {
+ "href": "/api/v3/projects/schema"
+ },
+ "ancestors": [],
+ "projectStorages": {
+ "href": "/api/v3/project_storages?filters=%5B%7B%22projectId%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%221%22%5D%7D%7D%5D"
+ },
+ "parent": {
+ "href": null
+ },
+ "status": {
+ "href": "/api/v3/project_statuses/on_track",
+ "title": "On track"
+ }
+ }
+ },
+ "status": {
+ "_type": "Status",
+ "id": 1,
+ "name": "New",
+ "isClosed": false,
+ "color": "#1098AD",
+ "isDefault": true,
+ "isReadonly": false,
+ "excludedFromTotals": false,
+ "defaultDoneRatio": 0,
+ "position": 1,
+ "_links": {
+ "self": {
+ "href": "/api/v3/statuses/1",
+ "title": "New"
+ }
+ }
+ },
+ "author": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2024-12-31T12:54:19.804Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "responsible": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2024-12-31T12:54:19.804Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "assignee": {
+ "_type": "User",
+ "id": 4,
+ "name": "Nirved Mishra",
+ "createdAt": "2024-12-31T12:52:44.786Z",
+ "updatedAt": "2024-12-31T12:54:19.804Z",
+ "login": "nirved431@gmail.com",
+ "admin": true,
+ "firstName": "Nirved",
+ "lastName": "Mishra",
+ "email": "nirved431@gmail.com",
+ "avatar": "https://secure.gravatar.com/avatar/aef06c318f044c985140e4ecae344c4a?default=404&secure=true",
+ "status": "active",
+ "identityUrl": null,
+ "language": "en",
+ "_links": {
+ "self": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "memberships": {
+ "href": "/api/v3/memberships?filters=%5B%7B%22principal%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%224%22%5D%7D%7D%5D",
+ "title": "Memberships"
+ },
+ "showUser": {
+ "href": "/users/4",
+ "type": "text/html"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/users/4",
+ "title": "Update nirved431@gmail.com",
+ "method": "patch"
+ },
+ "lock": {
+ "href": "/api/v3/users/4/lock",
+ "title": "Set lock on nirved431@gmail.com",
+ "method": "post"
+ },
+ "delete": {
+ "href": "/api/v3/users/4",
+ "title": "Delete nirved431@gmail.com",
+ "method": "delete"
+ }
+ }
+ },
+ "customActions": []
+ },
+ "_links": {
+ "attachments": {
+ "href": "/api/v3/work_packages/37/attachments"
+ },
+ "prepareAttachment": {
+ "href": "/api/v3/work_packages/37/attachments/prepare",
+ "method": "post"
+ },
+ "addAttachment": {
+ "href": "/api/v3/work_packages/37/attachments",
+ "method": "post"
+ },
+ "fileLinks": {
+ "href": "/api/v3/work_packages/37/file_links"
+ },
+ "addFileLink": {
+ "href": "/api/v3/work_packages/37/file_links",
+ "method": "post"
+ },
+ "self": {
+ "href": "/api/v3/work_packages/37",
+ "title": "Task1"
+ },
+ "update": {
+ "href": "/api/v3/work_packages/37/form",
+ "method": "post"
+ },
+ "schema": {
+ "href": "/api/v3/work_packages/schemas/1-1"
+ },
+ "updateImmediately": {
+ "href": "/api/v3/work_packages/37",
+ "method": "patch"
+ },
+ "delete": {
+ "href": "/api/v3/work_packages/37",
+ "method": "delete"
+ },
+ "logTime": {
+ "href": "/api/v3/time_entries",
+ "title": "Log time on work package 'Task1'"
+ },
+ "move": {
+ "href": "/work_packages/37/move/new",
+ "type": "text/html",
+ "title": "Move work package 'Task1'"
+ },
+ "copy": {
+ "href": "/work_packages/37/copy",
+ "type": "text/html",
+ "title": "Copy work package 'Task1'"
+ },
+ "pdf": {
+ "href": "/work_packages/37.pdf",
+ "type": "application/pdf",
+ "title": "Export as PDF"
+ },
+ "atom": {
+ "href": "/work_packages/37.atom",
+ "type": "application/rss+xml",
+ "title": "Atom feed"
+ },
+ "availableRelationCandidates": {
+ "href": "/api/v3/work_packages/37/available_relation_candidates",
+ "title": "Potential work packages to relate to"
+ },
+ "customFields": {
+ "href": "/projects/demo-project/settings/custom_fields",
+ "type": "text/html",
+ "title": "Custom fields"
+ },
+ "configureForm": {
+ "href": "/types/1/edit?tab=form_configuration",
+ "type": "text/html",
+ "title": "Configure form"
+ },
+ "activities": {
+ "href": "/api/v3/work_packages/37/activities"
+ },
+ "availableWatchers": {
+ "href": "/api/v3/work_packages/37/available_watchers"
+ },
+ "relations": {
+ "href": "/api/v3/work_packages/37/relations"
+ },
+ "revisions": {
+ "href": "/api/v3/work_packages/37/revisions"
+ },
+ "watchers": {
+ "href": "/api/v3/work_packages/37/watchers"
+ },
+ "addWatcher": {
+ "href": "/api/v3/work_packages/37/watchers",
+ "method": "post",
+ "payload": {
+ "user": {
+ "href": "/api/v3/users/{user_id}"
+ }
+ },
+ "templated": true
+ },
+ "removeWatcher": {
+ "href": "/api/v3/work_packages/37/watchers/{user_id}",
+ "method": "delete",
+ "templated": true
+ },
+ "addRelation": {
+ "href": "/api/v3/work_packages/37/relations",
+ "method": "post",
+ "title": "Add relation"
+ },
+ "addChild": {
+ "href": "/api/v3/projects/demo-project/work_packages",
+ "method": "post",
+ "title": "Add child of Task1"
+ },
+ "changeParent": {
+ "href": "/api/v3/work_packages/37",
+ "method": "patch",
+ "title": "Change parent of Task1"
+ },
+ "addComment": {
+ "href": "/api/v3/work_packages/37/activities",
+ "method": "post",
+ "title": "Add comment"
+ },
+ "previewMarkup": {
+ "href": "/api/v3/render/markdown?context=/api/v3/work_packages/37",
+ "method": "post"
+ },
+ "timeEntries": {
+ "href": "/api/v3/time_entries?filters=%5B%7B%22work_package_id%22%3A%7B%22operator%22%3A%22%3D%22%2C%22values%22%3A%5B%2237%22%5D%7D%7D%5D",
+ "title": "Time entries"
+ },
+ "ancestors": [],
+ "category": {
+ "href": null
+ },
+ "type": {
+ "href": "/api/v3/types/1",
+ "title": "Task"
+ },
+ "priority": {
+ "href": "/api/v3/priorities/8",
+ "title": "Normal"
+ },
+ "project": {
+ "href": "/api/v3/projects/1",
+ "title": "Demo project"
+ },
+ "status": {
+ "href": "/api/v3/statuses/1",
+ "title": "New"
+ },
+ "author": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "responsible": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "assignee": {
+ "href": "/api/v3/users/4",
+ "title": "Nirved Mishra"
+ },
+ "version": {
+ "href": null
+ },
+ "parent": {
+ "href": null,
+ "title": null
+ },
+ "customActions": [],
+ "meetings": {
+ "href": "/work_packages/37/tabs/meetings",
+ "title": "meetings"
+ },
+ "github": {
+ "href": "/work_packages/37/tabs/github",
+ "title": "github"
+ },
+ "github_pull_requests": {
+ "href": "/api/v3/work_packages/37/github_pull_requests",
+ "title": "GitHub pull requests"
+ },
+ "gitlab": {
+ "href": "/work_packages/37/tabs/gitlab",
+ "title": "gitlab"
+ },
+ "gitlab_merge_requests": {
+ "href": "/api/v3/work_packages/37/gitlab_merge_requests",
+ "title": "Gitlab merge requests"
+ },
+ "gitlab_issues": {
+ "href": "/api/v3/work_packages/37/gitlab_issues",
+ "title": "Gitlab Issues"
+ },
+ "convertBCF": {
+ "href": "/api/bcf/2.1/projects/demo-project/topics",
+ "title": "Convert to BCF",
+ "payload": {
+ "reference_links": [
+ "/api/v3/work_packages/37"
+ ]
+ },
+ "method": "post"
+ }
+ }
+ }
+ }
diff --git a/zerver/webhooks/openproject/tests.py b/zerver/webhooks/openproject/tests.py
new file mode 100644
index 0000000000..c06f13e263
--- /dev/null
+++ b/zerver/webhooks/openproject/tests.py
@@ -0,0 +1,102 @@
+from zerver.lib.test_classes import WebhookTestCase
+
+
+class OpenProjectHookTests(WebhookTestCase):
+ CHANNEL_NAME = "OpenProjectUpdates"
+ URL_TEMPLATE = "/api/v1/external/openproject?api_key={api_key}&stream={stream}"
+ WEBHOOK_DIR_NAME = "openproject"
+ STREAM_NAME = "OpenProjectUpdates"
+
+ def test_project_with_parent_created(self) -> None:
+ expected_topic = "AI Backend"
+ expected_message = (
+ "Project **AI Backend** was created as a sub-project of **Demo project**."
+ )
+
+ self.check_webhook(
+ "project_created__with_parent",
+ expected_topic,
+ expected_message,
+ )
+
+ def test_project_without_parent_created(self) -> None:
+ expected_topic = "AI Backend"
+ expected_message = "Project **AI Backend** was created."
+
+ self.check_webhook(
+ "project_created__without_parent",
+ expected_topic,
+ expected_message,
+ )
+
+ def test_project_updated(self) -> None:
+ expected_topic = "AI Backend"
+ expected_message = "Project **AI Backend** was updated."
+ self.check_webhook(
+ "project_updated",
+ expected_topic,
+ expected_message,
+ )
+
+ def test_work_package_created(self) -> None:
+ expected_topic = "Demo project"
+ expected_message = "**Task1** (work package **Task**) was created by **Nirved Mishra**."
+ self.check_webhook(
+ "work_package_created",
+ expected_topic,
+ expected_message,
+ )
+
+ def test_work_package_updated(self) -> None:
+ expected_topic = "Demo project"
+ expected_message = "**Task1** (work package **Task**) was updated by **Nirved Mishra**."
+ self.check_webhook(
+ "work_package_updated",
+ expected_topic,
+ expected_message,
+ )
+
+ def test_time_entry_with_workpackage_created(self) -> None:
+ expected_topic = "Project1"
+ expected_message = "**Nirved Mishra** logged **1 hour** on **kl**."
+ self.check_webhook(
+ "time_entry_created__with_workpackage",
+ expected_topic,
+ expected_message,
+ )
+
+ def test_time_entry_without_workpackage_created(self) -> None:
+ expected_topic = "Project1"
+ expected_message = "**Nirved Mishra** logged **1 hour** on **Project1**."
+ self.check_webhook(
+ "time_entry_created__without_workpackage",
+ expected_topic,
+ expected_message,
+ )
+
+ def test_time_entry_with_iso_hm(self) -> None:
+ expected_topic = "Project1"
+ expected_message = "**Nirved Mishra** logged **7 hours and 42 minutes** on **kl**."
+ self.check_webhook(
+ "time_entry_created__with_iso_hm",
+ expected_topic,
+ expected_message,
+ )
+
+ def test_time_entry_with_invalid_iso(self) -> None:
+ expected_topic = "Project1"
+ expected_message = "**Nirved Mishra** logged a time entry on **kl**."
+ self.check_webhook(
+ "time_entry_created__with_invalid_iso",
+ expected_topic,
+ expected_message,
+ )
+
+ def test_attachment_created(self) -> None:
+ expected_topic = "Project 2"
+ expected_message = "**Nirved Mishra** uploaded **a.out** in **task2**."
+ self.check_webhook(
+ "attachment_created",
+ expected_topic,
+ expected_message,
+ )
diff --git a/zerver/webhooks/openproject/view.py b/zerver/webhooks/openproject/view.py
new file mode 100644
index 0000000000..fec202b475
--- /dev/null
+++ b/zerver/webhooks/openproject/view.py
@@ -0,0 +1,123 @@
+import regex as re
+from django.http import HttpRequest, HttpResponse
+
+from zerver.decorator import webhook_view
+from zerver.lib.exceptions import UnsupportedWebhookEventTypeError
+from zerver.lib.response import json_success
+from zerver.lib.typed_endpoint import JsonBodyPayload, typed_endpoint
+from zerver.lib.validator import WildValue, check_string
+from zerver.lib.webhooks.common import check_send_webhook_message
+from zerver.models import UserProfile
+
+ALL_EVENT_TYPES: list[str] = [
+ "project:created",
+ "project:updated",
+ "work_package:created",
+ "work_package:updated",
+ "time_entry:created",
+ "attachment:created",
+]
+
+PROJECT_MESSAGE_TEMPLATE = "Project **{name}** was {action}{parent_project_message}."
+
+WORK_PACKAGE_MESSAGE_TEMPLATE = (
+ "**{subject}** (work package **{type}**) was {action} by **{author}**."
+)
+
+ATTACHMENT_MESSAGE_TEMPLATE = "**{author}** uploaded **{filename}** in **{container_name}**."
+
+TIME_ENTRY_MESSAGE_TEMPLATE = "**{user}** logged {duration_message}{parent_message}."
+
+
+def format_duration(iso_duration: str) -> str:
+ duration = re.fullmatch(
+ r"P(?:(?P\d+)D)?(?:T(?:(?P\d+)H)?(?:(?P\d+)M)?(?:(?P\d+)S)?)?",
+ iso_duration,
+ )
+ if duration is None:
+ raise ValueError(f"Invalid ISO 8601 duration format: {iso_duration}")
+
+ time_units = ["days", "hours", "minutes", "seconds"]
+ formatted_duration = [
+ f"{int(duration.group(unit))} {unit[:-1] if int(duration.group(unit)) == 1 else unit}"
+ for unit in time_units
+ if duration.group(unit)
+ ]
+
+ if len(formatted_duration) > 1:
+ return ", ".join(formatted_duration[:-1]) + " and " + formatted_duration[-1]
+ return formatted_duration[0]
+
+
+@webhook_view("OpenProject", all_event_types=ALL_EVENT_TYPES)
+@typed_endpoint
+def api_openproject_webhook(
+ request: HttpRequest,
+ user_profile: UserProfile,
+ *,
+ payload: JsonBodyPayload[WildValue],
+) -> HttpResponse:
+ event_type: str = payload["action"].tame(check_string)
+ item, action = event_type.split(":")
+ action_data: WildValue = payload[item]
+
+ if item == "project":
+ parent_project_message: str = ""
+ parent_project: str = (
+ action_data.get("_embedded", {}).get("parent", {}).get("name", "").tame(check_string)
+ )
+ if parent_project and action == "created":
+ parent_project_message = f" as a sub-project of **{parent_project}**"
+ message = PROJECT_MESSAGE_TEMPLATE.format(
+ name=action_data["name"].tame(check_string),
+ action=action,
+ parent_project_message=parent_project_message,
+ )
+ topic = action_data["name"].tame(check_string)
+ elif item == "work_package":
+ message = WORK_PACKAGE_MESSAGE_TEMPLATE.format(
+ subject=action_data["subject"].tame(check_string),
+ type=action_data["_embedded"]["type"]["name"].tame(check_string),
+ action=action,
+ author=action_data["_embedded"]["author"]["name"].tame(check_string),
+ )
+ topic = action_data["_embedded"]["project"]["name"].tame(check_string)
+ elif item == "attachment":
+ message = ATTACHMENT_MESSAGE_TEMPLATE.format(
+ filename=action_data["fileName"].tame(check_string),
+ author=action_data["_embedded"]["author"]["name"].tame(check_string),
+ container_name=action_data["_embedded"]["container"]["subject"].tame(check_string),
+ )
+ topic = action_data["_embedded"]["container"]["_links"]["project"]["title"].tame(
+ check_string
+ )
+ elif item == "time_entry":
+ parent_message: str = (
+ f" on **{action_data['_embedded']['project']['name'].tame(check_string)}**"
+ )
+ workpackage: str = (
+ action_data.get("_embedded", {})
+ .get("workPackage", {})
+ .get("subject", "")
+ .tame(check_string)
+ )
+ if workpackage:
+ parent_message = f" on **{workpackage}**"
+
+ try:
+ formatted_duration = format_duration(action_data["hours"].tame(check_string))
+ duration_message = f"**{formatted_duration}**"
+ except ValueError:
+ duration_message = "a time entry"
+
+ message = TIME_ENTRY_MESSAGE_TEMPLATE.format(
+ duration_message=duration_message,
+ user=action_data["_embedded"]["user"]["name"].tame(check_string),
+ parent_message=parent_message,
+ )
+ topic = action_data["_embedded"]["project"]["name"].tame(check_string)
+ else:
+ raise UnsupportedWebhookEventTypeError(event_type)
+
+ check_send_webhook_message(request, user_profile, topic, message)
+ return json_success(request)