Skip to content

Commit

Permalink
Merge pull request #84 from metagenlab/nj/dal
Browse files Browse the repository at this point in the history
Handle groups in hit extraction view.
  • Loading branch information
njohner committed May 2, 2024
2 parents cda062a + 0e8836d commit 0ef129e
Show file tree
Hide file tree
Showing 14 changed files with 470 additions and 95 deletions.
1 change: 1 addition & 0 deletions conda/webapp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ dependencies:
- bibtexparser
- blast>=2.9.0
- gxx
- django-autocomplete-light
- pip:
- scoary-2
2 changes: 2 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ to [Common Changelog](https://common-changelog.org)

### Changed

- Handle groups in hit extraction view. ([#84](https://github.com/metagenlab/zDB/pull/84)) (Niklaus Johner)
- Allow using groups to define phenotype in GWAS view. ([#82](https://github.com/metagenlab/zDB/pull/82)) (Niklaus Johner)
- Display form validation errors next to the corresponding fields. ([#83](https://github.com/metagenlab/zDB/pull/83)) (Niklaus Johner)
- Filter VF hits by SeqID and coverage and keep one hit per locus. ([#77](https://github.com/metagenlab/zDB/pull/77)) (Niklaus Johner)
- Improve layout for various views, making better use of available space. ([#70](https://github.com/metagenlab/zDB/pull/70)) (Niklaus Johner)
Expand Down
141 changes: 141 additions & 0 deletions testing/webapp/test_autocomplete_views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import json
from contextlib import contextmanager

from django.conf import settings
from django.test import SimpleTestCase
from lib.db_utils import DB


class BaseAutocompleteTestCase(SimpleTestCase):

def make_request(self, **kwargs):
url = f'{self.base_url}?forward={json.dumps(kwargs)}'
return self.client.get(url)


class TestAutocompleteTaxid(BaseAutocompleteTestCase):

base_url = '/autocomplete_taxid/'

taxons = [
{'id': '1', 'text': 'Klebsiella pneumoniae R6724_16313'},
{'id': '2', 'text': 'Klebsiella pneumoniae R6726_16314'},
{'id': '3', 'text': 'Klebsiella pneumoniae R6728_16315'}]

groups = [
{'id': 'group:positive', 'text': 'positive'},
{'id': 'group:negative', 'text': 'negative'},
{'id': 'group:all', 'text': 'all'}]

plasmids = [
{'id': 'plasmid:1', 'text': 'Klebsiella pneumoniae R6724_16313 plasmid'},
{'id': 'plasmid:2', 'text': 'Klebsiella pneumoniae R6726_16314 plasmid'},
{'id': 'plasmid:3', 'text': 'Klebsiella pneumoniae R6728_16315 plasmid'}]

@contextmanager
def add_plasmid_for_taxids(self, taxids):
"""We need to commit this change so that it is picked up
by the autocomplete view, so we need to cleanup afterwards.
I guess we could also have isolated the DB by making a backup in setUp
and restoring it in tearDown, but seemed like overkill for now.
"""
try:
plasmid_term_id = self.db.server.adaptor.execute_one(
"SELECT term_id FROM term WHERE name='plasmid'")[0]
for taxid in taxids:
self.db.server.adaptor.execute(
f"UPDATE bioentry_qualifier_value SET value=1 "
f"WHERE bioentry_id={taxid} AND term_id={plasmid_term_id};")
self.db.server.commit()
yield
finally:
for taxid in taxids:
self.db.server.adaptor.execute(
f"UPDATE bioentry_qualifier_value SET value=0 "
f"WHERE bioentry_id={taxid} AND term_id={plasmid_term_id};")
self.db.server.commit()

def assertItemsEqual(self, expected, actual):
self.assertEqual(sorted(expected, key=lambda x: x["id"]),
sorted(actual, key=lambda x: x["id"]))

def setUp(self):
biodb_path = settings.BIODB_DB_PATH
self.db = DB.load_db_from_name(biodb_path)

def tearDown(self):
self.db.server.close()

def test_handles_include_plasmids(self):
with self.add_plasmid_for_taxids([1, 2]):
resp = self.make_request()
self.assertItemsEqual(
self.taxons + self.groups,
resp.json()["results"])

resp = self.make_request(include_plasmids=True)
self.assertItemsEqual(
self.taxons + self.groups + self.plasmids[:2],
resp.json()["results"])

def test_handles_exclude(self):
resp = self.make_request(exclude=["3"])
self.assertItemsEqual(
[self.taxons[0], self.taxons[1], self.groups[0]],
resp.json()["results"])

resp = self.make_request(exclude=["group:positive"])
self.assertItemsEqual(
[self.taxons[2], self.groups[1]],
resp.json()["results"])

resp = self.make_request(exclude=["3", "group:positive"])
self.assertItemsEqual(
[],
resp.json()["results"])

def test_handles_exclude_taxids_in_groups(self):
# ignored because these are not groups
resp = self.make_request(exclude_taxids_in_groups=["1", "3"])
self.assertItemsEqual(
self.taxons + self.groups,
resp.json()["results"])

resp = self.make_request(exclude_taxids_in_groups=["group:positive"])
self.assertItemsEqual(
[self.taxons[2]] + self.groups,
resp.json()["results"])

resp = self.make_request(exclude_taxids_in_groups=["group:positive",
"group:negative"])
self.assertItemsEqual(
self.groups,
resp.json()["results"])


class TestAutocompleteNMissing(BaseAutocompleteTestCase):

base_url = '/autocomplete_n_missing/'

@staticmethod
def get_expected_response(n):
return [{"id": i, "text": i} for i in range(n)]

def test_handles_include_plasmids(self):
included = ["1", "2", "plasmid:2", "plasmid:3"]
resp = self.make_request(included=included)
self.assertEqual(self.get_expected_response(2),
resp.json()["results"])

resp = self.make_request(included=included, include_plasmids=True)
self.assertEqual(self.get_expected_response(4),
resp.json()["results"])

def test_handles_groups(self):
resp = self.make_request(included=["group:positive"])
self.assertEqual(self.get_expected_response(2),
resp.json()["results"])

resp = self.make_request(included=["group:positive", "group:negative"])
self.assertEqual(self.get_expected_response(3),
resp.json()["results"])
133 changes: 133 additions & 0 deletions testing/webapp/test_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
from django.test import SimpleTestCase

from webapp.views.utils import AccessionFieldHandler


class TestAccessionFieldHandler(SimpleTestCase):

taxons = [('1', 'Klebsiella pneumoniae R6724_16313'),
('2', 'Klebsiella pneumoniae R6726_16314'),
('3', 'Klebsiella pneumoniae R6728_16315')]

plasmids = [('plasmid:1', 'Klebsiella pneumoniae R6724_16313 plasmid'),
('plasmid:2', 'Klebsiella pneumoniae R6726_16314 plasmid'),
('plasmid:3', 'Klebsiella pneumoniae R6728_16315 plasmid')]

groups = [('group:all', 'all'),
('group:negative', 'negative'),
('group:positive', 'positive')]

def setUp(self):
self.handler = AccessionFieldHandler()
# Because we will not commit, we need to make all modification
# on the handler's database
self.db = self.handler.db

def tearDown(self):
self.db.server.close()

def add_plasmid_for_taxids(self, taxids):
plasmid_term_id = self.db.server.adaptor.execute_one(
"SELECT term_id FROM term WHERE name='plasmid'")[0]
for taxid in taxids:
self.db.server.adaptor.execute(
f"UPDATE bioentry_qualifier_value SET value=1 "
f"WHERE bioentry_id={taxid} AND term_id={plasmid_term_id};")

def assertItemsEqual(self, expected, choices):
self.assertEqual(sorted(expected), sorted(choices))

def test_get_choices_handles_plasmids(self):
self.assertItemsEqual(
self.taxons + self.groups,
self.handler.get_choices())

self.add_plasmid_for_taxids([1, 3])
self.assertItemsEqual(
self.taxons + self.groups + self.plasmids[::2],
self.handler.get_choices())

self.assertItemsEqual(
self.taxons + self.groups,
self.handler.get_choices(with_plasmids=False))

def test_get_choices_handles_groups(self):
self.assertItemsEqual(
self.taxons + self.groups,
self.handler.get_choices())

def test_get_choices_handles_taxid_exclusion(self):
exclude = [el[0] for el in self.taxons[1:]]
self.assertItemsEqual(
[self.taxons[0]],
self.handler.get_choices(exclude=exclude))

self.add_plasmid_for_taxids([1, 2, 3])
self.assertItemsEqual(self.taxons + self.groups + self.plasmids,
self.handler.get_choices())

exclude = [self.taxons[-1][0]]
self.assertItemsEqual(self.taxons[:-1] + [self.groups[2]] + self.plasmids,
self.handler.get_choices(exclude=exclude))

self.assertItemsEqual(self.taxons[:-1] + [self.groups[2]],
self.handler.get_choices(exclude=exclude,
with_plasmids=False))

def test_get_choices_handles_plasmid_exclusion(self):
self.add_plasmid_for_taxids([1, 2, 3])

exclude = [self.plasmids[-1][0]]
self.assertItemsEqual(self.taxons + self.groups + self.plasmids[:-1],
self.handler.get_choices(exclude=exclude))

exclude = [el[0] for el in self.plasmids]
self.assertItemsEqual(self.taxons + self.groups,
self.handler.get_choices(exclude=exclude))

def test_get_choices_handles_group_exclusion(self):
exclude = [self.groups[1][0]]
self.assertItemsEqual(self.taxons[:-1] + [self.groups[2]],
self.handler.get_choices(exclude=exclude))

exclude.append(self.groups[2][0])
self.assertItemsEqual([],
self.handler.get_choices(exclude=exclude))

def test_get_choices_handles_exclude_taxids_in_groups(self):
exclude = [self.groups[1][0]]
self.assertItemsEqual(
self.taxons[:-1] + self.groups,
self.handler.get_choices(exclude_taxids_in_groups=exclude))

exclude.append(self.groups[2][0])
self.assertItemsEqual(
self.groups,
self.handler.get_choices(exclude_taxids_in_groups=exclude))

def test_extract_choices_returns_none_when_include_plasmids_is_false(self):
self.assertEqual(
([1, 3], None),
self.handler.extract_choices(["1", "3"], False))

self.assertEqual(
([1, 3], []),
self.handler.extract_choices(["1", "3"], True))

def test_extract_choices_handles_plasmids(self):
self.assertEqual(
([2], [1, 3]),
self.handler.extract_choices(["plasmid:1", "2", "plasmid:3"], True))

self.assertEqual(
([2], None),
self.handler.extract_choices(["plasmid:1", "2", "plasmid:3"], False))

def test_extract_choices_handles_groups(self):
self.assertEqual(
([3], []),
self.handler.extract_choices(["group:negative"], True))

self.assertEqual(
([2, 3], [1]),
self.handler.extract_choices(["plasmid:1", "2", "group:negative"], True))
12 changes: 11 additions & 1 deletion testing/webapp/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
urls = [
'/about',
'/amr_comparison',
'/autocomplete_n_missing/',