Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create the DataObject View (Step 4 of issue #1758) #2521

Merged
merged 5 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
175 changes: 84 additions & 91 deletions src/js/models/SolrResult.js
robyngit marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <object-shorthand> reported by reviewdog 🐶
Expected method shorthand.

getInfo: function (fields) {
var model = this;
if (!fields)
var fields =
"abstract,id,seriesId,fileName,resourceMap,formatType,formatId,obsoletedBy,isDocumentedBy,documents,title,origin,keywords,attributeName,pubDate,eastBoundCoord,westBoundCoord,northBoundCoord,southBoundCoord,beginDate,endDate,dateUploaded,archived,datasource,replicaMN,isAuthorized,isPublic,size,read_count_i,isService,serviceTitle,serviceEndpoint,serviceOutput,serviceDescription,serviceType,project,dateModified";
var escapeSpecialChar = MetacatUI.appSearchModel.escapeSpecialChar;
var query = "q=";
//If there is no seriesId set, then search for pid or sid
if (!this.get("seriesId"))
query +=
'(id:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'" OR seriesId:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'")';
//If a seriesId is specified, then search for that
else if (this.get("seriesId") && this.get("id").length > 0)
query +=
'(seriesId:"' +
escapeSpecialChar(encodeURIComponent(this.get("seriesId"))) +
'" AND id:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'")';
//If only a seriesId is specified, then just search for the most recent version
else if (this.get("seriesId") && !this.get("id"))
query +=
'seriesId:"' +
escapeSpecialChar(encodeURIComponent(this.get("id"))) +
'" -obsoletedBy:*';
query +=
"&fl=" +
fields + //Specify the fields to return
"&wt=json&rows=1000" + //Get the results in JSON format and get 1000 rows
"&archived=archived:*"; //Get archived or unarchived content
var requestSettings = {
url: MetacatUI.appModel.get("queryServiceUrl") + query,
type: "GET",
success: function (data, response, xhr) {
//If the Solr response was not as expected, trigger and error and exit
if (!data || typeof data.response == "undefined") {
model.set("indexed", false);
model.trigger("getInfoError");
return;
}
var docs = data.response.docs;
if (docs.length == 1) {
docs[0].resourceMap = model.parseResourceMapField(docs[0]);
model.set(docs[0]);
model.trigger("sync");
}
//If we searched by seriesId, then let's find the most recent version in the series
else if (docs.length > 1) {
//Filter out docs that are obsoleted
var mostRecent = _.reject(docs, function (doc) {
return typeof doc.obsoletedBy !== "undefined";
});
//If there is only one doc that is not obsoleted (the most recent), then
// set this doc's values on this model
if (mostRecent.length == 1) {
mostRecent[0].resourceMap = model.parseResourceMapField(
mostRecent[0],
);
model.set(mostRecent[0]);
model.trigger("sync");
} else {
//If there are multiple docs without an obsoletedBy statement, then
// retreive the head of the series via the system metadata
var sysMetaRequestSettings = {
url:
MetacatUI.appModel.get("metaServiceUrl") +
encodeURIComponent(docs[0].seriesId),
type: "GET",
success: function (sysMetaData) {
//Get the identifier node from the system metadata
var seriesHeadID = $(sysMetaData).find("identifier").text();
//Get the doc from the Solr results with that identifier
var seriesHead = _.findWhere(docs, { id: seriesHeadID });
//If there is a doc in the Solr results list that matches the series head id
if (seriesHead) {
seriesHead.resourceMap =
model.parseResourceMapField(seriesHead);
//Set those values on this model
model.set(seriesHead);
}
//Otherwise, just fall back on the first doc in the list
else if (mostRecent.length) {
mostRecent[0].resourceMap = model.parseResourceMapField(
mostRecent[0],
);
model.set(mostRecent[0]);
} else {
docs[0].resourceMap = model.parseResourceMapField(
docs[0],
);
model.set(docs[0]);
}
model.trigger("sync");
},
error: function (xhr, textStatus, errorThrown) {
// Fall back on the first doc in the list
if (mostRecent.length) {
model.set(mostRecent[0]);
} else {
model.set(docs[0]);
}
model.trigger("sync");
},
};
$.ajax(
_.extend(
sysMetaRequestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
}
} else {
model.set("indexed", false);
//Try getting the system metadata as a backup
model.getSysMeta();
}
},
error: function (xhr, textStatus, errorThrown) {
model.set("indexed", false);
model.trigger("getInfoError");
},
};
$.ajax(
_.extend(
requestSettings,
MetacatUI.appUserModel.createAjaxSettings(),
),
);
},

Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
define(["jquery", "underscore", "backbone"], ($, _, Backbone) => {
/**
* @class SolrResult
* @classdesc A single result from the Solr search service
* @classcategory Models
* @extends Backbone.Model
*/
robyngit marked this conversation as resolved.
Show resolved Hide resolved
var SolrResult = Backbone.Model.extend(
const SolrResult = Backbone.Model.extend(
/** @lends SolrResult.prototype */ {
// This model contains all of the attributes found in the SOLR 'docs' field inside of the SOLR response element
defaults: {
Expand Down Expand Up @@ -278,103 +278,96 @@ define(["jquery", "underscore", "backbone"], function ($, _, Backbone) {
);
},

/*
* This method will download this object while sending the user's auth token in the request.
/**
* Download this object while sending the user's auth token in the
* request.
*/
downloadWithCredentials: function () {
//if(this.get("isPublic")) return;

//Get info about this object
var url = this.get("url"),
model = this;

//Create an XHR
var xhr = new XMLHttpRequest();
async downloadWithCredentials() {
const model = this;

//Open and send the request with the user's auth token
xhr.open("GET", url);
// Call the new getBlob method and handle the response
const response = await this.fetchDataObjectWithCredentials();
const blob = await response.blob();
const filename = this.getFileNameFromResponse(response);

if (MetacatUI.appUserModel.get("loggedIn")) xhr.withCredentials = true;

//When the XHR is ready, create a link with the raw data (Blob) and click the link to download
xhr.onload = function () {
if (this.status == 404) {
this.onerror.call(this);
return;
}

//Get the file name to save this file as
var filename = xhr.getResponseHeader("Content-Disposition");

if (!filename) {
filename =
model.get("fileName") ||
model.get("title") ||
model.get("id") ||
"download";
} else
filename = filename
.substring(filename.indexOf("filename=") + 9)
.replace(/"/g, "");

//Replace any whitespaces
filename = filename.trim().replace(/ /g, "_");

//For IE, we need to use the navigator API
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(xhr.response, filename);
}
//Other browsers can download it via a link
else {
var a = document.createElement("a");
a.href = window.URL.createObjectURL(xhr.response); // xhr.response is a blob

// Set the file name.
a.download = filename;

a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}

model.trigger("downloadComplete");

// Track this event
MetacatUI.analytics?.trackEvent(
"download",
"Download DataONEObject",
model.get("id"),
);
};

xhr.onerror = function (e) {
model.trigger("downloadError");
// For IE, we need to use the navigator API
if (navigator && navigator.msSaveOrOpenBlob) {
navigator.msSaveOrOpenBlob(blob, filename);
} else {
// Other browsers can download it via a link
const a = document.createElement("a");
a.href = window.URL.createObjectURL(blob);
a.download = filename;
a.style.display = "none";
document.body.appendChild(a);
a.click();
a.remove();
}

// Track the error
MetacatUI.analytics?.trackException(
`Download DataONEObject error: ${e || ""}`,
model.get("id"),
true,
);
};
// Track this event
model.trigger("downloadComplete");
MetacatUI.analytics?.trackEvent(
"download",
"Download DataONEObject",
model.get("id"),
);
},

xhr.onprogress = function (e) {
if (e.lengthComputable) {
var percent = (e.loaded / e.total) * 100;
model.set("downloadPercent", percent);
/**
* This method will fetch this object while sending the user's auth token
* in the request. The data can then be downloaded or displayed in the
* browser
* @returns {Promise} A promise that resolves when the data is fetched
* @since 0.0.0
*/
fetchDataObjectWithCredentials() {
const url = this.get("url");
const token = MetacatUI.appUserModel.get("token") || "";
const method = "GET";

return new Promise((resolve, reject) => {
const headers = {};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
};

xhr.responseType = "blob";

if (MetacatUI.appUserModel.get("loggedIn"))
xhr.setRequestHeader(
"Authorization",
"Bearer " + MetacatUI.appUserModel.get("token"),
);
fetch(url, { method, headers })
.then((response) => {
if (!response.ok) {
throw new Error(`Failed to fetch: ${response.statusText}`);
}
resolve(response);
})
.catch((error) => {
reject(error);
});
});
},

xhr.send();
/**
* Get the filename from the response headers or default to the model's
* title, id, or "download"
* @param {Response} response - The response object from the fetch request
* @returns {string} The filename to save the file as
* @since 0.0.0
*/
getFileNameFromResponse(response) {
const model = this;
let filename = response.headers.get("Content-Disposition");

if (!filename) {
filename =
model.get("fileName") ||
model.get("title") ||
model.get("id") ||
"download";
} else {
filename = filename
.substring(filename.indexOf("filename=") + 9)
.replace(/"/g, "");
}
filename = filename.trim().replace(/ /g, "_");
return filename;
},

getInfo: function (fields) {
Expand Down
2 changes: 1 addition & 1 deletion src/js/templates/tableEditor.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
</tbody>
</table>
</section>
<section class="spreadsheet-controls">
<section class="<%= controlsClass %>">
<button class="btn btn-danger btn-small"
id="reset">
Clear & reset table
Expand Down
85 changes: 85 additions & 0 deletions src/js/views/DataObjectView.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"use strict";

define(["backbone", "views/TableEditorView"], (Backbone, TableEditorView) => {
// The base class for the view
const BASE_CLASS = "object-view";

/**
* @class DataObjectView
* @classdesc A view that downloads and displays a DataONE object. Currently
* there is support for displaying CSV files as a table.
* @classcategory Views
* @augments Backbone.View
* @class
* @since 0.0.0
* @screenshot views/DataObjectView.png //TODO
*/
const DataObjectView = Backbone.View.extend(
/** @lends DataObjectView.prototype */
{
/** @inheritdoc */
type: "DataObjectView",

/** @inheritdoc */
className: BASE_CLASS,

/** @inheritdoc */
tagName: "div",

/**
* Initializes the DataObjectView
* @param {object} options - Options for the view
* @param {SolrResult} options.model - A SolrResult model
*/
initialize(options) {
this.model = options.model;
// TODO: We get format from the response headers, should we compare it,
// or prevent downloading the object if it's not a supported type?
// this.format = this.model.get("formatId") ||
// this.model.get("mediaType");
},

/** @inheritdoc */
render() {
this.$el.empty();
this.downloadObject().then((response) => this.renderObject(response));
return this;
},

/**
* With the already fetched DataONE object, check the format and render
* the object accordingly.
* @param {Response} response - The response from the DataONE object API
*/
renderObject(response) {
const format = response.headers.get("Content-Type");
if (format === "text/csv") {
response.text().then((text) => {
this.csv = text;
this.showTable();
});
}
},

/**
* Downloads the DataONE object
* @returns {Promise} Promise that resolves with the Response from DataONE
*/
downloadObject() {
return this.model.fetchDataObjectWithCredentials();
},

/** Shows the CSV file as a table */
showTable() {
this.table = new TableEditorView({
viewMode: true,
csv: this.csv,
});
this.el.innerHTML = "";
this.el.appendChild(this.table.render().el);
},
},
);

return DataObjectView;
});
Loading
Loading