• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dondi / GRNsight / 20403051579

21 Dec 2025 01:47AM UTC coverage: 80.06%. First build
20403051579

push

github

web-flow
Merge pull request #1298 from dondi/beta

v7.4.0 release

410 of 518 branches covered (79.15%)

Branch coverage included in aggregate %.

329 of 434 new or added lines in 18 files covered. (75.81%)

1200 of 1493 relevant lines covered (80.38%)

9320.67 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

75.78
/server/controllers/spreadsheet-controller.js
1
var multiparty = require("multiparty");
3✔
2
var xlsx = require("node-xlsx");
3✔
3
var path = require("path");
3✔
4
const { NETWORK_GRN_MODE, NETWORK_PPI_MODE } = require("./constants");
3✔
5
var parseAdditionalSheets = require(__dirname + "/additional-sheet-parser");
3✔
6
var parseExpressionSheets = require(__dirname + "/expression-sheet-parser");
3✔
7
var parseNetworkSheet = require(__dirname + "/network-sheet-parser");
3✔
8
var demoWorkbooks = require(__dirname + "/demo-workbooks");
3✔
9
var constants = require(__dirname + "/workbook-constants");
3✔
10
// var cytoscape = require("cytoscape"); //NOTE: Commented out for issue #474
11

12
var helpers = require(__dirname + "/helpers");
3✔
13

14
var EXPRESSION_SHEET_SUFFIXES = ["_expression", "_optimized_expression", "_sigmas"];
3✔
15

16
var SPECIES = [
3✔
17
    "Arabidopsis thaliana",
18
    "Caenorhabditis elegans",
19
    "Drosophila melanogaster",
20
    "Homo sapiens",
21
    "Mus musculus",
22
    "Saccharomyces cerevisiae",
23
];
24

25
const WORKBOOK_TYPES = [NETWORK_GRN_MODE, NETWORK_PPI_MODE];
3✔
26

27
var TAXON_ID = ["3702", "6293", "7227", "9606", "10090", "4932", "559292"];
3✔
28

29
var isExpressionSheet = function (sheetName) {
3✔
30
    return EXPRESSION_SHEET_SUFFIXES.some(function (suffix) {
543✔
31
        return sheetName.includes(suffix);
1,197✔
32
    });
33
};
34

35
var doesSpeciesExist = function (speciesInfo) {
3✔
36
    for (var s in SPECIES) {
42✔
37
        if (SPECIES[s] === speciesInfo) {
252✔
38
            return true;
36✔
39
        }
40
    }
41
    for (var t in TAXON_ID) {
6✔
42
        if (TAXON_ID[t] === speciesInfo) {
42!
43
            return true;
×
44
        }
45
    }
46
    return false;
6✔
47
};
48

49
var supportWorkbookType = function (type) {
3✔
50
    for (var t in WORKBOOK_TYPES) {
39✔
51
        if (WORKBOOK_TYPES[t] === type) {
39✔
52
            return true;
39✔
53
        }
54
    }
55
    return false;
×
56
};
57

58
var addMessageToArray = function (messageArray, message) {
3✔
59
    messageArray.push(message);
12✔
60
};
61

62
var addWarning = function (workbook, message) {
3✔
63
    var warningsCount = workbook.warnings.length;
3✔
64
    var MAX_WARNINGS = 75;
3✔
65
    if (warningsCount < MAX_WARNINGS) {
3!
66
        addMessageToArray(workbook.warnings, message);
3✔
67
    } else {
68
        addMessageToArray(workbook.errors, constants.errors.warningsCountError);
×
69
        return false;
×
70
    }
71
};
72

73
var addError = function (workbook, message) {
3✔
74
    var errorsCount = workbook.errors.length;
9✔
75
    var MAX_ERRORS = 20;
9✔
76
    if (errorsCount < MAX_ERRORS) {
9!
77
        addMessageToArray(workbook.errors, message);
9✔
78
    } else {
79
        addMessageToArray(workbook.errors, constants.errors.errorsCountError);
×
80
        return false;
×
81
    }
82
};
83

84
var difference = function (setA, setB) {
3✔
85
    let _difference = new Set(setA);
576✔
86
    for (let elemB of setB) {
8,358✔
87
        if (_difference.has(elemB)) {
8,358✔
88
            _difference.delete(elemB);
8,352✔
89
        }
90
    }
576✔
91
    return _difference;
576✔
92
};
93

94
var deepClone = function (object, isArray) {
3✔
95
    var clone = isArray ? [] : {};
1,563✔
96
    if (isArray) {
1,563✔
97
        for (let i of object) {
1,980✔
98
            if (i !== null && typeof i === "object") {
1,980✔
99
                clone.push(deepClone(i, Array.isArray(i)));
1,212✔
100
            } else {
101
                clone.push(i);
768✔
102
            }
103
        }
234✔
104
    } else {
105
        for (let i in object) {
1,329✔
106
            if (object[i] !== null && typeof object[i] === "object") {
4,635✔
107
                clone[i] = deepClone(object[i], Array.isArray(object[i]));
312✔
108
            } else {
109
                clone[i] = object[i];
4,323✔
110
            }
111
        }
112
    }
113
    return clone;
1,563✔
114
};
115

116
var crossSheetInteractions = function (workbookFile) {
3✔
117
    var workbook = {};
39✔
118

119
    // Refactored the parseNetworkSheet function to preserve all network type sheets including "network",
120
    // "network_optimized_weights",and "network_weights" restructuring workbook object as a result
121

122
    var networks = parseNetworkSheet.networks(workbookFile);
39✔
123

124
    // Parse expression and 2-column data, then add to workbook object
125
    // Eventually, will split this up into parsing for each type of sheet.
126
    var additionalData = parseAdditionalSheets(workbookFile);
39✔
127

128
    var expressionData = parseExpressionSheets(workbookFile);
39✔
129

130
    if (
39✔
131
        networks &&
156✔
132
        networks.networkOptimizedWeights &&
133
        typeof networks.networkOptimizedWeights === "object" &&
134
        Object.keys(networks.networkOptimizedWeights).length > 0
135
    ) {
136
        // Base workbook is a clone of the prefered Optimized weights sheet
137
        workbook = deepClone(networks.networkOptimizedWeights, false);
12✔
138
        // Add errors from network sheet if it exists
139
        if (
12✔
140
            networks.network &&
36✔
141
            typeof networks.network === "object" &&
142
            Object.keys(networks.network).length > 0
143
        ) {
144
            if (networks.network.errors !== undefined) {
12✔
145
                networks.network.errors.forEach(data => workbook.errors.push(data));
12✔
146
            }
147

148
            if (networks.network.warnings !== undefined) {
12✔
149
                networks.network.warnings.forEach(data => workbook.warnings.push(data));
12✔
150
            }
151
        }
152
    } else {
153
        // Set base workbook to a deep copy of the default network if network optimized weights does not exist
154
        workbook = deepClone(networks.network, false);
27✔
155
    }
156
    // Add errors and warnings from network weights to preserve the sheet
157
    if (
39✔
158
        networks.networkWeights &&
117✔
159
        typeof networks.networkWeights === "object" &&
160
        Object.keys(networks.networkWeights).length > 0
161
    ) {
162
        if (networks.networkWeights.errors !== undefined) {
36✔
163
            networks.networkWeights.errors.forEach(data => workbook.errors.push(data));
36✔
164
        }
165

166
        if (networks.networkWeights.warnings !== undefined) {
36✔
167
            networks.networkWeights.warnings.forEach(data => workbook.warnings.push(data));
36✔
168
        }
169
    }
170

171
    // Add errors and warnings from meta sheets
172
    if (additionalData && additionalData.meta) {
39✔
173
        if (additionalData.meta.errors !== undefined) {
39✔
174
            additionalData.meta.errors.forEach(data => workbook.errors.push(data));
39✔
175
        }
176

177
        if (additionalData.meta.warnings !== undefined) {
39✔
178
            additionalData.meta.warnings.forEach(data => workbook.warnings.push(data));
39✔
179
        }
180
    }
181

182
    if (additionalData && additionalData.twoColumnSheets) {
39✔
183
        // Add errors and warnings from two column sheets
184
        for (let sheet in additionalData.twoColumnSheets) {
39✔
185
            additionalData.twoColumnSheets[sheet].errors.forEach(data =>
117✔
186
                workbook.errors.push(data)
187
            );
188
        }
189

190
        for (let sheet in additionalData.twoColumnSheets) {
39✔
191
            additionalData.twoColumnSheets[sheet].warnings.forEach(data =>
117✔
192
                workbook.warnings.push(data)
193
            );
194
        }
195
    }
196

197
    if (additionalData && additionalData.meta2) {
39✔
198
        // Add errors and warnings from two column sheets
199
        if (additionalData.meta2.errors !== undefined) {
39✔
200
            additionalData.meta2.errors.forEach(data => workbook.errors.push(data));
12✔
201
        }
202

203
        if (additionalData.meta2.warnings !== undefined) {
39✔
204
            additionalData.meta2.warnings.forEach(data => workbook.warnings.push(data));
12✔
205
        }
206
    }
207

208
    additionalData.meta.data.workbookType = parseNetworkSheet.workbookType(workbookFile);
39✔
209
    if (additionalData.meta.data.workbookType === undefined) {
39!
210
        addWarning(workbook, constants.warnings.noWorkbookTypeDetected);
×
211
        additionalData.meta.data.workbookType = NETWORK_GRN_MODE;
×
212
    } else if (!supportWorkbookType(additionalData.meta.data.workbookType)) {
39!
213
        addWarning(
×
214
            workbook,
215
            constants.warnings.unsupportedWorkbookTypeDetected(
216
                additionalData.meta.data.workbookType
217
            )
218
        );
219
        additionalData.meta.data.workbookType = NETWORK_GRN_MODE;
×
220
    }
221

222
    if (
39!
223
        additionalData.meta.data.species === undefined &&
39!
224
        additionalData.meta.data.taxon_id === undefined
225
    ) {
226
        addWarning(workbook, constants.warnings.noSpeciesInformationDetected);
×
227
        additionalData.meta.data.species = "Saccharomyces cerevisiae";
×
228
        additionalData.meta.data["taxon_id"] = "559292";
×
229
    } else if (
39✔
230
        !doesSpeciesExist(additionalData.meta.data.species) &&
42✔
231
        !doesSpeciesExist(additionalData.meta.data.taxon_id)
232
    ) {
233
        addWarning(
3✔
234
            workbook,
235
            constants.warnings.unknownSpeciesDetected(
236
                additionalData.meta.data.species,
237
                additionalData.meta.data.taxon_id
238
            )
239
        );
240
        additionalData.meta.data.species = "Saccharomyces cerevisiae";
3✔
241
        additionalData.meta.data["taxon_id"] = 559292;
3✔
242
    }
243

244
    // Add errors and warnings from expression sheets
245
    // FUTURE IMPROVEMENT: not all expression sheets are specifically named 'wt_log2_expression.'
246
    // We need to account for all the different possible expression sheet names.
247
    if (expressionData) {
39✔
248
        if (additionalData.meta.data.workbookType === constants.NETWORK_GRN_MODE) {
39!
NEW
249
            if (
×
250
                expressionData["expression"] &&
×
251
                Object.keys(expressionData["expression"]).length === 0
252
            ) {
253
                addWarning(expressionData, constants.warnings.missingExpressionWarning());
×
254
            }
255
        }
256
        if (expressionData.errors !== undefined) {
39✔
257
            expressionData.errors.forEach(data => workbook.errors.push(data));
39✔
258
        }
259
        if (expressionData.warnings !== undefined) {
39✔
260
            expressionData.warnings.forEach(data => workbook.warnings.push(data));
39✔
261
        }
262
    }
263

264
    if (expressionData && expressionData.expression) {
39✔
265
        if (expressionData.expression.errors !== undefined) {
39!
266
            expressionData.expression.errors.forEach(data => workbook.errors.push(data));
×
267
        }
268

269
        if (expressionData.expression.warnings !== undefined) {
39!
270
            expressionData.expression.warnings.forEach(data => workbook.warnings.push(data));
×
271
        }
272
    }
273

274
    if (
39✔
275
        expressionData &&
117✔
276
        expressionData.expression &&
277
        expressionData.expression.wt_log2_expression
278
    ) {
279
        if (expressionData.expression.wt_log2_expression.errors !== undefined) {
39✔
280
            expressionData.expression.wt_log2_expression.errors.forEach(data =>
39✔
281
                workbook.errors.push(data)
282
            );
283
        }
284

285
        if (expressionData.expression.wt_log2_expression.warnings !== undefined) {
39✔
286
            expressionData.expression.wt_log2_expression.warnings.forEach(data =>
39✔
287
                workbook.warnings.push(data)
288
            );
289
        }
290
    }
291

292
    // Gene Mismatch and Label Error Tests
293

294
    workbookFile.forEach(function (sheet) {
39✔
295
        if (isExpressionSheet(sheet.name)) {
543✔
296
            var tempWorkbookGenes = new Set();
288✔
297
            for (let i = 0; i < workbook.genes.length; i++) {
288✔
298
                tempWorkbookGenes.add(workbook.genes[i].name);
4,179✔
299
            }
300
            var tempExpressionGenes = new Set(
288✔
301
                expressionData.expression[sheet.name].columnGeneNames
302
            );
303
            var extraExpressionGenes = difference(tempExpressionGenes, tempWorkbookGenes);
288✔
304
            var extraWorkbookGenes = difference(tempWorkbookGenes, tempExpressionGenes);
288✔
305

306
            if (extraExpressionGenes.size === 0 && extraWorkbookGenes.size === 0) {
288✔
307
                for (var i = 0; i < workbook.genes.length; i++) {
282✔
308
                    if (
4,152✔
309
                        workbook.genes[i].name !==
310
                        expressionData.expression[sheet.name].columnGeneNames[i]
311
                    ) {
312
                        addError(workbook, constants.errors.geneMismatchError(sheet.name));
3✔
313
                        break;
3✔
314
                    }
315
                }
316
            } else {
317
                if (extraWorkbookGenes.size > 0) {
6✔
318
                    addError(workbook, constants.errors.missingGeneNamesError(sheet.name));
3✔
319
                }
320
                if (extraExpressionGenes.size > 0) {
6✔
321
                    addError(workbook, constants.errors.extraGeneNamesError(sheet.name));
3✔
322
                }
323
            }
324
        }
325
    });
326

327
    // Integrate the desired properties from the other objects.
328
    workbook.network = networks.network;
39✔
329
    workbook.networkOptimizedWeights = networks.networkOptimizedWeights;
39✔
330
    workbook.networkWeights = networks.networkWeights;
39✔
331
    workbook.meta = additionalData.meta;
39✔
332
    workbook.twoColumnSheets = additionalData.twoColumnSheets;
39✔
333
    workbook.meta2 = additionalData.meta2;
39✔
334
    workbook.expression = expressionData.expression;
39✔
335
    return workbook;
39✔
336
};
337

338
var processGRNmap = function (path, res, app) {
3✔
339
    var sheet;
340

341
    helpers.attachCorsHeader(res, app);
×
342

343
    try {
×
344
        sheet = xlsx.parse(path);
×
345
    } catch (err) {
346
        return res.json(400, "Unable to read input. The file may be corrupt.");
×
347
    }
348

349
    helpers.attachFileHeaders(res, path);
×
350

351
    var workbook = crossSheetInteractions(sheet);
×
352

353
    return workbook.errors.length === 0
×
354
        ? // If all looks well, return the workbook with an all clear
355
          res.json(workbook)
356
        : // If all does not look well, return the workbook with an error 400
357
          res.status(400).json(workbook);
358
};
359

360
var grnSightToCytoscape = function (workbook) {
3✔
361
    var result = [];
×
362
    workbook.genes.forEach(function (gene) {
×
363
        result.push({
×
364
            data: {
365
                id: gene.name,
366
            },
367
        });
368
    });
369

370
    workbook.links.forEach(function (link) {
×
371
        var sourceGene = workbook.genes[link.source];
×
372
        var targetGene = workbook.genes[link.target];
×
373
        result.push({
×
374
            data: {
375
                id: sourceGene.name + targetGene.name,
376
                source: sourceGene.name,
377
                target: targetGene.name,
378
            },
379
        });
380
    });
381

382
    return result;
×
383
};
384

385
/* NOTE: See above. Commented out until resolution of #474
386
var graphStatisticsReport = function(workbook)  {
387
    var betweennessCentrality = [];
388
    var shortestPath = [];
389
    var cytoscapeElements = grnSightToCytoscape(workbook);
390
    var cy = cytoscape({
391
        headless: true,
392
        elements: cytoscapeElements
393
    });
394
    for (var i = 0; i < workbook.genes.length; i++) {
395
        var bc = cy.$().bc();
396
        betweennessCentrality.push({
397
            gene: workbook.genes[i],
398
            betweennessCentrality: bc.betweenness("#" + workbook.genes[i].name, null, true)
399
        });
400
        var dijkstra = cy.elements().dijkstra("#" + workbook.genes[i].name, null, true);
401
        for (var j = 0; j < workbook.genes.length; j++) {
402
            shortestPath.push({
403
                source: workbook.genes[i].name,
404
                pathData: {
405
                    target: workbook.genes[j].name,
406
                    shortestPath: dijkstra.distanceTo("#" + workbook.genes[j].name, null, true)
407
                }
408
            });
409
        }
410
    }
411
    return {
412
        betweennessCentrality: betweennessCentrality,
413
        shortestPath: shortestPath
414
    };
415
};
416
*/
417

418
module.exports = function (app) {
3✔
419
    if (app) {
3!
420
        // parse the incoming form data, then parse the spreadsheet. Finally, send back json.
421
        app.post("/upload", function (req, res) {
×
422
            // TODO: Add file validation (make sure that file is an Excel file)
423
            new multiparty.Form().parse(req, function (err, fields, files) {
×
424
                if (err) {
×
NEW
425
                    return res.json(
×
426
                        400,
427
                        "There was a problem uploading your file. Please try again."
428
                    );
429
                }
430
                var input;
431
                try {
×
432
                    input = files.file[0].path;
×
433
                } catch (err) {
434
                    return res.json(400, "No upload file selected.");
×
435
                }
436

437
                if (path.extname(input) !== ".xlsx") {
×
438
                    return res.json(
×
439
                        400,
440
                        "This file cannot be loaded because:<br><br> The file is \
441
                        not in a format GRNsight can read." +
442
                            "<br>Please select an Excel Workbook \
443
                        (.xlsx) file. Note that Excel 97-2003 Workbook (.xls) files are not " +
444
                            " able to be read by GRNsight. <br><br>SIF and GraphML files can be loaded \
445
                        using the importer under File > Import." +
446
                            " Additional information about file \
447
                        types that GRNsight supports is in the Documentation."
448
                    );
449
                }
450

451
                // input.meta holds the species and taxon data
452
                return processGRNmap(input, res, app);
×
453
            });
454
        });
455

456
        // Load the demos
457
        app.get("/demo/unweighted", function (req, res) {
×
NEW
458
            return demoWorkbooks(
×
459
                "test-files/demo-files/15-genes_28-edges_db5_Dahlquist-data_input.xlsx",
460
                res,
461
                app
462
            );
463
        });
464

465
        app.get("/demo/weighted", function (req, res) {
×
466
            return demoWorkbooks(
×
467
                "test-files/demo-files/15-genes_28-edges_db5_Dahlquist-data_estimation_output.xlsx",
468
                res,
469
                app
470
            );
471
        });
472

473
        app.get("/demo/schadeInput", function (req, res) {
×
NEW
474
            return demoWorkbooks(
×
475
                "test-files/demo-files/21-genes_31-edges_Schade-data_input.xlsx",
476
                res,
477
                app
478
            );
479
        });
480

481
        app.get("/demo/schadeOutput", function (req, res) {
×
482
            return demoWorkbooks(
×
483
                "test-files/demo-files/21-genes_31-edges_Schade-data_estimation_output.xlsx",
484
                res,
485
                app
486
            );
487
        });
488

489
        app.get("/demo/ppi", function (req, res) {
×
NEW
490
            return demoWorkbooks("test-files/demo-files/18_proteins_81_edges_PPI.xlsx", res, app);
×
491
        });
492
    }
493

494
    // exporting parseNetworkSheet for use in testing. Do not remove!
495
    return {
3✔
496
        grnSightToCytoscape: grnSightToCytoscape,
497
        processGRNmap: processGRNmap,
498
        crossSheetInteractions: crossSheetInteractions,
499
    };
500
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc