sequenceserver 3.1.1 → 3.1.3
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 +24 -0
- data/lib/sequenceserver/blast/tasks.rb +1 -1
- data/lib/sequenceserver/blast.rb +6 -0
- data/lib/sequenceserver/database.rb +13 -0
- data/lib/sequenceserver/routes.rb +1 -1
- data/lib/sequenceserver/sequence.rb +1 -2
- data/lib/sequenceserver/version.rb +1 -1
- data/lib/sequenceserver.rb +1 -1
- data/public/css/app.min.css +1 -1
- data/public/css/sequenceserver.css +0 -18
- data/public/css/sequenceserver.min.css +2 -2
- data/public/js/alignment_exporter.js +16 -28
- data/public/js/cloud_share_modal.js +42 -42
- data/public/js/form.js +12 -10
- data/public/js/grapher.js +4 -4
- data/public/js/hit.js +3 -3
- data/public/js/hits.js +276 -0
- data/public/js/jquery_world.js +1 -1
- data/public/js/mailto.js +1 -3
- data/public/js/null_plugins/report_plugins.js +1 -0
- data/public/js/options.js +2 -6
- data/public/js/query.js +1 -1
- data/public/js/report.js +68 -252
- data/public/js/report_root.js +7 -5
- data/public/js/search.js +28 -11
- data/public/js/sequence.js +158 -158
- data/public/js/sequence_modal.js +28 -36
- data/public/js/sidebar.js +7 -6
- 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/report.spec.js +37 -15
- 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 +300 -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 +292 -0
- data/public/sequenceserver-search.min.js.map +1 -0
- data/views/layout.erb +3 -7
- data/views/search.erb +1 -1
- data/views/search_layout.erb +1 -1
- metadata +11 -5
- data/public/config.js +0 -147
- data/public/packages/jquery-ui@1.11.4.js +0 -16624
data/public/js/sequence_modal.js
CHANGED
@@ -63,48 +63,38 @@ export default class SequenceModal extends React.Component {
|
|
63
63
|
/**
|
64
64
|
* Loads sequence using AJAX and updates modal state.
|
65
65
|
*/
|
66
|
-
loadJSON(url) {
|
66
|
+
async loadJSON(url) {
|
67
67
|
// Fetch sequence and update state.
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
requestCompleted: true,
|
75
|
-
});
|
76
|
-
}, this)
|
77
|
-
)
|
78
|
-
.fail((jqXHR, status, error) => {
|
79
|
-
this.hide();
|
80
|
-
this.props.showErrorModal(jqXHR.responseJSON);
|
68
|
+
try {
|
69
|
+
const response = await $.getJSON(url);
|
70
|
+
this.setState({
|
71
|
+
sequences: response.sequences,
|
72
|
+
error_msgs: response.error_msgs,
|
73
|
+
requestCompleted: true,
|
81
74
|
});
|
75
|
+
} catch (error) {
|
76
|
+
console.log('Error fetching sequence:', error);
|
77
|
+
this.hide();
|
78
|
+
this.props.showErrorModal(error.responseJSON);
|
79
|
+
}
|
82
80
|
}
|
83
81
|
|
84
82
|
resultsJSX() {
|
85
83
|
return (
|
86
84
|
<div className="modal-body">
|
87
|
-
{
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
}, this)
|
101
|
-
)}
|
102
|
-
{_.map(
|
103
|
-
this.state.sequences,
|
104
|
-
_.bind(function (sequence) {
|
105
|
-
return <SequenceViewer sequence={sequence} />;
|
106
|
-
}, this)
|
107
|
-
)}
|
85
|
+
{this.state.error_msgs.map((error_msg, index) => (
|
86
|
+
<div key={`error-message-${index}`} className="fastan">
|
87
|
+
<div className="section-header">
|
88
|
+
<h4>{error_msg[0]}</h4>
|
89
|
+
</div>
|
90
|
+
<div className="section-content">
|
91
|
+
<pre className="pre-reset">{error_msg[1]}</pre>
|
92
|
+
</div>
|
93
|
+
</div>
|
94
|
+
))}
|
95
|
+
{this.state.sequences.map((sequence, index) => (
|
96
|
+
<SequenceViewer key={`sequence-viewer-${index}`} sequence={sequence} />
|
97
|
+
))}
|
108
98
|
</div>
|
109
99
|
);
|
110
100
|
}
|
@@ -160,6 +150,8 @@ class SequenceViewer extends React.Component {
|
|
160
150
|
footer: false,
|
161
151
|
},
|
162
152
|
});
|
163
|
-
|
153
|
+
setTimeout(function() {
|
154
|
+
requestAnimationFrame(() => { widget.hideFormatSelector() }); // ensure React is done painting the DOM of the element before calling a function on it.
|
155
|
+
});
|
164
156
|
}
|
165
157
|
}
|
data/public/js/sidebar.js
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Component } from 'react';
|
1
|
+
import React, { Component } from 'react';
|
2
2
|
import _ from 'underscore';
|
3
3
|
|
4
4
|
import downloadFASTA from './download_fasta';
|
@@ -32,6 +32,7 @@ export default class extends Component {
|
|
32
32
|
this.copyURL = this.copyURL.bind(this);
|
33
33
|
this.shareCloudInit = this.shareCloudInit.bind(this);
|
34
34
|
this.sharingPanelJSX = this.sharingPanelJSX.bind(this);
|
35
|
+
this.cloudShareModal = React.createRef();
|
35
36
|
this.timeout = null;
|
36
37
|
this.queryElems = [];
|
37
38
|
this.state = {
|
@@ -203,7 +204,7 @@ export default class extends Component {
|
|
203
204
|
}
|
204
205
|
|
205
206
|
shareCloudInit() {
|
206
|
-
this.
|
207
|
+
this.cloudShareModal.current.show();
|
207
208
|
}
|
208
209
|
|
209
210
|
topPanelJSX() {
|
@@ -353,9 +354,9 @@ export default class extends Component {
|
|
353
354
|
{
|
354
355
|
!this.props.data.imported_xml && <li>
|
355
356
|
<a className="btn-link download" data-toggle="tooltip"
|
356
|
-
title="Results in
|
357
|
+
title="Results in text format."
|
357
358
|
href={'download/' + this.props.data.search_id + '.pairwise'}>
|
358
|
-
Full
|
359
|
+
Full Text report
|
359
360
|
</a>
|
360
361
|
</li>
|
361
362
|
}
|
@@ -403,7 +404,7 @@ export default class extends Component {
|
|
403
404
|
</ul>
|
404
405
|
{
|
405
406
|
<CloudShareModal
|
406
|
-
ref=
|
407
|
+
ref={this.cloudShareModal}
|
407
408
|
querydb={this.props.data.querydb}
|
408
409
|
program={this.props.data.program}
|
409
410
|
queryLength={this.props.data.queries.length}
|
@@ -422,7 +423,7 @@ export default class extends Component {
|
|
422
423
|
<div className="referral-panel">
|
423
424
|
<div className="section-header-sidebar">
|
424
425
|
<h4>Recommend SequenceServer</h4>
|
425
|
-
<p><a href="https://sequenceserver.com/referral-program" target="_blank">Earn up to $
|
426
|
+
<p><a href="https://sequenceserver.com/referral-program" target="_blank">Earn up to $400 per signup</a></p>
|
426
427
|
</div>
|
427
428
|
</div>
|
428
429
|
</div>
|
@@ -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
|