Openproject Part 2: SaaS, Time/Accounting, Meetings and more

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

/content/images/2024/02/openproject-44.png

I’ll confirm the email to get started

/content/images/2024/02/openproject-45.png

I’m now launched into my instance with the demo projects already set up

/content/images/2024/02/openproject-46.png

Something I liked was being able to create a wiki using markdown

/content/images/2024/02/openproject-47.png

I’ll write up a wiki

/content/images/2024/02/openproject-48.png

This looks good

/content/images/2024/02/openproject-49.png

But if I click the Markdown button again, we can see it converted the table to HTML

/content/images/2024/02/openproject-50.png

So it looks good, but would be hard to edit over time

/content/images/2024/02/openproject-51.png

I’ll explore a few premium features such as custom logo and theme (I like light)

/content/images/2024/02/openproject-52.png

Let’s add a custom field. I can add an optional date field

/content/images/2024/02/openproject-53.png

When saved, we see the note that it isn’t usable until we add to a work package type

/content/images/2024/02/openproject-54.png

In Work Package Types, we can see the Work Package (Work Item) types already defined

/content/images/2024/02/openproject-55.png

I’ll “+Type” to add a brand new type for BlogPosts

/content/images/2024/02/openproject-56.png

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

/content/images/2024/02/openproject-58.png

Now, when I go to create a new Work Package, I can see the “BlogPosts” type

/content/images/2024/02/openproject-59.png

And there is a Date picker enabled for the field

/content/images/2024/02/openproject-60.png

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

/content/images/2024/02/openproject-61.png

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

/content/images/2024/02/openproject-62.png

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

/content/images/2024/02/openproject-63.png

I’ll create an initial milestone

/content/images/2024/02/openproject-64.png

I made two more and we can see them on a gantt chart

/content/images/2024/02/openproject-65.png

I noted the UI in the SaaS version (which is newer than my self-hosted) looks a bit nicer

/content/images/2024/02/openproject-66.png

Which also includes a Card view

/content/images/2024/02/openproject-67.png

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”

/content/images/2024/02/openproject-71.png

I can then write up a post

/content/images/2024/02/openproject-72.png

The post is now live

/content/images/2024/02/openproject-73.png

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">
&lt;p class="op-uc-p"&gt;Some of the features of our first release&lt;/p&gt;
&lt;ul class="op-uc-list"&gt;
&lt;li class="op-uc-list--item"&gt;It kind of works&lt;/li&gt;
&lt;li class="op-uc-list--item"&gt;Only some bugs instead of many&lt;/li&gt;
&lt;li class="op-uc-list--item"&gt;Give it a pretty number&lt;/li&gt;
&lt;/ul&gt;    </content>
  </entry>
</feed>

/content/images/2024/02/openproject-74.png

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

/content/images/2024/02/openproject-75.png

The “Group By” fields let me tweak the row and column data to fit my needs

/content/images/2024/02/openproject-76.png

And if I export to Excel

/content/images/2024/02/openproject-77.png

I’ll download an XLS with the data (I expected a CSV or XSLX)

/content/images/2024/02/openproject-78.png

By default, the “Cost” value is in Euros, but we can change that in Administration/Time and Costs

/content/images/2024/02/openproject-79.png

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

/content/images/2024/02/openproject-80.png

Which I can now see in Cost Types

/content/images/2024/02/openproject-81.png

Now when I look at a project work package, I can “Log unit costs”

/content/images/2024/02/openproject-82.png

And enter the spend

/content/images/2024/02/openproject-83.png

I can see that reflected in the task

/content/images/2024/02/openproject-84.png

And in the Time and Costs report

/content/images/2024/02/openproject-85.png

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)

/content/images/2024/02/openproject-86.png

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

/content/images/2024/02/openproject-87.png

Now when I log costs, I can call out we had to pull in the DBA firm for a couple of hours

/content/images/2024/02/openproject-88.png

I can see the combined costs of this project so far in the report in USD

/content/images/2024/02/openproject-89.png

Or DBAs over time

/content/images/2024/02/openproject-90.png

Meetings

/content/images/2024/02/openproject-91.png

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

/content/images/2024/02/openproject-92.png

I’ll start with a Dynamic Kickoff meeting

/content/images/2024/02/openproject-93.png

The idea is we now have a page to collaborate on agenda items

/content/images/2024/02/openproject-94.png

I’ll start by adding an Agenda Item

/content/images/2024/02/openproject-95.png

I’ll write some notes

/content/images/2024/02/openproject-96.png

I’ll build out a few things on the fly

/content/images/2024/02/openproject-97.png

I’ve added some notes

/content/images/2024/02/openproject-98.png

Since someone called out the Wifi in the India office, let’s create a Work Package to look into that

/content/images/2024/02/openproject-99.png

While I cannot create a new WP from the Meetings page

/content/images/2024/02/openproject-100.png

I could create an IT Tasks task on the side

/content/images/2024/02/openproject-101.png

Then I can add it and make a note

/content/images/2024/02/openproject-102.png

Now, under the IT Updates task, we see a new entry in “Meetings” with that comment

/content/images/2024/02/openproject-103.png

As far as meeting actions, there is no real “close”. We can send emails, delete or download an iCal (ics) file

/content/images/2024/02/openproject-104.png

I’m not sure whether to blame Outlook or OpenProject, but the TimeZone information didn’t translate in the ICS

/content/images/2024/02/openproject-105.png

However, once I set my own users time zone in user settings

/content/images/2024/02/openproject-106.png

The meeting times lined up

/content/images/2024/02/openproject-107.png

If I wanted to track attendance, I could note the users who attended the meetings

/content/images/2024/02/openproject-108.png

I can then see any meetings in the past of which I attended

/content/images/2024/02/openproject-109.png

Let’s also look at “Classic” meeting types

/content/images/2024/02/openproject-110.png

This one is virtual so I’ll just put in the normal standup agenda

/content/images/2024/02/openproject-111.png

I can then copy it

/content/images/2024/02/openproject-112.png

and just set a new date for the copy

/content/images/2024/02/openproject-113.png

Provided you keep the same type (Classic), it will copy forward the agenda.

/content/images/2024/02/openproject-114.png

However, if you switch to dynamic, that gets lost

/content/images/2024/02/openproject-115.png

Calendars

First, to use Calendars, we need to enable the option on the project

/content/images/2024/02/openproject-118.png

From there we can create a new calendar that loads from the project

/content/images/2024/02/openproject-119.png

I can click on a task to see details

/content/images/2024/02/openproject-120b.png

Or, click on a date to create a new task

/content/images/2024/02/openproject-121b.png

On save (again, since this is in the past, it’s in red)

/content/images/2024/02/openproject-122.png

And as you can expect, highlighting days in front of today sets a range

/content/images/2024/02/openproject-123.png

I thought it was a bit odd that in this navigation, save was under the “…” menu:

/content/images/2024/02/openproject-124.png

On save, I can decide how to share

/content/images/2024/02/openproject-125.png

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

/content/images/2024/02/openproject-126.png

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

/content/images/2024/02/openproject-116.png

Though my cheapest option is still over $40/mo

/content/images/2024/02/openproject-117.png

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.

Kubernetes Docker Openproject PjM

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes