From 62a020c95fef0198591cfaa99429a52a2ed4c650 Mon Sep 17 00:00:00 2001 From: Stuart Langridge Date: Sat, 17 Feb 2018 15:53:46 +0000 Subject: [PATCH] Add recent contributors report, and allow reports to require auth to view. Closes #73 --- README.reports.md | 11 ++++ assets/css/main.css | 17 +++++- assets/img/lock.svg | 23 +++++++ lib/reports.js | 14 ++++- php/github-login.php | 4 ++ reports/recentContributors.js | 112 ++++++++++++++++++++++++++++++++++ templates/report.tmpl | 22 +++++++ templates/reportlist.tmpl | 9 ++- 8 files changed, 208 insertions(+), 4 deletions(-) create mode 100644 README.reports.md create mode 100644 assets/img/lock.svg create mode 100644 reports/recentContributors.js diff --git a/README.reports.md b/README.reports.md new file mode 100644 index 0000000..1622dd7 --- /dev/null +++ b/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. diff --git a/assets/css/main.css b/assets/css/main.css index 5dcbed5..2880480 100755 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -140,7 +140,7 @@ body { font-weight: 300; } -section button, section input, section select { +section button, section input, section select, .report select { color: black; } @@ -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; @@ -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); } diff --git a/assets/img/lock.svg b/assets/img/lock.svg new file mode 100644 index 0000000..2bdfeaa --- /dev/null +++ b/assets/img/lock.svg @@ -0,0 +1,23 @@ + +image/svg+xml \ No newline at end of file diff --git a/lib/reports.js b/lib/reports.js index dd84150..88ea6b6 100644 --- a/lib/reports.js +++ b/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'); @@ -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) { @@ -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); @@ -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 }); }) }); diff --git a/php/github-login.php b/php/github-login.php index 4f5b72f..1a5584b 100644 --- a/php/github-login.php +++ b/php/github-login.php @@ -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)); } diff --git a/reports/recentContributors.js b/reports/recentContributors.js new file mode 100644 index 0000000..2008a99 --- /dev/null +++ b/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 '' + o + ''; + }).join("/"); + var contributor = '' + p.login + ""; + return "" + contributor + "" + orglist + + "" + p.yyyymmdd + ""; + }) + + var table = '' + + '\n' + + '\n\n' + + trs.join("\n") + "
ContributorOrganisationsMost recent contribution
"; + + var dropdown = '

Show contributors from last ' + + '

'; + var filter_script = ` + `; + + var html = dropdown + table + filter_script; + + return callback(null, { + title: "Recent Contributors", + html: html, + requires_authentication: true + }) + }) +} \ No newline at end of file diff --git a/templates/report.tmpl b/templates/report.tmpl index 878dffe..b8cd843 100644 --- a/templates/report.tmpl +++ b/templates/report.tmpl @@ -1,5 +1,27 @@ {{> mainlessheader.tmpl}}
+{{#requires_authentication}} +{{title}} {{{html}}} +EOF; +$isok = generic_verify($_COOKIE["MeasureAuth"]); + +if ($isok) { + echo $report; +} else { + echo '

This report is only available after logging in. Please '; + echo ' to view.

'; +} +?> + +{{/requires_authentication}} + +{{^requires_authentication}} +

{{title}}

+ {{{html}}} +{{/requires_authentication}} {{> footer.tmpl}} diff --git a/templates/reportlist.tmpl b/templates/reportlist.tmpl index f982876..c8297cf 100644 --- a/templates/reportlist.tmpl +++ b/templates/reportlist.tmpl @@ -2,7 +2,14 @@