Skip to content

Commit

Permalink
Add recent contributors report, and allow reports to require auth to …
Browse files Browse the repository at this point in the history
…view. Closes #73
  • Loading branch information
stuartlangridge committed Feb 17, 2018
1 parent a6aaca5 commit 62a020c
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 4 deletions.
11 changes: 11 additions & 0 deletions README.reports.md
@@ -0,0 +1,11 @@
# How to write Measure reports

Measure allows the creation of "reports"; full-page summaries of data. These are very free-form; basically, a report can display whatever it wants without restriction.

## Basic report creation

Your widget is expected to be a Node.js module which exports one function. That function takes two parameters, `options` and `callback`. It should call the callback with an object with `title` and `html` keys. The `title` is displayed as the title of the report, and should be plain text; `html` is a block of HTML which is placed as-is, without escaping, into the body of the report page. Reports are automatically linked from the "Reports" summary page.

## Authentication

A report may contain restricted information. If so, it is possible to restrict the report to be viewed only by authenticated users. To do this, add a `requires_authentication: true` entry to the output object. Note that authentication has to be enabled in `config.yaml`, otherwise the report will not be generated at all (a warning is printed in this situation). It is not possible to restrict a report to _specific_ authenticated users; just authenticated or not.
17 changes: 16 additions & 1 deletion assets/css/main.css
Expand Up @@ -140,7 +140,7 @@ body {
font-weight: 300;
}

section button, section input, section select {
section button, section input, section select, .report select {
color: black;
}

Expand All @@ -154,6 +154,20 @@ section button, section input, section select {
display: block;
}

p.is-signed-in button { color: black; margin: 0 1em; display: inline-block; }

.main.report li a.requires_authentication {
padding-right: 20px;
background: url(../img/lock.svg) center right no-repeat;
background-size: contain;
}

.main.report a.login-required-link {
padding: 0.5em;
background: white;
color: black;
}

.main.report table th {
color: white;
font-weight: 500;
Expand Down Expand Up @@ -369,6 +383,7 @@ https://thenounproject.com/latyshevaoksana/collection/vending-machine/?i=880796
https://thenounproject.com/search/?q=error&i=560230
https://thenounproject.com/search/?q=search&i=591162
https://thenounproject.com/search/?q=close&i=392991
https://thenounproject.com/term/lock/424/
*/
nav + nav li a.nav-repos::before { background-image: url(../img/Dashboards.svg); height: 18px; width: 18px; }
nav + nav li a.nav-orgs::before { background-image: url(../img/Groups.svg); }
Expand Down
23 changes: 23 additions & 0 deletions assets/img/lock.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 12 additions & 2 deletions lib/reports.js
@@ -1,6 +1,7 @@
const fs = require('fs');
const glob = require('glob');
const path = require('path');
const wrap = require('word-wrap');

const utils = require('./utils');
const NICE_ERRORS = require('./nice_errors');
Expand Down Expand Up @@ -40,6 +41,12 @@ function executeReportModule(mod, options, in_params) {
console.warn("Skipping report which threw an error", err, mod.filename);
return resolve(null);
}
if (result.requires_authentication && !options.userConfig.authentication) {
console.warn(wrap("WARNING: we skipped the report '" + result.title +
"' because it requires authentication to view and there is no " +
"authentication key defined in config.yaml.", {width:65}));
return resolve(null);
}
return resolve({result: result, module: mod});
});
} catch(e) {
Expand All @@ -50,7 +57,8 @@ function executeReportModule(mod, options, in_params) {
}
function writeReport(result, options) {
return new Promise((resolve, reject) => {
const outputFile = path.join(options.userConfig.output_directory, "reports", result.module.name + ".html");
var extension = result.result.requires_authentication ? "php" : "html";
const outputFile = path.join(options.userConfig.output_directory, "reports", result.module.name + "." + extension);
var tmplvars = Object.assign(result.result, {isReport: true, subtitle: result.result.title, pageTitle: result.result.title});
options.templates.report(tmplvars, (err, output) => {
if (err) return reject(err);
Expand All @@ -61,7 +69,9 @@ function writeReport(result, options) {
}
return resolve({
slug: result.module.name,
title: result.result.title
title: result.result.title,
extension: extension,
requires_authentication: result.result.requires_authentication
});
})
});
Expand Down
4 changes: 4 additions & 0 deletions php/github-login.php
Expand Up @@ -67,6 +67,10 @@ function do_code() {
$inorg = FALSE;
}
$auth = github_create_token($userdata["data"]->login, $at);

// set the auth token as a cookie
setcookie("MeasureAuth", $auth);
// and return it so it can be set in JS
showError(signed_in_with("GitHub", $userdata["data"]->login, $auth));
}

Expand Down
112 changes: 112 additions & 0 deletions reports/recentContributors.js
@@ -0,0 +1,112 @@
/*
"Recent Contributors report"
Report with name, org, and email address of everyone who's made a
contribution in the last ___ where ___ is a dropdown with say
week/month/quarter/year. Should not show if not authenticated.
(https://github.com/MeasureOSS/Measure/issues/73)
*/
var moment = require("moment");
var async = require("async");

module.exports = function(options, callback) {
var people2Org = {};
for (var orgname in options.org2People) {
options.org2People[orgname].forEach(p => {
if (p.left != "") return;
if (!people2Org[p.login]) people2Org[p.login] = new Set();
people2Org[p.login].add(orgname);
});
}

var mostRecentUserAction = {};

options.db.issue.find({}, {"user.login": 1, "closed_by.login": 1, created_at: 1, closed_at: 1}).toArray().then(issues => {
issues.forEach(function(i) {
if (i.closed_at) {
var ca = moment(i.closed_at);
var mr = mostRecentUserAction[i.closed_by.login];
if (mr) {
mr = moment(mr);
if (ca.isAfter(mr)) {
mostRecentUserAction[i.closed_by.login] = ca;
}
} else {
mostRecentUserAction[i.closed_by.login] = ca;
}
}

var oa = moment(i.created_at);
var mr = mostRecentUserAction[i.user.login];
if (mr) {
mr = moment(mr);
if (oa.isAfter(mr)) {
mostRecentUserAction[i.user.login] = oa;
}
} else {
mostRecentUserAction[i.user.login] = oa;
}
})

var peopleList = [];
for (var k in mostRecentUserAction) {
peopleList.push({
login: k,
date: mostRecentUserAction[k],
yyyymmdd: mostRecentUserAction[k].format("YYYY-MM-DD"),
org: people2Org[k] || [],
ts: mostRecentUserAction[k].unix()
})
}
peopleList.sort(function(b, a) {
return a.ts - b.ts;
});

var trs = peopleList.map(function(p) {
var orglist = Array.from(p.org).map(function(o) {
return '<a href="' + options.url("org", o) + '">' + o + '</a>';
}).join("/");
var contributor = '<a href="' + options.url("contributor", p.login) + '">' + p.login + "</a>";
return "<tr><td>" + contributor + "</td><td>" + orglist +
"</td><td sorttable_customkey='"+p.ts+"'>" + p.yyyymmdd + "</td></tr>";
})

var table = '<table id="report_t" class="sortable">' +
'<thead>\n<tr><th>Contributor</th><th>Organisations</th>' +
'<th>Most recent contribution</th></tr>\n</thead>\n<tbody>' +
trs.join("\n") + "<tbody></table>";

var dropdown = '<p>Show contributors from last ' +
'<select id="report_dd" onchange="report_filter()"><option value="365">year</option>' +
'<option value="90">quarter</option><option value="30">month</option>' +
'<option value="7">week</option></select></p>';
var filter_script = `<script>
var report_dd = document.getElementById("report_dd");
var report_t = document.getElementById("report_t");
function report_filter() {
var days = report_dd.options[report_dd.selectedIndex].value;
var ts = new Date().getTime();
var then = ts - (days * 24 * 60 * 60 * 1000);
var then_yyyymmdd = (new Date(then)).toISOString().substring(0, 10);
console.log("looking for dates bigger than", then_yyyymmdd);
Array.prototype.slice.call(report_t.rows).forEach(function(r) {
var dval = r.cells[2].textContent;
if (dval > then_yyyymmdd) {
r.style.display = ""
} else {
r.style.display = "none";
}
});
}
report_filter();
</script>
`;

var html = dropdown + table + filter_script;

return callback(null, {
title: "Recent Contributors",
html: html,
requires_authentication: true
})
})
}
22 changes: 22 additions & 0 deletions templates/report.tmpl
@@ -1,5 +1,27 @@
{{> mainlessheader.tmpl}}
<div class="main wrapper clearfix report">
{{#requires_authentication}}
<?php
require("../authlist.php");

$report = <<<EOF
<h1>{{title}}</h1>
{{{html}}}
EOF;
$isok = generic_verify($_COOKIE["MeasureAuth"]);

if ($isok) {
echo $report;
} else {
echo '<p>This report is only available after logging in. Please ';
echo '<a href="../login.php" class="login-required-link">log in</a> to view.</p>';
}
?>

{{/requires_authentication}}

{{^requires_authentication}}
<h1>{{title}}</h1>
{{{html}}}
{{/requires_authentication}}
{{> footer.tmpl}}
9 changes: 8 additions & 1 deletion templates/reportlist.tmpl
Expand Up @@ -2,7 +2,14 @@
<div class="main wrapper clearfix report">
<ul>
{{#reports}}
<li><a href="$$BASEURL$$/reports/{{slug}}.html">{{title}}</a></li>
<li>
<a
href="$$BASEURL$$/reports/{{slug}}.{{extension}}"
{{#requires_authentication}}
class="requires_authentication"
{{/requires_authentication}}
>{{title}}</a>
</li>
{{/reports}}
</ul>
</div>
Expand Down

0 comments on commit 62a020c

Please sign in to comment.