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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sequenceserver/api_errors.rb +18 -0
  3. data/lib/sequenceserver/blast/hit.rb +1 -2
  4. data/lib/sequenceserver/blast/job.rb +15 -5
  5. data/lib/sequenceserver/blast/report.rb +5 -1
  6. data/lib/sequenceserver/blast/tasks.rb +1 -1
  7. data/lib/sequenceserver/database.rb +5 -1
  8. data/lib/sequenceserver/job.rb +26 -10
  9. data/lib/sequenceserver/makeblastdb.rb +2 -1
  10. data/lib/sequenceserver/routes.rb +38 -13
  11. data/lib/sequenceserver/version.rb +1 -1
  12. data/lib/sequenceserver/zip_file_generator.rb +1 -1
  13. data/lib/sequenceserver.rb +1 -1
  14. data/public/404.html +8 -16
  15. data/public/css/app.css +794 -111
  16. data/public/css/app.min.css +1 -1
  17. data/public/css/sequenceserver.min.css +1 -8
  18. data/public/js/alignment_exporter.js +16 -28
  19. data/public/js/circos.js +103 -96
  20. data/public/{packages/circosJS@1.7.0.js → js/circosjs.js} +156 -778
  21. data/public/js/cloud_share_modal.js +95 -98
  22. data/public/js/collapse_preferences.js +13 -15
  23. data/public/js/databases.js +4 -3
  24. data/public/js/databases_tree.js +51 -30
  25. data/public/js/error_modal.js +35 -22
  26. data/public/js/exporter.js +2 -0
  27. data/public/js/form.js +55 -17
  28. data/public/js/grapher.js +94 -92
  29. data/public/js/hit.js +24 -22
  30. data/public/js/hits.js +283 -0
  31. data/public/js/hits_overview.js +8 -27
  32. data/public/js/hsp.js +140 -144
  33. data/public/js/jquery_world.js +25 -16
  34. data/public/js/kablammo.js +14 -10
  35. data/public/js/length_distribution.js +23 -37
  36. data/public/js/mailto.js +1 -3
  37. data/public/js/null_plugins/report_plugins.js +4 -5
  38. data/public/js/options.js +19 -25
  39. data/public/js/query.js +36 -35
  40. data/public/js/report/alignment_results.js +18 -0
  41. data/public/js/report/graphical_overview.js +38 -0
  42. data/public/js/report/report_header.js +28 -0
  43. data/public/js/report/run_summary.js +43 -0
  44. data/public/js/report.js +124 -348
  45. data/public/js/report_root.js +10 -8
  46. data/public/js/search.js +31 -11
  47. data/public/js/search_button.js +85 -78
  48. data/public/js/sequence.js +169 -180
  49. data/public/js/sequence_modal.js +58 -65
  50. data/public/js/share_url.js +5 -5
  51. data/public/js/sidebar.js +152 -89
  52. data/public/js/svgExporter.js +8 -3
  53. data/public/js/tests/alignment_exporter.spec.js +38 -0
  54. data/public/js/tests/cloud_share_modal.spec.js +75 -0
  55. data/public/js/tests/form.spec.js +1 -1
  56. data/public/js/tests/report.spec.js +37 -15
  57. data/public/js/tests/visualisation_helpers.spec.js +28 -0
  58. data/public/js/visualisation_helpers.js +24 -27
  59. data/public/packages/jquery-ui@1.13.3.js +19070 -0
  60. data/public/sequenceserver-report.min.js +3 -2481
  61. data/public/sequenceserver-report.min.js.LICENSE.txt +213 -0
  62. data/public/sequenceserver-report.min.js.map +1 -0
  63. data/public/sequenceserver-search.min.js +3 -2382
  64. data/public/sequenceserver-search.min.js.LICENSE.txt +262 -0
  65. data/public/sequenceserver-search.min.js.map +1 -0
  66. data/views/error.erb +1 -1
  67. data/views/layout.erb +122 -133
  68. data/views/report.erb +3 -2
  69. data/views/search.erb +10 -14
  70. metadata +21 -28
  71. data/public/config.js +0 -147
  72. data/public/css/fonts.css +0 -31
  73. data/public/css/grapher.css +0 -670
  74. data/public/css/sequenceserver.css +0 -527
  75. data/public/packages/jquery-ui@1.11.4.js +0 -16624
  76. data/public/vendor/github/mbostock/d3@3.5.6/d3.js +0 -9506
  77. data/public/vendor/github/mbostock/d3@3.5.6.js +0 -1
  78. 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
+ });
@@ -12,7 +12,7 @@ export const setMockJSONResult = (result) => {
12
12
  global.$.getJSON = (_, cb) => cb(result);
13
13
  };
14
14
 
15
- describe('ADVANCED PARAMETERS', () => {
15
+ describe('PARAMETERS', () => {
16
16
  let csrfMetaTag;
17
17
 
18
18
  beforeEach(() => {
@@ -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.$.getJSON = () => ({ complete: jest.fn((callback) => callback(result)) });
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
- setMockJSONResult({ status: 500 });
44
- render(<Report showErrorModal={showErrorModal} />);
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
- container = render(<Report getCharacterWidth={jest.fn()} />).container;
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 alignment_download_link = container.querySelector('.download-alignment-of-all');
115
- const expected_num_hits = container.querySelectorAll('.hit-links input[type="checkbox"]').length;
116
- const file_name = `alignment-${expected_num_hits}_hits.txt`;
117
- expect(alignment_download_link.download).toEqual(file_name);
118
- expect(alignment_download_link.hred).not.toEqual('#');
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.scale
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
- * Defines how ticks will be formatted.
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
- var ticks = scale.ticks();
36
- var prefix = d3.formatPrefix(ticks[ticks.length - 1]);
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
- var digits = 0;
40
- var format;
41
- var _ticks;
44
+ return function (d) {
45
+ const formatted = prefix(d);
42
46
 
43
- do {
44
- format = d3.format('.' + digits + 'f');
45
- _ticks = scale.ticks().map(function (d) {
46
- return format(prefix.scale(d));
47
- });
48
- digits++;
47
+ return `${formatNumberUnits(formatted)}${suffixes[seq_type]}`;
48
+ };
49
+ }
49
50
 
50
- } while (_ticks.length !== _.uniq(_ticks).length);
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 function (d) {
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
+ }