27 Nov 2019

Get number of BIM 360 projects per user

There are several ways of extracting data from BIM 360, especially when it comes to reports. One common request is the number or list of Document Manager (Docs) projects per user. This is possible with a combination with BIM 360 API and Data Management API. Let's get that information and create a basic bar chart with it.

Here are the steps:

  1. Start with a 2 legged access token, make sure to use account:read and data:read scopes.
  2. GET users of the BIM 360 Account, this returns a complete list of users on the account.
  3. Foreach user, GET projects using the user.uid at the x-user-id header, which tells the Data Management to impersonate that user.

That's it, actually. We just need some basic JSON manipulation to cross-reference the users from #2 with the projects from #3. But there are a few tricks: depending on how many users on the account, GET projects will likely be rate-limited, so we would need a basic "wait and retry" code. There are many ways to handle it, for this article let's use a very simple approach.

Let me demontrate it with a C# sample code:

GET users: just name and uid, assuming US region and getting all pages (100 per page)

public async Task<JArray> GetUsers(string hubId, string accessToken, JArray users = null, int offset = 0)
{
    users = (users == null ? new JArray() : users);
    RestClient client = new RestClient(BASE_URL);
    RestRequest request = new RestRequest("/hq/v1/accounts/{account_id}/users?limit={limit}&offset={offset}&field=name,uid", RestSharp.Method.GET);
    request.AddParameter("account_id", hubId.Replace("b.", string.Empty), ParameterType.UrlSegment);
    request.AddParameter("limit", 100);
    request.AddParameter("offset", offset);
    request.AddHeader("Authorization", "Bearer " + accessToken);
    IRestResponse response = await client.ExecuteTaskAsync(request);
    if (response.StatusCode != HttpStatusCode.OK) return null;
    JArray page = JArray.Parse(response.Content);
    users.Merge(page);
    if (page.Count >= 100) await GetUsers(hubId, accessToken, users, offset += 100);
    return users;
}

GET projects: there is a big chance this will be rate-limited, so let's retry after the specified value (Retry-After header)

public async Task<JObject> GetProjectsAsync(string hubId, string userId, string accessToken, int attempt = 0)
{
    RestClient client = new RestClient(BASE_URL);
    RestRequest request = new RestRequest("project/v1/hubs/{hubId}/projects", RestSharp.Method.GET);
    request.AddParameter("hubId", hubId, ParameterType.UrlSegment);
    request.AddHeader("Authorization", "Bearer " + accessToken);
    request.AddHeader("x-user-id", userId);
    IRestResponse response = await client.ExecuteTaskAsync(request);
    if (response.StatusCode == HttpStatusCode.OK)
        return JObject.Parse(response.Content);
    else if (response.StatusCode == HttpStatusCode.TooManyRequests)
    {
        if (attempt > 5) return null;
        int retryAfter = int.Parse(response.Headers.ToList().Find(h => h.Name == "Retry-After").Value.ToString());
        System.Threading.Thread.Sleep(retryAfter * 1000);
        return await GetProjectsAsync(hubId, userId, accessToken, ++attempt);
    }
    return null;
}

Now let's expose as an endpoint that takes the account_id (or hub_id) and return the information. To make our life easier, let's sort by number of projets. This piece assumes your code has a GetAppSettings method that returns your client ID & secret.

[HttpGet]
[Route("api/forge/bim360/hubs/{hubId}/users")]
public async Task<JArray> GetProjectByUser(string hubId)
{
    TwoLeggedApi oauth = new TwoLeggedApi();
    dynamic bearer = await oauth.AuthenticateAsync(GetAppSetting("FORGE_CLIENT_ID"), GetAppSetting("FORGE_CLIENT_SECRET"), "client_credentials", new Scope[] { Scope.AccountRead, Scope.DataRead });
    JArray users = await GetUsers(hubId, bearer.access_token);
    foreach (dynamic user in users)
    {
        user.projects = new JArray();
        user.projects_count = 0;

        if (user.uid == null) continue; // not activated yet
        dynamic projects = await GetProjectsAsync(hubId, (string)user.uid, (string)bearer.access_token);
        if (projects == null) continue;
        foreach (dynamic project in projects.data)
        {
            dynamic projectInfo = new JObject();
            projectInfo.name = project.attributes.name;
            projectInfo.id = project.id;
            user.projects.Add(projectInfo);
        }
        user.projects_count = ((JArray)user.projects).Count;
    }

    return new JArray(users.OrderByDescending(u => (int)u["projects_count"]));
}

Finally, for the UI, let's use Chartjs to draw a bar chart:

<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/2.8.0/Chart.bundle.min.js"></script>
<script>
    $(document).ready(function () {
        jQuery.ajax({
            url: '/api/forge/bim360/hubs/YOUR_ACCOUNT_ID_HERE/users',
            success: function (users) {
                var labels = [];
                var data = [];
                users.forEach(function (user) {
                    if (user.projects_count <= 3) return;
                    labels.push(user.name);
                    data.push(user.projects_count);
                });

                var context = document.getElementById('projectsPerUser').getContext('2d');
                var barChart = new Chart(context, {
                    type: 'bar',
                    data: {
                        labels: labels,
                        datasets: [{
                            label: 'Projects',
                            data: data,
                            backgroundColor: getRandomColor(data.length)
                        }]
                    },
                    options: {
                        responsive: true,
                        legend: {
                            position: 'top',
                        },
                        title: {
                            display: true,
                            text: 'BIM 360 user statistics'
                        },
                        scales: {
                            yAxes: [{
                                ticks: {
                                    beginAtZero: true
                                }
                            }]
                        }
                    }
                });
            }
        });
    });

    function getRandomColor(count) {
        var colors = [];
        for (var i = 0; i < count; i++) {
            var r = Math.round(Math.random() * 255); var g = Math.round(Math.random() * 255); var b = Math.round(Math.random() * 255);
            colors.push('rgba(' + r + ', ' + g + ', ' + b + ', 0.2)');
        }
        return colors;
    }
</script>
<div style="width:100%;">
    <canvas id="projectsPerUser"></canvas>
</div>

That's it. 

Tags:

Related Article