sequenceserver 3.1.2 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/sequenceserver/api_errors.rb +18 -0
- data/lib/sequenceserver/blast/hit.rb +1 -2
- data/lib/sequenceserver/blast/job.rb +15 -5
- data/lib/sequenceserver/blast/report.rb +5 -1
- data/lib/sequenceserver/blast/tasks.rb +1 -1
- data/lib/sequenceserver/database.rb +5 -1
- data/lib/sequenceserver/job.rb +26 -10
- data/lib/sequenceserver/makeblastdb.rb +2 -1
- data/lib/sequenceserver/routes.rb +38 -13
- data/lib/sequenceserver/version.rb +1 -1
- data/lib/sequenceserver/zip_file_generator.rb +1 -1
- data/lib/sequenceserver.rb +1 -1
- data/public/404.html +8 -16
- data/public/css/app.css +794 -111
- data/public/css/app.min.css +1 -1
- data/public/css/sequenceserver.min.css +1 -8
- data/public/js/alignment_exporter.js +16 -28
- data/public/js/circos.js +103 -96
- data/public/{packages/circosJS@1.7.0.js → js/circosjs.js} +156 -778
- data/public/js/cloud_share_modal.js +95 -98
- data/public/js/collapse_preferences.js +13 -15
- data/public/js/databases.js +4 -3
- data/public/js/databases_tree.js +51 -30
- data/public/js/error_modal.js +35 -22
- data/public/js/exporter.js +2 -0
- data/public/js/form.js +55 -17
- data/public/js/grapher.js +94 -92
- data/public/js/hit.js +24 -22
- data/public/js/hits.js +283 -0
- data/public/js/hits_overview.js +8 -27
- data/public/js/hsp.js +140 -144
- data/public/js/jquery_world.js +25 -16
- data/public/js/kablammo.js +14 -10
- data/public/js/length_distribution.js +23 -37
- data/public/js/mailto.js +1 -3
- data/public/js/null_plugins/report_plugins.js +4 -5
- data/public/js/options.js +19 -25
- data/public/js/query.js +36 -35
- data/public/js/report/alignment_results.js +18 -0
- data/public/js/report/graphical_overview.js +38 -0
- data/public/js/report/report_header.js +28 -0
- data/public/js/report/run_summary.js +43 -0
- data/public/js/report.js +124 -348
- data/public/js/report_root.js +10 -8
- data/public/js/search.js +31 -11
- data/public/js/search_button.js +85 -78
- data/public/js/sequence.js +169 -180
- data/public/js/sequence_modal.js +58 -65
- data/public/js/share_url.js +5 -5
- data/public/js/sidebar.js +152 -89
- data/public/js/svgExporter.js +8 -3
- data/public/js/tests/alignment_exporter.spec.js +38 -0
- data/public/js/tests/cloud_share_modal.spec.js +75 -0
- data/public/js/tests/form.spec.js +1 -1
- data/public/js/tests/report.spec.js +37 -15
- data/public/js/tests/visualisation_helpers.spec.js +28 -0
- data/public/js/visualisation_helpers.js +24 -27
- data/public/packages/jquery-ui@1.13.3.js +19070 -0
- data/public/sequenceserver-report.min.js +3 -2481
- data/public/sequenceserver-report.min.js.LICENSE.txt +213 -0
- data/public/sequenceserver-report.min.js.map +1 -0
- data/public/sequenceserver-search.min.js +3 -2382
- data/public/sequenceserver-search.min.js.LICENSE.txt +262 -0
- data/public/sequenceserver-search.min.js.map +1 -0
- data/views/error.erb +1 -1
- data/views/layout.erb +122 -133
- data/views/report.erb +3 -2
- data/views/search.erb +10 -14
- metadata +21 -28
- data/public/config.js +0 -147
- data/public/css/fonts.css +0 -31
- data/public/css/grapher.css +0 -670
- data/public/css/sequenceserver.css +0 -527
- data/public/packages/jquery-ui@1.11.4.js +0 -16624
- data/public/vendor/github/mbostock/d3@3.5.6/d3.js +0 -9506
- data/public/vendor/github/mbostock/d3@3.5.6.js +0 -1
- data/views/search_layout.erb +0 -153
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import AlignmentExporter from '../alignment_exporter';
|
|
2
|
+
|
|
3
|
+
describe('AlignmentExporter', () => {
|
|
4
|
+
let exporter;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
exporter = new AlignmentExporter();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('wrap_string', () => {
|
|
11
|
+
it('wraps a string to a specified width', () => {
|
|
12
|
+
const str = 'abcdefghijklmnopqrstuvwxyz';
|
|
13
|
+
const width = 5;
|
|
14
|
+
const expected = 'abcde\nfghij\nklmno\npqrst\nuvwxy\nz';
|
|
15
|
+
expect(exporter.wrap_string(str, width)).toBe(expected);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('generate_fasta', () => {
|
|
20
|
+
it('generates a fasta string from hsps', () => {
|
|
21
|
+
const hsps = [
|
|
22
|
+
{
|
|
23
|
+
query_id: 'query1',
|
|
24
|
+
qstart: 1,
|
|
25
|
+
qend: 10,
|
|
26
|
+
qseq: 'ATGCATGCAT',
|
|
27
|
+
hit_id: 'hit1',
|
|
28
|
+
sstart: 1,
|
|
29
|
+
send: 10,
|
|
30
|
+
midline: '||||||||||',
|
|
31
|
+
sseq: 'ATGCATGCAT',
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
const expected = '>query1:1-10\nATGCATGCAT\n>query1:1-10_alignment_hit1:1-10\n||||||||||\n>hit1:1-10\nATGCATGCAT\n';
|
|
35
|
+
expect(exporter.generate_fasta(hsps)).toBe(expected);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { render, fireEvent, waitFor } from '@testing-library/react';
|
|
2
|
+
import CloudShareModal, { handleSubmit } from '../cloud_share_modal';
|
|
3
|
+
|
|
4
|
+
describe('CloudShareModal', () => {
|
|
5
|
+
let component;
|
|
6
|
+
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
global.fetch = jest.fn(() =>
|
|
9
|
+
Promise.resolve({
|
|
10
|
+
json: () => Promise.resolve({ shareable_url: 'http://test.com' }),
|
|
11
|
+
ok: true
|
|
12
|
+
})
|
|
13
|
+
);
|
|
14
|
+
component = render(<CloudShareModal querydb="" program="" queryLength="0" />);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('renders without crashing', () => {
|
|
18
|
+
expect(component).toBeTruthy();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('initial state is correct', () => {
|
|
22
|
+
const { getByLabelText } = component;
|
|
23
|
+
const emailInput = getByLabelText('Your Email Address');
|
|
24
|
+
const tosCheckbox = getByLabelText('I agree to the Terms and Conditions of Service');
|
|
25
|
+
|
|
26
|
+
expect(emailInput.value).toBe('');
|
|
27
|
+
expect(tosCheckbox.checked).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('handles form submission', async () => {
|
|
31
|
+
const { getByLabelText, getByText, getByDisplayValue } = component;
|
|
32
|
+
|
|
33
|
+
// Fill out the form
|
|
34
|
+
fireEvent.change(getByLabelText(/Your Email Address/i), { target: { value: 'test@test.com' } });
|
|
35
|
+
fireEvent.click(getByLabelText(/I agree to the/i));
|
|
36
|
+
|
|
37
|
+
// Submit the form
|
|
38
|
+
fireEvent.click(getByText('Submit'));
|
|
39
|
+
|
|
40
|
+
// Wait for the loading state to finish
|
|
41
|
+
await waitFor(() => getByText('Uploading the job to SequenceServer Cloud, please wait...'));
|
|
42
|
+
|
|
43
|
+
// Check that the results are displayed
|
|
44
|
+
await waitFor(() => getByText('Copy to Clipboard'));
|
|
45
|
+
|
|
46
|
+
expect(getByDisplayValue(/http:\/\/test.com/i)).toBeTruthy()
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('handles form submission errors', async () => {
|
|
50
|
+
// Override the fetch mock to simulate a server error
|
|
51
|
+
global.fetch = jest.fn(() =>
|
|
52
|
+
Promise.resolve({
|
|
53
|
+
json: () => Promise.resolve({ errors: ['Test error'] }),
|
|
54
|
+
ok: false
|
|
55
|
+
})
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const { getByLabelText, getByText } = component;
|
|
59
|
+
|
|
60
|
+
// Fill out the form
|
|
61
|
+
fireEvent.change(getByLabelText(/Your Email Address/i), { target: { value: 'test@test.com' } });
|
|
62
|
+
fireEvent.click(getByLabelText(/I agree to the/i));
|
|
63
|
+
|
|
64
|
+
// Submit the form
|
|
65
|
+
fireEvent.click(getByText('Submit'));
|
|
66
|
+
|
|
67
|
+
// Wait for the loading state to finish
|
|
68
|
+
await waitFor(() => getByText('Uploading the job to SequenceServer Cloud, please wait...'));
|
|
69
|
+
|
|
70
|
+
// Check that the results are displayed
|
|
71
|
+
await waitFor(() => getByText('Network response was not ok'));
|
|
72
|
+
|
|
73
|
+
expect(getByText(/Network response was not ok/i)).toBeTruthy();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/* eslint-disable no-undef */
|
|
2
2
|
/* eslint-disable no-unused-vars */
|
|
3
3
|
import { render, screen, fireEvent } from '@testing-library/react';
|
|
4
|
+
import { act } from 'react';
|
|
4
5
|
import Report from '../report';
|
|
5
6
|
import Sidebar from '../sidebar';
|
|
6
7
|
import shortResponseJSON from './mock_data/short_response.json';
|
|
7
8
|
import longResponseJSON from './mock_data/long_response.json';
|
|
8
9
|
|
|
9
10
|
const setMockJSONResult = (result) => {
|
|
10
|
-
global
|
|
11
|
+
global.fetch = jest.fn(() =>
|
|
12
|
+
Promise.resolve({
|
|
13
|
+
ok: result.status === 200,
|
|
14
|
+
status: result.status,
|
|
15
|
+
text: () => Promise.resolve(JSON.stringify(result.responseJSON)),
|
|
16
|
+
})
|
|
17
|
+
);
|
|
11
18
|
};
|
|
12
19
|
|
|
13
20
|
const nextQueryButton = () => screen.queryByRole('button', { name: /next query/i });
|
|
@@ -38,19 +45,30 @@ describe('REPORT PAGE', () => {
|
|
|
38
45
|
expect(screen.getByRole('heading', { name: 'BLAST-ing' })).toBeInTheDocument();
|
|
39
46
|
});
|
|
40
47
|
|
|
41
|
-
it('should show error modal if error occurs while fetching queries', () => {
|
|
48
|
+
it('should show error modal if error occurs while fetching queries', async () => {
|
|
42
49
|
const showErrorModal = jest.fn();
|
|
43
|
-
|
|
44
|
-
|
|
50
|
+
|
|
51
|
+
setMockJSONResult({ status: 500, responseJSON: { error: "Internal Server Error" }});
|
|
52
|
+
|
|
53
|
+
await act(async () => {
|
|
54
|
+
render(<Report showErrorModal={showErrorModal} />);
|
|
55
|
+
});
|
|
56
|
+
|
|
45
57
|
expect(showErrorModal).toHaveBeenCalledTimes(1);
|
|
46
58
|
});
|
|
47
59
|
|
|
48
|
-
it('it should render the report page correctly if there\'s a response provided', () => {
|
|
60
|
+
it('it should render the report page correctly if there\'s a response provided', async () => {
|
|
49
61
|
setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
|
|
50
|
-
const { container } = render(<Report getCharacterWidth={jest.fn()} />);
|
|
51
|
-
expect(container.querySelector('#results')).toBeInTheDocument();
|
|
52
62
|
|
|
63
|
+
let container;
|
|
64
|
+
await act(async () => {
|
|
65
|
+
const result = render(<Report getCharacterWidth={jest.fn()} />);
|
|
66
|
+
container = result.container;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(container.querySelector('#results')).toBeInTheDocument();
|
|
53
70
|
});
|
|
71
|
+
|
|
54
72
|
describe('SIDEBAR', () => {
|
|
55
73
|
it('should render the sidebar component with correct heading', () => {
|
|
56
74
|
setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
|
|
@@ -105,23 +123,27 @@ describe('REPORT PAGE', () => {
|
|
|
105
123
|
|
|
106
124
|
describe('DOWNLOAD LINKS', () => {
|
|
107
125
|
let container;
|
|
108
|
-
beforeEach(() => {
|
|
126
|
+
beforeEach(async () => {
|
|
109
127
|
setMockJSONResult({ status: 200, responseJSON: shortResponseJSON });
|
|
110
|
-
|
|
128
|
+
await act(async () => {
|
|
129
|
+
container = render(<Report getCharacterWidth={jest.fn()} />).container;
|
|
130
|
+
});
|
|
111
131
|
});
|
|
112
132
|
describe('ALIGNMENT DOWNLOAD', () => {
|
|
113
133
|
it('should generate a blob url and filename for downloading alignment of all hits on render', () => {
|
|
114
|
-
const
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
expect(
|
|
134
|
+
const alignmentDownloadLink = container.querySelector('.download-alignment-of-all');
|
|
135
|
+
const hitLinks = container.querySelectorAll('.hit-links input[type="checkbox"]');
|
|
136
|
+
const expectedNumHits = hitLinks.length;
|
|
137
|
+
const fileName = `alignment-${expectedNumHits}_hits.txt`;
|
|
138
|
+
expect(alignmentDownloadLink.download).toEqual(fileName);
|
|
139
|
+
expect(alignmentDownloadLink.href).not.toEqual('#');
|
|
119
140
|
});
|
|
141
|
+
|
|
120
142
|
it('link for downloading alignment of specific number of selected hits should be disabled on initial load', () => {
|
|
121
143
|
const alignment_download_link = container.querySelector('.download-alignment-of-selected');
|
|
122
144
|
expect(alignment_download_link.classList.contains('disabled')).toBeTruthy();
|
|
123
|
-
|
|
124
145
|
});
|
|
146
|
+
|
|
125
147
|
it('should generate a blob url and filename for downloading alignment of specific number of selected hits', () => {
|
|
126
148
|
const alignment_download_link = container.querySelector('.download-alignment-of-selected');
|
|
127
149
|
// QUERY ALL HIT LINKS CHECKBOXES
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Import the necessary functions
|
|
2
|
+
import { tick_formatter, formatNumberUnits } from '../visualisation_helpers';
|
|
3
|
+
|
|
4
|
+
describe('tick_formatter', () => {
|
|
5
|
+
test('formats tick for amino_acid sequence', () => {
|
|
6
|
+
const formatter = tick_formatter(null, 'amino_acid');
|
|
7
|
+
expect(formatter(1_000)).toBe('1 kaa');
|
|
8
|
+
expect(formatter(25_286_936)).toBe('25.3 Maa');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test('formats tick for nucleic_acid sequence', () => {
|
|
12
|
+
const formatter = tick_formatter(null, 'nucleic_acid');
|
|
13
|
+
expect(formatter(1_000)).toBe('1 kbp');
|
|
14
|
+
expect(formatter(25_965_000)).toBe('26 Mbp');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('formatNumberUnits', () => {
|
|
19
|
+
test('formats number with units correctly', () => {
|
|
20
|
+
expect(formatNumberUnits('1.9k')).toBe('1.9 k');
|
|
21
|
+
expect(formatNumberUnits('1.5k')).toBe('1.5 k');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('rounds up correctly based on threshold', () => {
|
|
25
|
+
expect(formatNumberUnits('2.95k')).toBe('3 k');
|
|
26
|
+
expect(formatNumberUnits('2.94k')).toBe('2.9 k');
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import _ from 'underscore';
|
|
2
|
-
import d3 from 'd3';
|
|
2
|
+
import * as d3 from 'd3';
|
|
3
3
|
export function get_colors_for_evalue(evalue, hits) {
|
|
4
|
-
var colors = d3.
|
|
5
|
-
.log()
|
|
4
|
+
var colors = d3.scaleLog()
|
|
6
5
|
.domain([
|
|
7
6
|
d3.min([1e-5, d3.min(hits.map(function (d) {
|
|
8
7
|
if (parseFloat(d.evalue) === 0.0) return undefined;
|
|
@@ -24,40 +23,38 @@ export function toLetters(num) {
|
|
|
24
23
|
return pow ? toLetters(pow) + out : out;
|
|
25
24
|
}
|
|
26
25
|
|
|
26
|
+
export function getPrefix(str) {
|
|
27
|
+
if (str.length === 0) return '';
|
|
28
|
+
|
|
29
|
+
var lastChar = str.charAt(str.length - 1);
|
|
30
|
+
return /[a-zA-Z]/.test(lastChar) ? lastChar : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
|
-
|
|
34
|
+
* Defines how ticks will be formatted.
|
|
29
35
|
*
|
|
30
36
|
* Examples: 200 aa, 2.4 kbp, 7.6 Mbp.
|
|
31
37
|
*
|
|
32
38
|
* Borrowed from Kablammo. Modified by Priyam based on https://github.com/mbostock/d3/issues/1722.
|
|
33
39
|
*/
|
|
34
40
|
export function tick_formatter(scale, seq_type) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
var suffixes = {amino_acid: 'aa', nucleic_acid: 'bp'};
|
|
41
|
+
const prefix = d3.format('~s')
|
|
42
|
+
const suffixes = {amino_acid: 'aa', nucleic_acid: 'bp'};
|
|
38
43
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
var _ticks;
|
|
44
|
+
return function (d) {
|
|
45
|
+
const formatted = prefix(d);
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return format(prefix.scale(d));
|
|
47
|
-
});
|
|
48
|
-
digits++;
|
|
47
|
+
return `${formatNumberUnits(formatted)}${suffixes[seq_type]}`;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
49
50
|
|
|
50
|
-
|
|
51
|
+
export function formatNumberUnits(number) {
|
|
52
|
+
const suffix = number.replace(/[0-9.]/g, '');
|
|
53
|
+
const numericPart = parseFloat(number).toFixed(2);
|
|
54
|
+
// Round to 1 decimal place
|
|
55
|
+
const rounded = Math.round(numericPart * 10) / 10;
|
|
51
56
|
|
|
52
|
-
return
|
|
53
|
-
if (!prefix.symbol || d === scale.domain()[0]) {
|
|
54
|
-
return (d + ' ' + suffixes[seq_type]);
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
57
|
-
return (format(prefix.scale(d)) +
|
|
58
|
-
' ' + prefix.symbol + suffixes[seq_type]);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
57
|
+
return `${rounded % 1 === 0 ? rounded.toFixed(0) : rounded} ${suffix}`;
|
|
61
58
|
}
|
|
62
59
|
|
|
63
60
|
export function get_seq_type(algorithm) {
|
|
@@ -103,4 +100,4 @@ export function prettify_evalue(evalue) {
|
|
|
103
100
|
else
|
|
104
101
|
return base;
|
|
105
102
|
}
|
|
106
|
-
}
|
|
103
|
+
}
|