-
-
Notifications
You must be signed in to change notification settings - Fork 27
/
CollectionModel.js
416 lines (345 loc) · 13.7 KB
/
CollectionModel.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
/* global define */
define(["jquery",
"underscore",
"backbone",
"uuid",
"collections/Filters",
"collections/SolrResults",
"models/DataONEObject",
"models/filters/Filter",
"models/Search"],
function($, _, Backbone, uuid, Filters, SolrResults, DataONEObject, Filter, Search) {
var CollectionModel = DataONEObject.extend({
//The default attributes for this model
defaults: function(){
return {
name: null,
label: null,
description: null,
filters: new Filters(),
/** @type {Search} - A Search model with a Filters collection */
// that contains the filters associated with this collection
searchModel: new Search(),
/** @type {SolrResults} - A SolrResults collection that contains the */
// filtered search results of datasets in this collection
searchResults: new SolrResults(),
/** @type {SolrResults} - A SolrResults collection that contains the */
// unfiltered search results of all datasets in this collection
allSearchResults: null
}
},
/**
* The default Backbone.Model.initialize() function
*
*/
initialize: function(options){
//Call the super class initialize function
DataONEObject.prototype.initialize.call(this, options);
this.listenToOnce(this.get("searchResults"), "sync", this.cacheSearchResults);
//If the searchResults collection is replaced at any time, reset the listener
this.on("change:searchResults", function(searchResults){
this.listenToOnce(this.get("searchResults"), "sync", this.cacheSearchResults);
});
},
/**
*
*
*/
url: function(){
return MetacatUI.appModel.get("objectServiceUrl") + encodeURIComponent(this.get("id"));
},
/**
* Overrides the default Backbone.Model.fetch() function to provide some custom
* fetch options
*
*/
fetch: function(){
var model = this;
var requestSettings = {
dataType: "xml",
error: function(){
model.trigger("error");
}
}
//Add the authorization header and other AJAX settings
requestSettings = _.extend(requestSettings, MetacatUI.appUserModel.createAjaxSettings());
return Backbone.Model.prototype.fetch.call(this, requestSettings);
},
/**
* Sends an AJAX request to fetch the system metadata for this object.
* Will not trigger a sync event since it does not use Backbone.Model.fetch
*/
fetchSystemMetadata: function(options){
if(!options) var options = {};
else options = _.clone(options);
var model = this,
fetchOptions = _.extend({
url: MetacatUI.appModel.get("metaServiceUrl") + (this.get("id") || this.get("seriesId")),
dataType: "text",
success: function(response){
model.set(DataONEObject.prototype.parse.call(model, response));
},
error: function(){
model.trigger('error');
}
}, options);
//Add the authorization header and other AJAX settings
_.extend(fetchOptions, MetacatUI.appUserModel.createAjaxSettings());
$.ajax(fetchOptions);
},
/**
* Overrides the default Backbone.Model.parse() function to parse the custom
* collection XML document
*
* @param {XMLDocument} response - The XMLDocument returned from the fetch() AJAX call
* @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
*/
parse: function(json){
//Start the empty JSON object
var modelJSON = {},
collectionNode;
//Iterate over each root XML node to find the collection node
$(response).children().each(function(i, el){
if( el.tagName.indexOf("collection") > -1 ){
collectionNode = el;
return false;
}
});
//If a collection XML node wasn't found, return an empty JSON object
if( typeof collectionNode == "undefined" || !collectionNode )
return {};
//Parse the collection XML and return it
return this.parseCollectionXML(collectionNode);
},
/**
* Parses the collection XML into a JSON object
*
* @param {Element} rootNode - The XML Element that contains all the collection nodes
* @return {JSON} The result of the parsed XML, in JSON. To be set directly on the model.
*/
parseCollectionXML: function( rootNode ){
var modelJSON = {};
//Parse the simple text nodes
modelJSON.name = this.parseTextNode(rootNode, "name");
modelJSON.label = this.parseTextNode(rootNode, "label");
modelJSON.description = this.parseTextNode(rootNode, "description");
//Create a Search model for this collection's filters
modelJSON.searchModel = new Search();
modelJSON.searchModel.set("filters", new Filters());
modelJSON.searchModel.get("filters").createCatalogFilters();
//Create a Filters collection to contain the collection definition Filters
modelJSON.filters = new Filters();
// Parse the collection definition
_.each( $(rootNode).find("definition > filter"), function(filterNode){
//Create a new Filter model
var filterModel = new Filter({
objectDOM: filterNode,
isInvisible: true,
// projDefFilter allows us to distinguish this type of filter
// from other filters during serialization
projDefFilter: true
});
//Add the filter to the Filters collection
modelJSON.filters.add(filterModel);
//Add the filter to the Search model
modelJSON.searchModel.get("filters").add(filterModel);
});
return modelJSON;
},
/**
* Generate a UUID, reserve it using the DataOne API, and set it on the model
*/
reserveSeriesId: function(){
// Create a new series ID
var seriesId = "urn:uuid:" + uuid.v4();
// Set the seriesId on the project model right away, since reserving takes
// time. This will be updated in the rare case that the first seriesId was
// already taken.
this.set("seriesId", seriesId);
// Reserve a series ID for the new project
var model = this;
var options = {
url: MetacatUI.appModel.get("d1CNBaseUrl") +
MetacatUI.appModel.get("d1CNService") +
"/reserve",
type: "POST",
data: { pid: seriesId },
tryCount : 0,
// If a generated seriesId is already reserved, how many times to retry
retryLimit : 5,
success: function(xhr){
// If the first seriesId was taken, then update the model with the
// successfully reserved seriesId.
if(this.tryCount > 0) {
model.set("seriesId", $(xhr).find("identifier").text());
}
},
error : function(xhr) {
// If the seriesId was already reserved, try again
if (xhr.status == 409) {
this.tryCount++;
if (this.tryCount <= this.retryLimit) {
// Generate another seriesId
this.data = { pid:"urn:uuid:" + uuid.v4() };
// Send the reserve request again
$.ajax(this);
return;
}
return;
// If the user isn't logged in, or doesn't have write access
} else if (xhr.status = 401 ){
model.set("isAuthorized", false);
} else {
parsedResponse = $(xhr.responseText).not("style, title").text();
model.set("errorMessage", parsedResponse);
}
}
}
_.extend(options, MetacatUI.appUserModel.createAjaxSettings());
$.ajax(options);
},
/**
* Creates a FilterModel with isPartOf as the field and the seriesId as
* the value. Adds the filter to the searchModel and the filters model
* attributes.
* @param {string} [seriesId] - the seriesId of the collection or project
*/
addIsPartOfFilter: function(seriesId){
// If no seriesId is given
if(!seriesId){
// Use the seriesId set on the model
if(this.get("seriesId")){
seriesId = this.get("seriesId");
// If there's no seriesId on the model, make and reserve one
} else {
this.reserveSeriesId()
seriesId = this.get("seriesId");
// Set a listener to create an isPartOf filter using the seriesId once
// the series Id is set. Just in case the first seriesId generated was
// already reserved, update the isPartOf filters on the subsequent
// attempts to create and resere an ID.
this.on("change:seriesId", function(seriesId){
this.addIsPartOfFilter();
});
}
}
// Create the new filterModel
var filterModel = new Filter({
fields: ["isPartOf"],
values: [seriesId],
isInvisible: true,
// projDefFilter allows us to distinguish this type of filter
// from other filters during serialization
projDefFilter: true
});
//Remove any existing isPartOf filters
this.get("searchModel").get("filters").removeFiltersByField("isPartOf");
this.get("filters").removeFiltersByField("isPartOf");
//Add the new isPartOf filter
this.get("searchModel").get("filters").add(filterModel);
this.get("filters").add(filterModel);
MetacatUI.proj = this;
},
/**
* Gets the text content of the XML node matching the given node name
*
* @param {Element} parentNode - The parent node to select from
* @param {string} nodeName - The name of the XML node to parse
* @param {boolean} isMultiple - If true, parses the nodes into an array
* @return {(string|Array)} - Returns a string or array of strings of the text content
*/
parseTextNode: function( parentNode, nodeName, isMultiple ){
var node = $(parentNode).children(nodeName);
//If no matching nodes were found, return falsey values
if( !node || !node.length ){
//Return an empty array if the isMultiple flag is true
if( isMultiple )
return [];
//Return null if the isMultiple flag is false
else
return null;
}
//If exactly one node is found and we are only expecting one, return the text content
else if( node.length == 1 && !isMultiple ){
return node[0].textContent.trim();
}
//If more than one node is found, parse into an array
else{
return _.map(node, function(node){
return node.textContent.trim() || null;
});
}
},
/**
* Updates collection XML with values in the collection model
*
* @param {XMLDocument} objectDOM the XML element to be updated
* @return {XMLElement} An updated XML element
*/
serializeCollectionXML: function(objectDOM){
// Get or make objectDOM
if(!objectDOM){
if (this.get("objectDOM")) {
var objectDOM = this.get("objectDOM").cloneNode(true);
$(objectDOM).empty();
} else {
// create an XML collection element from scratch
var objectDOM = $($.parseXML("<collection></collection>")).children()[0];
}
};
// Serialize filters
// Get all the search filter models (not all of which are project definition filters)
var filterModels = this.get("searchModel").get("filters").models;
// Remove definition node if it exists in XML already
$(objectDOM).find("definition").remove();
// Create new definition element
var definitionSerialized = objectDOM.ownerDocument.createElement("definition");
// Iterate through the filter models
$(filterModels).each(function(i, filterModel){
// Find the filters that are project definition filters
if(filterModel.get("projDefFilter")){
// updateDOM of project definition filters, then append to <definition>
var filterSerialized = filterModel.updateDOM();
$(definitionSerialized).append(filterSerialized);
};
});
$(objectDOM).prepend(definitionSerialized);
// Get and update the simple text strings (everything but definition)
// in reverse order because we prepend them consecutively to objectDOM
var collectionTextData = {
description: this.get("description"),
label: this.get("label"),
name: this.get("name")
}
_.map(collectionTextData, function(value, nodeName){
// Remove the node if it exists
// Use children() and not find() as there are sub-children named label
$(objectDOM).children(nodeName).remove();
// Don't serialize falsey values
if(value){
// Make new sub-node
var collectionSubnodeSerialized = objectDOM.ownerDocument.createElement(nodeName);
$(collectionSubnodeSerialized).text(value);
// Append new sub-node to the start of the objectDOM
$(objectDOM).prepend(collectionSubnodeSerialized);
}
});
return objectDOM;
},
/**
* Creates a copy of the SolrResults collection and saves it in this
* model so that there is always access to the unfiltered list of datasets
*
* @param {SolrResults} searchResults - The SolrResults collection to cache
*/
cacheSearchResults: function(searchResults){
//Save a copy of the SolrResults so that we always have a copy of
// the unfiltered list of datasets
this.set("allSearchResults", searchResults.clone());
//Make a copy of the facetCounts object
var allSearchResults = this.get("allSearchResults");
allSearchResults.facetCounts = Object.assign({}, searchResults.facetCounts);
}
});
return CollectionModel;
});