Skip to content

Commit

Permalink
Docstore: Initial version of per-element comments
Browse files Browse the repository at this point in the history
  • Loading branch information
bensummers committed Jan 12, 2018
1 parent 9c97fbf commit 58271b8
Show file tree
Hide file tree
Showing 15 changed files with 556 additions and 6 deletions.
5 changes: 5 additions & 0 deletions COPYRIGHT
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ Tablesort.js
std_reporting/static/tablesort.js
https://github.com/tristen/tablesort
MIT License

Font Awesome (comment icon)
std_document_store/static/comment.svg
http://fontawesome.io/
http://fontawesome.io/license/
41 changes: 40 additions & 1 deletion example_usage/js/egu_forms.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,43 @@ var form0 = P.form({
label:"Description",
required:true
},
{
type:"text",
path:"something",
label:"Another note",
},
{
"type": "repeating-section",
"path": "researchLocation",
"label": "If your research fieldwork takes place outside of the UK, please state the location.",
"allowAdd": true,
"allowDelete": true,
"elements": [
{ "type": "text", "path": "region", "label": "Region" },
{ "type": "choice", "path": "country", "label": "Country",
"choices": [
["GB", "United Kingdom"],
["AD", "Andorra"],
["AX", "Åland Islands"],
["AF", "Afghanistan"],
["AL", "Albania"],
["DZ", "Algeria"],
["AS", "American Samoa"],
["AO", "Angola"],
["AI", "Anguilla"],
["AQ", "Antarctica"],
["AG", "Antigua and Barbuda"],
["AR", "Argentina"]
]
}
]
},
{
"type": "date",
"path": "startDate",
"label": "Project start date",
"required": true
},
{
type:"boolean",
path:"secondForm",
Expand Down Expand Up @@ -55,5 +92,7 @@ P.ExampleUsageWorkflow.use("std:document_store", {
return true;
},
view: [{}],
edit: [{roles:["user"], selector:{state:"wait_submit"}}]
edit: [{roles:["user"], selector:{state:"wait_submit"}}],
addComment: [{}],
viewComments: [{}]
});
76 changes: 76 additions & 0 deletions std_document_store/js/docstore_comments.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@

// checkPermissions called with key & action, where action is 'addComment' or 'viewComments'
P.implementService("std:document_store:comments:respond", function(E, docstore, key, checkPermissions) {
E.response.kind = 'json';

// Check permission
if(!checkPermissions(key, (E.request.method === "POST") ? 'addComment' : 'viewComments')) {
E.response.body = JSON.stringify({result:"error",message:"Permission denied"});
E.response.statusCode = HTTP.UNAUTHORIZED;
return;
}

var instance = docstore.instance(key);
var response = {result:"success"};

if(E.request.method === "POST") {
// Add a comment for this user
var version = parseInt(E.request.parameters.version,10),
formId = E.request.parameters.form,
elementUName = E.request.parameters.uname,
comment = E.request.parameters.comment;
if(!(version && formId && elementUName && comment && (formId.length < 200) && (elementUName.length < 200) && (comment.length < 131072))) {
response.result = "error";
response.method = "Bad parameters";
} else {
var row = docstore.commentsTable.create({
keyId: instance.keyId,
version: version,
userId: O.currentUser.id,
datetime: new Date(),
formId: formId,
elementUName: elementUName,
comment: comment
});
row.save();
response.comment = rowForClient(row);
response.commentUserName = O.currentUser.name;
}

} else {
// Return all comments for this document
var users = {}, forms = {};
var allComments = docstore.commentsTable.select().
where("keyId","=",instance.keyId).
order("datetime", true); // latest comments first
var onlyCommentsForForm = E.request.parameters.onlyform;
if(onlyCommentsForForm) { allComments.where("formId","=",onlyCommentsForForm); }
_.each(allComments, function(row) {
var form = forms[row.formId];
if(!form) { form = forms[row.formId] = {}; }
var comments = form[row.elementUName];
if(!comments) { comments = form[row.elementUName] = []; }
comments.push(rowForClient(row));
var uid = row.userId;
if(!users[uid]) {
users[uid] = O.user(uid).name;
}
});
response.users = users;
response.forms = forms;
}

E.response.body = JSON.stringify(response);
});

// --------------------------------------------------------------------------

var rowForClient = function(row) {
return {
id: row.id,
uid: row.userId,
version: row.version,
datetime: (new XDate(row.datetime)).toString("dd MMM yyyy HH:mm"),
comment: row.comment
};
};
16 changes: 15 additions & 1 deletion std_document_store/js/docstore_instance.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,14 @@ DocumentInstance.prototype.__defineGetter__("committedDocumentIsComplete", funct
}
});

DocumentInstance.prototype.__defineGetter__("committedVersionNumber", function() {
var latest = this.store.versionsTable.select().
where("keyId","=",this.keyId).
order("version", true).
limit(1);
return latest.length ? latest[0].version : undefined;
});

DocumentInstance.prototype._notifyDelegate = function(fn) {
var delegate = this.store.delegate;
if(delegate[fn]) {
Expand Down Expand Up @@ -185,7 +193,7 @@ DocumentInstance.prototype._displayForms = function(document) {
};

// Render as document
DocumentInstance.prototype._renderDocument = function(document, deferred, idPrefix) {
DocumentInstance.prototype._renderDocument = function(document, deferred, idPrefix, requiresUNames) {
var html = [];
var delegate = this.store.delegate;
var key = this.key;
Expand All @@ -194,6 +202,7 @@ DocumentInstance.prototype._renderDocument = function(document, deferred, idPref
idPrefix = idPrefix || '';
_.each(forms, function(form) {
var instance = form.instance(document);
if(requiresUNames) { instance.setIncludeUniqueElementNamesInHTML(true); }
if(delegate.prepareFormInstance) {
delegate.prepareFormInstance(key, form, instance, "document");
}
Expand Down Expand Up @@ -254,6 +263,7 @@ DocumentInstance.prototype.handleEditDocument = function(E, actions) {
var instance = this,
delegate = this.store.delegate,
cdocument = this.currentDocument,
requiresUNames = !!actions.viewComments,
forms,
pages, isSinglePage,
activePage;
Expand All @@ -266,6 +276,7 @@ DocumentInstance.prototype.handleEditDocument = function(E, actions) {
for(var i = 0; i < forms.length; ++i) {
var form = forms[i],
formInstance = form.instance(cdocument);
if(requiresUNames) { formInstance.setIncludeUniqueElementNamesInHTML(true); }
if(!delegate.shouldEditForm || delegate.shouldEditForm(instance.key, form, cdocument) || actions._showAllForms) {
if(delegate.prepareFormInstance) {
delegate.prepareFormInstance(instance.key, form, formInstance, "form");
Expand Down Expand Up @@ -381,6 +392,9 @@ DocumentInstance.prototype.handleEditDocument = function(E, actions) {
}
actions.render(this, E, P.template("edit").deferredRender({
isSinglePage: isSinglePage,
viewComments: actions.viewComments,
commentsUrl: actions.commentsUrl,
versionForComments: actions.viewComments ? this.committedVersionNumber : undefined,
saveButtonStyle: saveButtonStyle,
navigation: navigation,
showFormTitles: actions.showFormTitlesWhenEditing,
Expand Down
15 changes: 15 additions & 0 deletions std_document_store/js/docstore_store.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,21 @@ var DocumentStore = P.DocumentStore = function(P, delegate) {
// Keep references to the databases
this.currentTable = P.db[currentDbName];
this.versionsTable = P.db[versionsDbName];
// Create comments table, if in use by this document store
this.enablePerElementComments = delegate.enablePerElementComments;
if(this.enablePerElementComments) {
var commentsDbName = "dsComment"+dbNameFragment;
P.db.table(commentsDbName, {
keyId: { type:delegate.keyIdType || "int", indexed:true, indexedWith:"datetime" },
version: { type:"bigint" },
userId: { type:"int" }, // don't use 'user' to avoid unnecessary creation of user objects
datetime: { type:"datetime"},
formId: { type:"text" },
elementUName: { type:"text" },
comment: { type:"text" }
});
this.commentsTable = P.db[commentsDbName];
}
};

// ----------------------------------------------------------------------------
Expand Down
33 changes: 31 additions & 2 deletions std_document_store/js/docstore_viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
// version - version number to display (overrides version)
// showVersions - true to allow user to select a version to view
// showCurrent - allow the user to see the current version
// viewComments - show comments in this viewer
// addComment - user is allowed to add comments
// commentsUrl - path of comment server (required if viewComments or addComment is true)
// hideFormNavigation - hide the interform links from the sidebar
// uncommittedChangesWarningText - specify (or disable) the uncommitted changes warning text
// style - specify the style of the viewer
Expand All @@ -25,7 +28,8 @@ var DocumentViewer = P.DocumentViewer = function(instance, E, options) {
if("version" in this.options) {
this.version = this.options.version;
} else if(this.options.showVersions && ("version" in E.request.parameters)) {
this.version = parseInt(E.request.parameters.version, 10);
var vstr = E.request.parameters.version;
this.version = (vstr === '') ? undefined : parseInt(vstr,10);
}

// Requested change?
Expand Down Expand Up @@ -104,6 +108,31 @@ var DocumentViewer = P.DocumentViewer = function(instance, E, options) {
this.showChangesFromDocument = JSON.parse(requestedPrevious[0].json);
}

// Commenting? (but only if we're not showing changes)
if(!(this.showChangesFrom) && (this.options.viewComments || this.options.addComment)) {
this.requiresComments = true;
if(!this.options.commentsUrl) {
throw new Error("viewComments or addComment used in docstore viewer, but commentsUrl not specified");
}
this.versionForComments = this.version;
if(!this.versionForComments) {
this.versionForComments = instance.committedVersionNumber;
}
if(!this.versionForComments) {
this.requiresComments = false; // disable if there isn't a committed version yet, so won't have comments anyway
}
// Don't want to clutter up display of final versions, so comments can be turned off
if(this.requiresComments && this.options.hideCommentsByDefault && (E.request.parameters.comments !== "1")) {
this.requiresComments = false; // just turn it all off
var numberOfComments = store.commentsTable.select().
where("keyId","=",instance.keyId).
count();
if(numberOfComments > 0) {
this.couldShowNumberOfComments = numberOfComments;
}
}
}

// Get any additional UI to display
var delegate = this.instance.store.delegate;
if(delegate.getAdditionalUIForViewer) {
Expand Down Expand Up @@ -141,7 +170,7 @@ DocumentViewer.prototype.__defineGetter__("_viewerBody", function() {
});

DocumentViewer.prototype.__defineGetter__("_viewerDocumentDeferred", function() {
return this.instance._renderDocument(this.document, true);
return this.instance._renderDocument(this.document, true, undefined, this.requiresComments /* so needs unames */);
});

DocumentViewer.prototype.__defineGetter__("_viewerSelectedForm", function() {
Expand Down
47 changes: 45 additions & 2 deletions std_document_store/js/docstore_workflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ P.use("std:workflow");
// property allows the history to be viewable by everyone
// view: [{roles:[],selector:{}}, ...] - when the document can be viewed
// (omit roles key to mean everyone)
// viewDraft: [{roles:[],selector:{}}, ...] - when drafts of the document can be viewed
// edit: [{roles:[],selector:{},transitionsFiltered:[]},optional:true, ...] - when the document
// can be edited, the (optional) transitionsFiltered property specifies
// which transitions should only be avaialble if the form has been
// edited & completed, the optional property overrides the default that,
// when a user is allowed to edit a document, there must be a committed
// version before they can transition
// addComment: [{roles:[],selector:{}}, ...] - OPTIONAL, when a user can comment on the forms
// viewComments: [{roles:[],selector:{}}, ...] - OPTIONAL, when a user can view the comments
// hideCommentsWhen: selector - OPTIONAL, defaults to {closed:true}
// ----------
// actionableUserMustReview: (selector) - a selector which specifies when the
// current actionable user should be shown the completed document and
Expand All @@ -45,6 +49,8 @@ P.use("std:workflow");

// ----------------------------------------------------------------------------

var DEFAULT_HIDE_COMMENTS_WHEN = {closed:true};

var Delegate = function() { };
Delegate.prototype = {
__formSubmissionDoesNotCompleteProcess: true,
Expand Down Expand Up @@ -134,6 +140,12 @@ P.workflow.registerWorkflowFeature("std:document_store", function(workflow, spec
}

var delegate = _.extend(new Delegate(), spec);

// The 'addComment' permission implies that per element comments are needed
if(spec.addComment) {
delegate.enablePerElementComments = true;
}

var docstore = plugin.defineDocumentStore(delegate);
if(!("documentStore" in workflow)) {
workflow.documentStore = {};
Expand Down Expand Up @@ -278,7 +290,13 @@ P.workflow.registerWorkflowFeature("std:document_store", function(workflow, spec
O.stop("Not permitted.");
}
var instance = docstore.instance(M);
instance.handleEditDocument(E, editor);
var configuredEditor = editor;
if(delegate.enablePerElementComments) {
configuredEditor = Object.create(editor);
configuredEditor.viewComments = can(M, O.currentUser, spec, 'viewComments');
configuredEditor.commentsUrl = spec.path+"/comments/"+M.workUnit.id;
}
instance.handleEditDocument(E, configuredEditor);
});

// ------------------------------------------------------------------------
Expand Down Expand Up @@ -324,7 +342,9 @@ P.workflow.registerWorkflowFeature("std:document_store", function(workflow, spec
E.setResponsiblePlugin(P); // take over as source of templates, etc
var instance = docstore.instance(M);
var ui = instance.makeViewerUI(E, {
showCurrent: true
showCurrent: true,
viewComments: delegate.enablePerElementComments && can(M, O.currentUser, spec, 'viewComments'),
commentsUrl: delegate.enablePerElementComments ? spec.path+"/comments/"+M.workUnit.id : undefined
});
// std:ui:choose
var text = M.getTextMaybe("docstore-review-prompt:"+spec.name) ||
Expand Down Expand Up @@ -369,6 +389,8 @@ P.workflow.registerWorkflowFeature("std:document_store", function(workflow, spec
var ui = instance.makeViewerUI(E, {
showVersions: spec.history ? can(M, O.currentUser, spec, 'history') : true,
showCurrent: true,
viewComments: delegate.enablePerElementComments && can(M, O.currentUser, spec, 'viewComments'),
commentsUrl: delegate.enablePerElementComments ? spec.path+"/comments/"+M.workUnit.id : undefined,
uncommittedChangesWarningText: M.getTextMaybe("docstore-draft-warning-text:"+
spec.name) || "This is a draft version"
});
Expand Down Expand Up @@ -398,6 +420,10 @@ P.workflow.registerWorkflowFeature("std:document_store", function(workflow, spec
var ui = instance.makeViewerUI(E, {
showVersions: spec.history ? can(M, O.currentUser, spec, 'history') : true,
showCurrent: canEdit,
addComment: delegate.enablePerElementComments && can(M, O.currentUser, spec, 'addComment'),
viewComments: delegate.enablePerElementComments && can(M, O.currentUser, spec, 'viewComments'),
commentsUrl: delegate.enablePerElementComments ? spec.path+"/comments/"+M.workUnit.id : undefined,
hideCommentsByDefault: delegate.enablePerElementComments ? M.selected(spec.hideCommentsByDefault||DEFAULT_HIDE_COMMENTS_WHEN) : true,
uncommittedChangesWarningText: M.getTextMaybe("docstore-uncommitted-changes-warning-text:"+
spec.name)
});
Expand All @@ -417,6 +443,23 @@ P.workflow.registerWorkflowFeature("std:document_store", function(workflow, spec

// ----------------------------------------------------------------------

if(delegate.enablePerElementComments) {

var checkPermissions = function(M, action) {
return can(M, O.currentUser, spec, action);
};

plugin.respond("GET,POST", spec.path+'/comments', [
{pathElement:0, as:"workUnit", workType:workflow.fullName, allUsers:true}
], function(E, workUnit) {
var M = workflow.instance(workUnit);
O.service("std:document_store:comments:respond", E, docstore, M, checkPermissions);
});

}

// ----------------------------------------------------------------------

plugin.respond("GET,POST", spec.path+'/admin', [
{pathElement:0, as:"workUnit", workType:workflow.fullName, allUsers:true}
], function(E, workUnit) {
Expand Down
1 change: 1 addition & 0 deletions std_document_store/js/std_document_store.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// Delegate has properties:
// name - of store (short string) - REQUIRED
// keyIdType - type of keyId, if not "int"
// enablePerElementComments - true if users can comment on individual elements in form
// __formSubmissionDoesNotCompleteProcess -- (internal) if true, UI adjusted with expectation submitting form is not last step
// TODO: public API for __formSubmissionDoesNotCompleteProcess equivalent
// Delegate has methods:
Expand Down
Loading

0 comments on commit 58271b8

Please sign in to comment.