Published: Mar 2, 2024 by Isaac Johnson
In part 2 of our deep dive on OpenProject I’ll cover the SaaS (Cloud) offering, the Time and Accounting area (with custom cost types), custom fields with REST, the Meetings system, and more.
SaaS
We can signup for a 14-day trial
I’ll confirm the email to get started
I’m now launched into my instance with the demo projects already set up
Something I liked was being able to create a wiki using markdown
I’ll write up a wiki
This looks good
But if I click the Markdown button again, we can see it converted the table to HTML
So it looks good, but would be hard to edit over time
I’ll explore a few premium features such as custom logo and theme (I like light)
Let’s add a custom field. I can add an optional date field
When saved, we see the note that it isn’t usable until we add to a work package type
In Work Package Types, we can see the Work Package (Work Item) types already defined
I’ll “+Type” to add a brand new type for BlogPosts
You can see how we add the fields to various screens in the “form configuration” pane
Lastly, I’ll want to add it to the existing projects
Now, when I go to create a new Work Package, I can see the “BlogPosts” type
And there is a Date picker enabled for the field
I should add that just as in other fields, markdown renders to HTML on save, however, to be fair, they have a nice table editor inline which is really where I would find the HTML annoying if I had to mess with data tables
I want to cover Page Size and Offset on API Queries because I glossed over them (as do their docs). By default, you get 20 work packages back on a work package query. This isn’t very helpful when you get 30 some “demo” work packages already. Put simply, all of your “new work” is in page 2.
You can pass in offset (starting number) or a different page size as a GET parameter. For instance, to query 50 (instead of 20) results, I would use:
$ curl -X GET -H 'Content-Type: application/json' -H 'Accept: application/json' -u "apikey:92xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx6" https://freshbrewed.openproject.com/api/v3/work_packages?pageSize=50 | jq | more
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 172k 100 172k 0 0 153k 0 0:00:01 0:00:01 --:--:-- 153k
{
"_type": "WorkPackageCollection",
"total": 33,
"count": 33,
"pageSize": 50,
"offset": 1,
"_embedded": {
"elements": [
{
"derivedStartDate": "2024-02-13",
"derivedDueDate": "2024-02-27",
"_type": "WorkPackage",
"id": 2,
...snip...
As you can imagine, I was curious how my custom field would be represented in the JSON output
{
"derivedStartDate": null,
"derivedDueDate": null,
"_type": "WorkPackage",
"id": 37,
"lockVersion": 1,
"subject": "My Great Idea",
"description": {
"format": "markdown",
"raw": "A Blog Post or writeup idea\n\n| author | bio link |\n| --- | --- | \n| Isaac | noc.social/ijohnson |\n\nstuff",
"html": "<p class=\"op-uc-p\">A Blog Post or writeup idea</p>\n<figure class=\"op-uc-figure\"><div class=\"op-uc-figure--content\"><table class=\"op-uc-table\">\n<thead class=\"op-uc-table--head\">\n<tr class=\"op-uc-table--row\">\n<th class=\"op-uc-table--cell op-uc-table--cell_head\">author</th>\n<th class=\"op-uc-table--cell op-uc-table--cell_head\">bio link</th>\n</tr>\n</thead>\n<tbody>\n<tr class=\"op-uc-table--row\">\n<td class=\"op-uc-table--cell\">Isaac</td>\n<td class=\"op-uc-table--cell\">noc.social/ijohnson</td>\n</tr>\n</tbody>\n</table></div></figure>\n<p class=\"op-uc-p\">stuff</p>"
},
"scheduleManually": false,
"startDate": null,
"dueDate": null,
"estimatedTime": null,
"derivedEstimatedTime": null,
"remainingTime": null,
"derivedRemainingTime": null,
"duration": null,
"ignoreNonWorkingDays": false,
"percentageDone": 0,
"createdAt": "2024-02-13T12:15:46.793Z",
"updatedAt": "2024-02-13T12:15:46.822Z",
"readonly": false,
"customField1": "2024-02-13",
"_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"
},
"update": {
"href": "/api/v3/work_packages/37/form",
"method": "post"
},
"schema": {
"href": "/api/v3/work_packages/schemas/1-8"
},
"updateImmediately": {
"href": "/api/v3/work_packages/37",
"method": "patch"
},
"delete": {
"href": "/api/v3/work_packages/37",
"method": "delete"
},
"move": {
"href": "/work_packages/37/move/new",
"type": "text/html",
"title": "Move My Great Idea"
},
"copy": {
"href": "/work_packages/37/copy",
"title": "Copy My Great Idea"
},
"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/8/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"
},
"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 My Great Idea"
},
"changeParent": {
"href": "/api/v3/work_packages/37",
"method": "patch",
"title": "Change parent of My Great Idea"
},
"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"
},
"category": {
"href": null
},
"type": {
"href": "/api/v3/types/8",
"title": "BlogPosts"
},
"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": "Isaac Johnson"
},
"responsible": {
"href": null
},
"assignee": {
"href": null
},
"version": {
"href": null
},
"meetings": {
"href": "/work_packages/37/tabs/meetings",
"title": "meetings"
},
"github_pull_requests": {
"href": "/api/v3/work_packages/37/github_pull_requests",
"title": "GitHub pull requests"
},
"self": {
"href": "/api/v3/work_packages/37",
"title": "My Great Idea"
},
"unwatch": {
"href": "/api/v3/work_packages/37/watchers/4",
"method": "delete"
},
"ancestors": [],
"parent": {
"href": null,
"title": null
},
"customActions": []
}
}
There it is, customfield1:
"customField1": "2024-02-13",
"configureForm": {
"href": "/types/8/edit?tab=form_configuration",
"type": "text/html",
"title": "Configure form"
},
"type": {
"href": "/api/v3/types/8",
"title": "BlogPosts"
},
I’m not really sure exactly how we get from the type of “8” to the customField1
I tried to initially POST this JSON
$ cat test.json | jq
{
"subject": "API Work Package with date",
"description": {
"format": "markdown",
"raw": "#A new test post",
"html": "<p class=\"op-uc-p\">A new test post</p>"
},
"scheduleManually": false,
"startDate": null,
"dueDate": null,
"estimatedTime": null,
"customField1": "2024-02-20",
"_links": {
"customFields": {
"href": "/projects/demo-project/settings/custom_fields",
"type": "text/html",
"title": "Custom fields"
},
"configureForm": {
"href": "/types/8/edit?tab=form_configuration",
"type": "text/html",
"title": "Configure form"
},
"type": {
"href": "/api/v3/types/8",
"title": "BlogPosts"
},
"author": {
"href": "/api/v3/users/4",
"title": "Isaac Johnson"
}
}
}
But was rejected due to missing project field and blocked trying to manually set author:
{"_type":"Error","errorIdentifier":"urn:openproject-org:api:v3:errors:MultipleErrors","message":"Multiple field constraints have been violated.","_embedded":{"errors":[{"_type":"Error","errorIdentifier":"urn:openproject-org:api:v3:errors:PropertyConstraintViolation","message":"Project can't be blank.","_embedded":{"details":{"attribute":"project"}}},{"_type":"Error","errorIdentifier":"urn:openproject-org:api:v3:errors:PropertyIsReadOnly","message":"Author was attempted to be written but is not writable.","_embedded":{"details":{"attribute":"author"}}}]}}
I’ll try using the Demo project
$ curl -X GET -H 'Content-Type: application/json' -H 'Accept: application/json' -u "apikey:9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx6" https://freshbrewed.openproject.com/api/v3/projects | jq | grep -C 5 "\"id\": 1"
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 4165 100 4165 0 0 7011 0 --:--:-- --:--:-- --:--:-- 7000
}
}
},
{
"_type": "Project",
"id": 1,
"identifier": "demo-project",
"name": "Demo project",
"active": true,
"public": true,
"description": {
The URL for the POST will include the Project ID. The following worked:
$ cat test.json
{
"subject": "API Work Package with date",
"description": {
"format": "markdown",
"raw": "#A new test post",
"html": "<p class=\"op-uc-p\">A new test post</p>"
},
"scheduleManually": false,
"startDate": null,
"dueDate": null,
"estimatedTime": null,
"customField1": "2024-02-20",
"_links": {
"customFields": {
"href": "/projects/demo-project/settings/custom_fields",
"type": "text/html",
"title": "Custom fields"
},
"configureForm": {
"href": "/types/8/edit?tab=form_configuration",
"type": "text/html",
"title": "Configure form"
},
"type": {
"href": "/api/v3/types/8",
"title": "BlogPosts"
}
}
}
$ curl -X POST -H 'Content-Type: application/json' -H 'Accept: application/json' -u "apikey:9xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx6" https://freshbrewed.openproject.com/api/v3/projects/1/work_packages -d @test.json
{"derivedStartDate":null,"derivedDueDate":null,"_embedded":{"attachments":{"_type":"Collection","total":0,"count":0,"_embedded":... snip ...
And I can see the new Work Package the Demo Project
Milestones
One of the nicer features in OpenProject are Milestones
They are a primary type so to use them, create a new milestone from the Work Packages page
I’ll create an initial milestone
I made two more and we can see them on a gantt chart
I noted the UI in the SaaS version (which is newer than my self-hosted) looks a bit nicer
Which also includes a Card view
News
News is a bit of a catch all in that it covers dashboarding and posts. But the advantage is it has an Atom feed as well (for RSS readers)
I can create a new News item by clicking “+ News”
I can then write up a post
The post is now live
If I click that “Atom” link on the bottom, I get the Atom RSS feed URL (https://openproject.freshbrewed.science/projects/myfreshbrewedproject/news.atom?key=7c416c28943ee4ce99c63b3e876a1d024d081e20acad6d22f36212684bfe59bc)
<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>MyFreshBrewedProject: News</title>
<link rel="self" href="https://openproject.freshbrewed.science/projects/myfreshbrewedproject/news"/>
<link rel="alternate" href="https://openproject.freshbrewed.science/projects/myfreshbrewedproject/news"/>
<id>https://openproject.freshbrewed.science/</id>
<updated>2024-02-14T11:57:08Z</updated>
<author>
<name>OpenProject</name>
</author>
<generator uri="https://www.openproject.org/">
OpenProject </generator>
<entry>
<title>Release 0.0.1</title>
<link rel="alternate" href="https://openproject.freshbrewed.science/news/3"/>
<id>https://openproject.freshbrewed.science/news/3</id>
<updated>2024-02-14T11:57:08Z</updated>
<author>
<name>OpenProject Admin</name>
</author>
<content type="html">
<p class="op-uc-p">Some of the features of our first release</p>
<ul class="op-uc-list">
<li class="op-uc-list--item">It kind of works</li>
<li class="op-uc-list--item">Only some bugs instead of many</li>
<li class="op-uc-list--item">Give it a pretty number</li>
</ul> </content>
</entry>
</feed>
Time and Costs
We can do reports against Time spent on projects and tasks. Perhaps I wanted to find out all the “Fresh” work since a date, I could query that in Time and Costs
The “Group By” fields let me tweak the row and column data to fit my needs
And if I export to Excel
I’ll download an XLS with the data (I expected a CSV or XSLX)
By default, the “Cost” value is in Euros, but we can change that in Administration/Time and Costs
Let’s say I want to track other costs. Perhaps we use Bitcoin to pay for some project work.
I could create a new Cost Type
Which I can now see in Cost Types
Now when I look at a project work package, I can “Log unit costs”
And enter the spend
I can see that reflected in the task
And in the Time and Costs report
Since the value of blockchain currency varies, I may just want to total the amount of BTC spent (perhaps we had a pre-allocated pool)
I do not expect, as the example showed, many projects using BTC to buy dark web material, but I do see it as a way to bundle up abstracted finance units.
For instance, very often when working with a Contract house you have per-hour rates and you want to carefully dole them out.
For instance, say I had a DBA firm that will give me the first 5 weeks of Database Administrator time (units) at USD$200 an hour. But after March 31st, that would jump to $550 for “extended” support and we have a note that any work after April 1 is at a rate of $950/hr
Now when I log costs, I can call out we had to pull in the DBA firm for a couple of hours
I can see the combined costs of this project so far in the report in USD
Or DBAs over time
Meetings
We can track meeting notes under “Meetings”. In fact there is a whole notification system with invites and attendees and calendars.
Let’s start with a new Meeting
I’ll start with a Dynamic Kickoff meeting
The idea is we now have a page to collaborate on agenda items
I’ll start by adding an Agenda Item
I’ll write some notes
I’ll build out a few things on the fly
I’ve added some notes
Since someone called out the Wifi in the India office, let’s create a Work Package to look into that
While I cannot create a new WP from the Meetings page
I could create an IT Tasks task on the side
Then I can add it and make a note
Now, under the IT Updates task, we see a new entry in “Meetings” with that comment
As far as meeting actions, there is no real “close”. We can send emails, delete or download an iCal (ics) file
I’m not sure whether to blame Outlook or OpenProject, but the TimeZone information didn’t translate in the ICS
However, once I set my own users time zone in user settings
The meeting times lined up
If I wanted to track attendance, I could note the users who attended the meetings
I can then see any meetings in the past of which I attended
Let’s also look at “Classic” meeting types
This one is virtual so I’ll just put in the normal standup agenda
I can then copy it
and just set a new date for the copy
Provided you keep the same type (Classic), it will copy forward the agenda.
However, if you switch to dynamic, that gets lost
Calendars
First, to use Calendars, we need to enable the option on the project
From there we can create a new calendar that loads from the project
I can click on a task to see details
Or, click on a date to create a new task
On save (again, since this is in the past, it’s in red)
And as you can expect, highlighting days in front of today sets a range
I thought it was a bit odd that in this navigation, save was under the “…” menu:
On save, I can decide how to share
Though I found most of the time, after I saved it, it would not actually show tasks. I tried a few different variations on a filter that should have worked
Expiring SaaS
I was interested to circle back to my SaaS option after the time window expired.
I was able to login and change task status
Though my cheapest option is still over $40/mo
Summary
We covered quite a lot in this second part. We looked at signing up for the SaaS offering and how to use the API to fetch data. We looked at Meetings and all the ways to capture and share notes. In a similar fashion, we looked at News and the built-in Calendar feature. Lastly, we experimented with “Time and Costs” and some notes on the expiring SaaS.
Overall, it’s hard to really put our heads around all that OpenProject can do. Not everything works perfect (such as Calendar), but it’s a very compelling tool and one worth checking out.