Skip to content

Commit 8a5a7a5

Browse files
authored
Merge pull request #1 from ENCODE-DCC/dev
initial commit
2 parents 40c4560 + f02523e commit 8a5a7a5

9 files changed

Lines changed: 1461 additions & 2 deletions

File tree

README.md

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,11 @@
1-
# google-sheet-metadata-submitter
2-
ENCODE metadata submitter based on Google Sheet and Apps script
1+
## ENCODE metadata submitter
2+
3+
Metadata manager based on Google Sheet + Google Apps Script
4+
5+
6+
## Google Sheet
7+
8+
Make a copy of this document.
9+
10+
https://docs.google.com/spreadsheets/d/1mmTsrT4tnD4fRAf7nkdduq810nZvMQxZy0ga4Z_zK74/edit?usp=sharing
11+

src/Connection.gs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
const PROPERTY_USERNAME = "username";
2+
const PROPERTY_PASSWORD = "password";
3+
4+
5+
function getUsername() {
6+
var userProperties = PropertiesService.getUserProperties();
7+
return userProperties.getProperty(PROPERTY_USERNAME);
8+
}
9+
10+
function setUsername(username) {
11+
var userProperties = PropertiesService.getUserProperties();
12+
return userProperties.setProperty(PROPERTY_USERNAME, username);
13+
}
14+
15+
function getPassword() {
16+
var userProperties = PropertiesService.getUserProperties();
17+
return userProperties.getProperty(PROPERTY_PASSWORD);
18+
}
19+
20+
function setPassword(password) {
21+
var userProperties = PropertiesService.getUserProperties();
22+
return userProperties.setProperty(PROPERTY_PASSWORD, password);
23+
}
24+
25+
function makeAuthHeaders(username, password) {
26+
return {"Authorization" : "Basic " + Utilities.base64Encode(username + ":" + password)};
27+
}
28+
29+
function restGet(url) {
30+
var params = {"method" : "GET", "contentType": "application/json", "muteHttpExceptions": true};
31+
var username = getUsername();
32+
var password = getPassword();
33+
if (username && password) {
34+
params["headers"] = makeAuthHeaders(username, password);
35+
}
36+
return UrlFetchApp.fetch(url, params);
37+
}
38+
39+
function restPut(url, payloadJson, method="PUT") {
40+
var params = {"method" : method, "contentType": "application/json", "muteHttpExceptions": true};
41+
var username = getUsername();
42+
var password = getPassword();
43+
if (username && password) {
44+
params["headers"] = makeAuthHeaders(username, password);
45+
}
46+
params["payload"] = JSON.stringify(payloadJson);
47+
return UrlFetchApp.fetch(url, params);
48+
}
49+
50+
function restPost(url, payloadJson) {
51+
return restPut(url, payloadJson, method="POST");
52+
}

src/Library.gs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
const DEBUG = false;
2+
const DEBUG_AUTO_YES = false;
3+
// const DEBUG = true;
4+
// const DEBUG_AUTO_YES = true;
5+
6+
7+
function getType(p) {
8+
if (Array.isArray(p)) return "array";
9+
else if (typeof p == "string") return "string";
10+
else if (typeof p == "number") return "number";
11+
else if (p != null && typeof p == "object") return "object";
12+
else return "other";
13+
}
14+
15+
function last(array) {
16+
return array[array.length - 1];
17+
}
18+
19+
function toBoolean(val) {
20+
var s = String(val).toLowerCase();
21+
return ["1", "true", "t", "o"].includes(s);
22+
}
23+
24+
function isArrayString(str) {
25+
var trimmed = str.trim();
26+
return trimmed.startsWith("[") && trimmed.endsWith("]");
27+
}
28+
29+
function isJsonString(str) {
30+
var trimmed = str.trim();
31+
return trimmed.startsWith("{") && trimmed.endsWith("}");
32+
}
33+
34+
function trimTrailingDot(str) {
35+
return str.replace(/\.$/, "");
36+
}
37+
38+
function trimTrailingSlash(str) {
39+
return str.replace(/\/+$/, "");
40+
}
41+
42+
function snakeToCamel(snake) {
43+
return snake.toLowerCase().replace(/([-_][a-z])/g,
44+
group => group.toUpperCase().replace('-', '').replace('_', '')
45+
);
46+
}
47+
48+
function capitalizeWord(word) {
49+
return word[0].toUpperCase() + word.substr(1);
50+
}
51+
52+
function alertBoxOkCancel(prompt) {
53+
return SpreadsheetApp.getUi().alert(
54+
prompt, SpreadsheetApp.getUi().ButtonSet.OK_CANCEL
55+
) === SpreadsheetApp.getUi().Button.OK;
56+
}
57+
58+
function alertBox(prompt) {
59+
SpreadsheetApp.getUi().alert(prompt);
60+
}
61+
62+
function getCurrentLocalTimeString(sep="-") {
63+
// returns current time string with all special characters
64+
// replaced with `sep`
65+
var d = new Date();
66+
d = new Date(d.getTime() - d.getTimezoneOffset() * 60000);
67+
return d.toISOString().replace(/T/g,sep).replace(/\:/g,sep).replace(/Z/g,'') .replace(/\..*/g,'');
68+
}
69+
70+
// https://stackoverflow.com/a/47098533/8819536
71+
function openUrl( url ){
72+
var html = HtmlService.createHtmlOutput('<html><script>'
73+
+'window.close = function(){window.setTimeout(function(){google.script.host.close()},9)};'
74+
+'var a = document.createElement("a"); a.href="'+url+'"; a.target="_blank";'
75+
+'if(document.createEvent){'
76+
+' var event=document.createEvent("MouseEvents");'
77+
+' if(navigator.userAgent.toLowerCase().indexOf("firefox")>-1){window.document.body.append(a)}'
78+
+' event.initEvent("click",true,true); a.dispatchEvent(event);'
79+
+'}else{ a.click() }'
80+
+'close();'
81+
+'</script>'
82+
// Offer URL as clickable link in case above code fails.
83+
+'<body style="word-break:break-word;font-family:sans-serif;">Failed to open automatically. <a href="'+url+'" target="_blank" onclick="window.close()">Click here to proceed</a>.</body>'
84+
+'<script>google.script.host.setHeight(40);google.script.host.setWidth(410)</script>'
85+
+'</html>')
86+
.setWidth( 90 ).setHeight( 1 );
87+
SpreadsheetApp.getUi().showModalDialog( html, "Opening ..." );
88+
}

src/Metadata.gs

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
const HELP_TEXT_INDENT = 2;
2+
const EXPORTED_JSON_INDENT = 2;
3+
const HEADER_COMMENTED_PROP_SKIP = "#skip";
4+
const HEADER_COMMENTED_PROP_ERROR = "#error";
5+
const HEADER_PROP_ACCESSION = "accession";
6+
const HEADER_PROP_UUID = "uuid";
7+
const DEFAULT_PROP_PRIORITY = [
8+
HEADER_COMMENTED_PROP_SKIP, HEADER_COMMENTED_PROP_ERROR,
9+
HEADER_PROP_ACCESSION, HEADER_PROP_UUID, "aliases", "award", "lab",
10+
];
11+
const PRIORITY_INDENTIFYING_PROP = [HEADER_PROP_ACCESSION, HEADER_PROP_UUID];
12+
const DEFAULT_EXPORTED_JSON_FILE_PREFIX = "encode-metadata-submitter.exported";
13+
const TOOLTIP_FOR_PROP_SKIP = "Dry-run any REST actions (GET/PUT/POST)\n\nIf recent REST action is successful (200 or 201) then it is automatically set as 1 to prevent duplicate submission/retrieval.";
14+
const TOOLTIP_FOR_PROP_ERROR = "Recent REST action + HTTP error code + JSON response\n\n-200: Successful.\n-201: Successfully POSTed.\n-409: Found a conflict when POSTing\n";
15+
16+
17+
function getTooltipForCommentedProp(prop) {
18+
if (prop === HEADER_COMMENTED_PROP_SKIP) {
19+
return TOOLTIP_FOR_PROP_SKIP;
20+
}
21+
else if(prop === HEADER_COMMENTED_PROP_ERROR) {
22+
return TOOLTIP_FOR_PROP_ERROR;
23+
}
24+
}
25+
26+
function getNumMetadataInSheet(sheet) {
27+
return getLastRow(sheet) - HEADER_ROW;
28+
}
29+
30+
function makeMetadataUrl(method, profileName, endpoint, identifyingVal) {
31+
switch(method) {
32+
case "GET":
33+
return `${endpoint}/${profileName}/${identifyingVal}/?format=json&frame=edit`;
34+
case "PUT":
35+
return `${endpoint}/${profileName}/${identifyingVal}`;
36+
case "POST":
37+
return `${endpoint}/${profileName}`;
38+
default:
39+
Logger.log("makeMetadataUrl: Not supported method " + method);
40+
}
41+
}
42+
43+
function getMetadataFromPortal(identifyingVal, identifyingProp, profileName, endpoint) {
44+
var url = makeMetadataUrl("GET", profileName, endpoint, identifyingVal);
45+
var response = restGet(url);
46+
var error = response.getResponseCode();
47+
48+
var object = {
49+
[HEADER_COMMENTED_PROP_ERROR]: "GET" + "," + error,
50+
// [HEADER_PROP_GET_URL]: url,
51+
[HEADER_COMMENTED_PROP_SKIP]: 0,
52+
[identifyingProp]: identifyingVal
53+
};
54+
55+
var responseJson = JSON.parse(response.getContentText());
56+
if (error === 200) {
57+
// if no error, merge parsed JSON to row object
58+
object[HEADER_COMMENTED_PROP_SKIP] = 1;
59+
object = {...object, ...responseJson};
60+
}
61+
else {
62+
// if error, write helpText to provide debugging information
63+
object[HEADER_COMMENTED_PROP_ERROR] += "\n" + JSON.stringify(responseJson, null, HELP_TEXT_INDENT);
64+
}
65+
return object;
66+
}
67+
68+
function getSortedProps(props, profile, propPriority=DEFAULT_PROP_PRIORITY) {
69+
// sort metadata's props by given profile and propPriority
70+
// - props in propPriority come first if exists
71+
// - and then props under required key in profile come next
72+
// - all the other commented (#) props come last
73+
var sortedProps = [];
74+
75+
// priority props first
76+
for (var prop of propPriority.concat(profile["required"])) {
77+
if (props.includes(prop) && !sortedProps.includes(prop)) {
78+
sortedProps.push(prop);
79+
}
80+
}
81+
82+
// non-commented props
83+
for (var prop of props) {
84+
if (!prop.startsWith("#") && !sortedProps.includes(prop)) {
85+
sortedProps.push(prop);
86+
}
87+
}
88+
89+
// and then commented props
90+
for (var prop of props) {
91+
if (prop.startsWith("#") && !sortedProps.includes(prop)) {
92+
sortedProps.push(prop);
93+
}
94+
}
95+
return sortedProps;
96+
}
97+
98+
99+
getIdentifyingPropForProfile
100+
101+
function updateSheetWithMetadataFromPortal(sheet, profileName, endpointForGet, endpointForProfile) {
102+
var profile = getProfile(profileName, endpointForProfile);
103+
if (!profile) {
104+
Logger.log(`Couldn't find a profile schema ${profile} for profile name ${profileName}.`)
105+
return 0;
106+
}
107+
108+
var identifyingProp = getIdentifyingPropForProfile(profile);
109+
if (!identifyingProp) {
110+
Logger.log(`Could't find identifying property ${identifyingProp} in header row ${HEADER_ROW}.`);
111+
return -1;
112+
}
113+
var identifyingCol = findColumnByHeaderValue(sheet, identifyingProp);
114+
115+
// also check #skip column exists. if so skip row with #skip===1
116+
var skipCol = findColumnByHeaderValue(sheet, HEADER_COMMENTED_PROP_SKIP);
117+
118+
// update each row if has accession value
119+
var numUpdated = 0;
120+
for (var row = HEADER_ROW + 1; row <= getLastRow(sheet); row++) {
121+
var identifyingVal = getCellValue(sheet, row, identifyingCol);
122+
if (skipCol && toBoolean(getCellValue(sheet, row, skipCol))) {
123+
continue;
124+
}
125+
if (identifyingVal) {
126+
var metadataObj = getMetadataFromPortal(
127+
identifyingVal, identifyingProp, profileName, endpointForGet
128+
);
129+
var sortedProps = getSortedProps(Object.keys(metadataObj), profile);
130+
writeJsonToRow(sheet, metadataObj, row, sortedProps);
131+
numUpdated++;
132+
}
133+
}
134+
return numUpdated;
135+
}
136+
137+
function exportSheetToJsonFile(sheet, profileName, endpointForProfile, keepCommentedProps, jsonFilePath) {
138+
var profile = getProfile(profileName, endpointForProfile);
139+
140+
var result = [];
141+
for (var row = HEADER_ROW + 1; row <= getLastRow(sheet); row++) {
142+
var jsonBeforeTypeCast = rowToJson(
143+
sheet, row, keepCommentedProps=false, bypassGoogleAutoParsing=true
144+
);
145+
var json = typeCastJsonValuesByProfile(profile, jsonBeforeTypeCast);
146+
result.push(json);
147+
}
148+
DriveApp.createFile(jsonFilePath, JSON.stringify(result, null, EXPORTED_JSON_INDENT));
149+
}
150+
151+
function putSheetToPortal(sheet, profileName, endpointForPut, endpointForProfile, method="PUT") {
152+
// returns actual number of submitted rows
153+
var profile = getProfile(profileName, endpointForProfile);
154+
var identifyingProp = getIdentifyingPropForProfile(profile);
155+
156+
const numData = getNumMetadataInSheet(sheet);
157+
var numSubmitted = 0;
158+
159+
for (var row = HEADER_ROW + 1; row <= numData + HEADER_ROW; row++) {
160+
var jsonBeforeTypeCast = rowToJson(
161+
sheet, row, keepCommentedProps=true, bypassGoogleAutoParsing=true
162+
);
163+
164+
// if has #skip and it is 1 then skip
165+
if (jsonBeforeTypeCast.hasOwnProperty(HEADER_COMMENTED_PROP_SKIP)) {
166+
if (toBoolean(jsonBeforeTypeCast[HEADER_COMMENTED_PROP_SKIP])) {
167+
continue;
168+
}
169+
}
170+
171+
var json = typeCastJsonValuesByProfile(
172+
profile, jsonBeforeTypeCast, keepCommentedProps=false
173+
);
174+
175+
switch(method) {
176+
case "PUT":
177+
178+
var url = makeMetadataUrl(method, profileName, endpointForPut, json[identifyingProp]);
179+
var response = restPut(url, payloadJson=json);
180+
break;
181+
182+
case "POST":
183+
var url = makeMetadataUrl(method, profileName, endpointForPut);
184+
var response = restPost(url, payloadJson=json);
185+
break;
186+
187+
default:
188+
Logger.log("putSheetToPortal: Wrong REST method " + method);
189+
continue;
190+
}
191+
192+
var error = response.getResponseCode();
193+
var responseJson = JSON.parse(response.getContentText());
194+
195+
json[HEADER_COMMENTED_PROP_ERROR] = method + "," + error;
196+
197+
switch(error) {
198+
case 200:
199+
json[HEADER_COMMENTED_PROP_SKIP] = 1;
200+
break;
201+
202+
case 201:
203+
var identifyingVal = responseJson["@graph"][0][identifyingProp];
204+
json[identifyingProp] = identifyingVal;
205+
json[HEADER_COMMENTED_PROP_ERROR] += "\n" + JSON.stringify(responseJson, null, HELP_TEXT_INDENT);
206+
json[HEADER_COMMENTED_PROP_SKIP] = 1;
207+
break;
208+
209+
default:
210+
json[HEADER_COMMENTED_PROP_ERROR] += "\n" + JSON.stringify(responseJson, null, HELP_TEXT_INDENT);
211+
json[HEADER_COMMENTED_PROP_SKIP] = 0;
212+
}
213+
214+
// rewrite data, with commented headers such as error and text, on the sheet
215+
writeJsonToRow(sheet, json, row);
216+
numSubmitted++;
217+
}
218+
return numSubmitted;
219+
}
220+
221+
function postSheetToPortal(sheet, profileName, endpointForPost, endpointForProfile) {
222+
// returns actual number of submitted rows
223+
return putSheetToPortal(sheet, profileName, endpointForPost, endpointForProfile, method="POST");
224+
}
225+
226+
function addTemplateMetadataToSheet(sheet, profile) {
227+
var metadataObj = makeTemplateMetadataObjectFromProfile(profile);
228+
var sortedProps = getSortedProps(Object.keys(metadataObj), profile);
229+
addJsonToSheet(sheet, metadataObj, sortedProps);
230+
}

0 commit comments

Comments
 (0)