Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add views to add, delete, and display groups. #86

Merged
merged 13 commits into from
May 7, 2024
Merged
1 change: 1 addition & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ to [Common Changelog](https://common-changelog.org)

### Added

- Add views to add, delete, and display groups. ([#86](https://github.com/metagenlab/zDB/pull/86)) (Niklaus Johner)
- Allow defining groups of genomes in input file. ([#82](https://github.com/metagenlab/zDB/pull/82)) (Niklaus Johner)
- Add view to produce custom plots (phylogenetic trees and table). ([#78](https://github.com/metagenlab/zDB/pull/78)) (Niklaus Johner)
- Add AMRs and VFs to search index. ([#73](https://github.com/metagenlab/zDB/pull/73)) (Niklaus Johner)
Expand Down
53 changes: 53 additions & 0 deletions testing/webapp/test_groups.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from django.conf import settings
from django.test import SimpleTestCase
from lib.db_utils import DB


class TestGroupsViews(SimpleTestCase):

def test_groups_overview(self):
resp = self.client.get("/groups/")
self.assertEqual(200, resp.status_code)
self.assertTemplateUsed(resp, 'chlamdb/groups_overview.html')

groups = [
'<a href=/groups/positive>positive</a>',
'<a href=/groups/negative>negative</a>',
'<a href=/groups/all>all</a>']
self.assertEqual(groups, resp.context["groups"])
for group in groups:
self.assertContains(resp, group)

self.assertContains(resp, 'Add new group')

def test_add_and_delete_group_view(self):
resp = self.client.get("/groups/add/")
self.assertEqual(200, resp.status_code)
self.assertTemplateUsed(resp, 'chlamdb/group_add.html')
self.assertEqual(list(resp.context.get("form").fields.keys()),
['group_name', 'genomes'])

db = DB.load_db_from_name(settings.BIODB_DB_PATH)
data = {"group_name": "test_group", "genomes": ["group:negative", "1"]}
resp = self.client.post("/groups/add/", data=data)
self.assertEqual(302, resp.status_code)
self.assertEqual('/groups/test_group', resp.url)
self.assertTrue(db.get_group("test_group"))

resp = self.client.post("/groups/test_group/delete/")
self.assertEqual(302, resp.status_code)
self.assertEqual('/groups/', resp.url)
self.assertFalse(db.get_group("test_group"))

def test_group_details_view(self):
resp = self.client.get("/groups/positive")
self.assertEqual(200, resp.status_code)
self.assertTemplateUsed(resp, 'chlamdb/group_details.html')
genomes = [
'<a href="/extract_contigs/1">Klebsiella pneumoniae R6724_16313</a>',
'<a href="/extract_contigs/2">Klebsiella pneumoniae R6726_16314</a>']
self.assertEqual(
[row.accession for i, row in resp.context["genome_table"]["table_data"].iterrows()],
genomes)
for genome in genomes:
self.assertContains(resp, genome, html=True)
4 changes: 4 additions & 0 deletions testing/webapp/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

untested_patterns = {
'^favicon\\.ico$',
r'^groups/([a-zA-Z0-9_\.\(\)\-\'\s]+)/delete/$',
'^module_cat_info/([a-zA-Z0-9_\\.]+)/([a-zA-Z0-9_\\.\\+-]+)$',
'^plot_region/$',
'^robots.txt$',
Expand Down Expand Up @@ -55,6 +56,9 @@
'/FAQ',
'/genomes',
'/get_cog/3/L?h=1&h=2&h=3',
'/groups/',
'/groups/add/',
'/groups/positive',
'/gwas_amr/',
'/gwas_cog/',
'/gwas_ko/',
Expand Down
2 changes: 2 additions & 0 deletions webapp/assets/css/style_FILE_zDB.css
Original file line number Diff line number Diff line change
Expand Up @@ -1781,3 +1781,5 @@ form.example input[type=text] {
.bootstrap-select.form-control.input-group-btn {
z-index: auto;
}

a.button_link { text-decoration: none; color: rgb(255, 255, 255);}
Binary file added webapp/assets/img/icons8-group-96.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
48 changes: 48 additions & 0 deletions webapp/chlamdb/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -712,3 +712,51 @@ def clean_entries(self):
code="invalid")
entries.append(self.Entry(entry_id, label, object_type))
return entries


def make_group_add_form(db):

accession_choices = AccessionFieldHandler().get_choices(
with_plasmids=False)

class GroupAddForm(forms.Form):
group_name = forms.CharField(max_length=50,
required=True)
genomes = forms.MultipleChoiceField(
choices=accession_choices,
widget=Select2Multiple(
url="autocomplete_taxid",
forward=(forward.Field("genomes", "exclude_taxids_in_groups"),),
attrs={"data-close-on-select": "false",
"data-placeholder": "Nothing selected"}),
required=True)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.db = db
self.helper = FormHelper()
self.helper.form_method = "post"

self.helper.layout = Layout(
Fieldset(
" ",
Row('group_name'),
Row('genomes', style="margin-top:1em"),
Submit('submit', 'Submit',
style="padding-left:15px; margin-top:15px; margin-bottom:15px "),
css_class="col-lg-5 col-md-6 col-sm-6")
)

def clean_group_name(self):
group_name = self.cleaned_data["group_name"]
if self.db.get_group(group_name):
raise ValidationError(f'Group "{group_name}" already exists.')
return group_name

def get_taxids(self):
indices = self.cleaned_data["genomes"]
taxids, plasmids = AccessionFieldHandler().extract_choices(
indices, False)
return taxids

return GroupAddForm
8 changes: 6 additions & 2 deletions webapp/chlamdb/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from django.urls import re_path
from django.views.generic import TemplateView
from django.views.generic.base import RedirectView
from views import (autocomplete, custom_plots, entry_lists, fam, gwas,
from views import (autocomplete, custom_plots, entry_lists, fam, groups, gwas,
hits_extraction, locus, tabular_comparison, venn, views)

favicon_view = RedirectView.as_view(url='/assets/favicon.ico', permanent=True)
Expand Down Expand Up @@ -52,8 +52,12 @@
re_path(r'^gwas_ko/', gwas.KoGwasView.as_view(), name="gwas_ko"),
re_path(r'^gwas_cog/', gwas.CogGwasView.as_view(), name="gwas_cog"),
re_path(r'^gwas_amr/', gwas.AmrGwasView.as_view(), name="gwas_amr"),
re_path(r'^groups/add/$', groups.GroupAdd.as_view(), name=groups.GroupAdd.view_name), # noqa
re_path(r'^groups/([a-zA-Z0-9_\.\(\)\-\'\s]+)/delete/$', groups.GroupDelete.as_view(), name=groups.GroupDelete.view_name), # noqa
re_path(r'^groups/([a-zA-Z0-9_\.\(\)\-\'\s]+)', groups.GroupDetails.as_view(), name=groups.GroupDetails.view_name), # noqa
re_path(r'^groups/$', groups.GroupsOverview.as_view(), name=groups.GroupsOverview.view_name), # noqa
re_path(r'^get_cog/([a-zA-Z0-9_\.]+)/([a-zA-Z0-9_\.\%]+)$', views.get_cog, name="get_cog"), # noqa
re_path(r'^genomes', views.genomes, name='genomes'),
re_path(r'^genomes', views.Genomes.as_view(), name=views.Genomes.view_name),
re_path(r'^favicon\.ico$', favicon_view),
re_path(r'^FAQ', views.faq, name='FAQ'),
re_path(r'^fam_vf/(VFG[0-9]+)$', fam.FamVfView.as_view(), name="fam_vf"),
Expand Down
43 changes: 32 additions & 11 deletions webapp/lib/db_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1270,13 +1270,17 @@ def get_term_id(self, term, create_if_absent=False, ontology="Annotation Tags"):
gc_term_id = result[0][0]
return gc_term_id

def get_genomes_description(self):
def get_genomes_description(self, taxids=None):
"""
Returns the description of the genome as it has been read from the
genbank files, indexed by taxon_id. The output also contains a flag
has_plasmid indicating whether the genome contains a plasmid or not.
"""

if taxids:
plchd = self.gen_placeholder_string(taxids)
where_clause = f"WHERE entry.taxon_id IN ({plchd})"
else:
where_clause = ""
has_plasmid_query = (
"SELECT * "
"FROM bioentry_qualifier_value AS has_plasmid "
Expand All @@ -1293,13 +1297,13 @@ def get_genomes_description(self):
"INNER JOIN taxon_name as txn_name ON entry.taxon_id=txn_name.taxon_id "
"INNER JOIN term AS orga_term ON orga.term_id=orga_term.term_id "
" AND orga_term.name=\"organism\" "
"GROUP BY entry.taxon_id;"
f"{where_clause} GROUP BY entry.taxon_id;"
)
descr = self.server.adaptor.execute_and_fetchall(query)
descr = self.server.adaptor.execute_and_fetchall(query, taxids)
columns = ["taxon_id", "description", "has_plasmid"]
return DB.to_pandas_frame(descr, columns).set_index(["taxon_id"])

def get_genomes_infos(self):
def get_genomes_infos(self, taxids=None):
"""
Note: for efficiency sake, it would be possible to order the different
tables by taxon_id, walk through the table using zip in an iteator
Expand All @@ -1308,30 +1312,35 @@ def get_genomes_infos(self):
However, given the small size of the dataset, it's probably not worth
the implementation effort.
"""
if taxids:
plchd = self.gen_placeholder_string(taxids)
where_clause = f"WHERE taxon_id IN ({plchd})"
else:
where_clause = ""

query = (
"SELECT entry.taxon_id, entry.taxon_id, COUNT(*) "
" FROM seqfeature AS seq "
" INNER JOIN term AS cds ON cds.term_id = seq.type_term_id AND cds.name=\"CDS\" "
" INNER JOIN bioentry AS entry ON entry.bioentry_id = seq.bioentry_id "
" GROUP BY entry.taxon_id;"
f"{where_clause} GROUP BY entry.taxon_id;"
)
n_prot_results = self.server.adaptor.execute_and_fetchall(query)
n_prot_results = self.server.adaptor.execute_and_fetchall(query, taxids)

query = (
"SELECT entry.taxon_id, COUNT(*) "
"FROM bioentry AS entry "
"GROUP BY entry.taxon_id;"
f"{where_clause} GROUP BY entry.taxon_id;"
)
n_contigs = self.server.adaptor.execute_and_fetchall(query)
n_contigs = self.server.adaptor.execute_and_fetchall(query, taxids)

cols = ["taxon_id", "completeness", "contamination",
"gc", "length", "coding_density"]
query = (
"SELECT taxon_id, completeness, contamination, gc, length, "
"coding_density from genome_summary;"
f"coding_density from genome_summary {where_clause};"
)
all_other_results = self.server.adaptor.execute_and_fetchall(query)
all_other_results = self.server.adaptor.execute_and_fetchall(query, taxids)

df_n_prot = DB.to_pandas_frame(
n_prot_results, ["taxon_id", "id", "n_prot"]).set_index("taxon_id")
Expand Down Expand Up @@ -1476,6 +1485,18 @@ def load_groups(self, groups, group_taxons):
self.load_data_into_table("groups", groups)
self.load_data_into_table("taxon_in_group", group_taxons)

def delete_group(self, group_name):
sql = f"DELETE FROM taxon_in_group WHERE group_name='{group_name}';"
self.server.adaptor.execute(sql)
sql = f"DELETE FROM groups WHERE group_name='{group_name}';"
self.server.adaptor.execute(sql)
self.commit()
return

def get_group(self, group_name):
query = f"SELECT * FROM groups WHERE group_name='{group_name}' LIMIT 1;"
return self.server.adaptor.execute_and_fetchall(query)

def get_groups(self):
query = "SELECT * FROM groups;"
return self.server.adaptor.execute_and_fetchall(query)
Expand Down
115 changes: 35 additions & 80 deletions webapp/templates/chlamdb/genomes.html
Original file line number Diff line number Diff line change
@@ -1,91 +1,46 @@

<!DOCTYPE html>


<html>

<head>

<link rel= "stylesheet" href="style_home.css">
{% load static %}
{% load custom_tags %}
{% include "chlamdb/header.html" %}
{% include "chlamdb/style_menu.html" %}
<link rel= "stylesheet" href="style_home.css">
{% load static %}
{% load custom_tags %}
{% include "chlamdb/header.html" %}
{% include "chlamdb/style_menu.html" %}

</head>

<body>
<div class="container-fluid" id="main_container">
<div class="row">
<div id="wrapper">
<div id="page-content-wrapper">
<div class="row">
<div class="col-lg-12">
{% include "chlamdb/menu.html" %}

<hr class="lines-home">
<div class="tab-pane" id="genomes" style="margin-right: 1em; overflow-x:auto;">

<table class="display" id="genomes_table">
<thead>


<tr>
{% for header in data_table_header %}
<th>{{header}}</th>
{% endfor %}

</tr>
</thead>
<tbody>
{% for entry in data_table %}
<tr>
<td><a href="{% url 'extract_contigs' entry.0 %}">{{entry.1}}</a></td>
<td>{{entry.2}}</td>
<td>{{entry.3}}</td>
<td>{{entry.4}}</td>
<td>{{entry.5}}</td>
<td>{{entry.6}}</td>
<td>{{entry.7}}</td>
<td><a href="{{ entry.8 }}"> .faa </a></td>
<td><a href="{{ entry.9 }}"> .fna </a></td>
<td><a href="{{ entry.10 }}"> .ffn </a></td>
<td><a href="{{ entry.11 }}"> .gbk </a></td>
</tr>
{% endfor %}
</tbody>
</table>

<div class="panel panel-success" style=" width:100%; margin-top: 2em;">
<div class="panel-heading" style="width:100%;">
<h3 class="panel-title">Help</h3>
</div>

<p style="padding:1em 3em 1em 1em; line-height: 160% ; width:100%" >This table contains the list of genomes included in the database and a summary of their content. <br> Clicking on the genome name (first column) a second table with the protein content is displayed. It shows to which contig each protein belongs and provides the link to the locus tags.
<br> Fasta and gbk files can be downloaded.
</p>
</div>
</div>

<script>

$(document).ready(function() {
$("#genomes_table").DataTable({
columns: [
{ data: "genome", className: "editable" },
{ data: "GC" },
{ data: "n_prot"},
{ data: "n_contigs"},
{ data: "size" },
{ data: "perc_coding"},
{ data: "has_plasmid"},
{ data: "faa_seq"},
{ data: "fna_seq"},
{ data: "ffn_seq"},
{ data: "gbk_file"}
]
});}
);

</script>
<div class="container-fluid" id="main_container">
<div class="row">
<div id="wrapper">
<div id="page-content-wrapper">
<div class="row">
<div class="col-lg-12">
{% include "chlamdb/menu.html" %}

<hr class="lines-home">
<div class="tab-pane" id="genomes" style="margin-right: 1em; overflow-x:auto;">

{% include "chlamdb/result_table.html" with results=results %}

<div class="panel panel-success" style=" width:100%; margin-top: 2em;">
<div class="panel-heading" style="width:100%;">
<h3 class="panel-title">Help</h3>
</div>

<p style="padding:1em 3em 1em 1em; line-height: 160% ; width:100%" >
{{results.table_help|safe}}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
Loading