diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 16e86757b..e6f8a8157 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -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) diff --git a/testing/webapp/test_groups.py b/testing/webapp/test_groups.py new file mode 100644 index 000000000..717177bdf --- /dev/null +++ b/testing/webapp/test_groups.py @@ -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 = [ + 'positive', + 'negative', + 'all'] + 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 = [ + 'Klebsiella pneumoniae R6724_16313', + 'Klebsiella pneumoniae R6726_16314'] + 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) diff --git a/testing/webapp/test_views.py b/testing/webapp/test_views.py index 3a8da3cd6..7fba11b5e 100644 --- a/testing/webapp/test_views.py +++ b/testing/webapp/test_views.py @@ -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$', @@ -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/', diff --git a/webapp/assets/css/style_FILE_zDB.css b/webapp/assets/css/style_FILE_zDB.css index fb5fd61f1..9291f06d9 100644 --- a/webapp/assets/css/style_FILE_zDB.css +++ b/webapp/assets/css/style_FILE_zDB.css @@ -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);} diff --git a/webapp/assets/img/icons8-group-96.png b/webapp/assets/img/icons8-group-96.png new file mode 100644 index 000000000..ead3cc9c4 Binary files /dev/null and b/webapp/assets/img/icons8-group-96.png differ diff --git a/webapp/chlamdb/forms.py b/webapp/chlamdb/forms.py index fc3c37497..3ff7152e8 100644 --- a/webapp/chlamdb/forms.py +++ b/webapp/chlamdb/forms.py @@ -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 diff --git a/webapp/chlamdb/urls.py b/webapp/chlamdb/urls.py index 31f9f4c52..2237b5048 100644 --- a/webapp/chlamdb/urls.py +++ b/webapp/chlamdb/urls.py @@ -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) @@ -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"), diff --git a/webapp/lib/db_utils.py b/webapp/lib/db_utils.py index e6cc2e105..fe9415b94 100644 --- a/webapp/lib/db_utils.py +++ b/webapp/lib/db_utils.py @@ -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 " @@ -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 @@ -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") @@ -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) diff --git a/webapp/templates/chlamdb/genomes.html b/webapp/templates/chlamdb/genomes.html index cacb50162..385076e81 100644 --- a/webapp/templates/chlamdb/genomes.html +++ b/webapp/templates/chlamdb/genomes.html @@ -1,91 +1,46 @@ - - - - {% load static %} - {% load custom_tags %} - {% include "chlamdb/header.html" %} - {% include "chlamdb/style_menu.html" %} + + {% load static %} + {% load custom_tags %} + {% include "chlamdb/header.html" %} + {% include "chlamdb/style_menu.html" %} -
-
-
-
-
-
- {% include "chlamdb/menu.html" %} - -
-
- - - - - - - {% for header in data_table_header %} - - {% endfor %} - - - - - {% for entry in data_table %} - - - - - - - - - - - - - - {% endfor %} - -
{{header}}
{{entry.1}}{{entry.2}}{{entry.3}}{{entry.4}}{{entry.5}}{{entry.6}}{{entry.7}} .faa .fna .ffn .gbk
- -
-
-

Help

-
- -

This table contains the list of genomes included in the database and a summary of their content.
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. -
Fasta and gbk files can be downloaded. -

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

Help

+
+ +

+ {{results.table_help|safe}} +

+
+
+
+
+
+
+
+
+ diff --git a/webapp/templates/chlamdb/group_add.html b/webapp/templates/chlamdb/group_add.html new file mode 100644 index 000000000..8510f4a6e --- /dev/null +++ b/webapp/templates/chlamdb/group_add.html @@ -0,0 +1,36 @@ + + + + + +{% include "chlamdb/header.html" %} +{% load custom_tags %} + + + +
+
+
+
+
+
+ {% include "chlamdb/menu.html" %} +

+ {{description|safe}} +

+ {% include "chlamdb/error.html" %} + {% load crispy_forms_tags %} + {% block content %} + {% csrf_token %} + {% crispy form %} + {% endblock %} +
+
+
+
+
+
+ + +{% include "chlamdb/style_menu.html" %} + diff --git a/webapp/templates/chlamdb/group_details.html b/webapp/templates/chlamdb/group_details.html new file mode 100644 index 000000000..37490cca8 --- /dev/null +++ b/webapp/templates/chlamdb/group_details.html @@ -0,0 +1,79 @@ + + + + + +{% include "chlamdb/header.html" %} +{% load custom_tags %} + + + +
+
+
+
+
+
+ {% include "chlamdb/menu.html" %} +

+ {{description|safe}} +

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

Help

+
+ +

+ {{genome_table.table_help|safe}} +

+ +
+
+
+
+
+ + + + + +
+
+
+
+
+ + +{% include "chlamdb/style_menu.html" %} + diff --git a/webapp/templates/chlamdb/groups_overview.html b/webapp/templates/chlamdb/groups_overview.html new file mode 100644 index 000000000..9b9376441 --- /dev/null +++ b/webapp/templates/chlamdb/groups_overview.html @@ -0,0 +1,51 @@ + + + + + +{% include "chlamdb/header.html" %} +{% load custom_tags %} + + + +
+
+
+
+
+
+ {% include "chlamdb/menu.html" %} +

+ {{description|safe}} +

+
+ +
+ +
+
+
+

{{object_name_plural}}

+
+ + {% for group in groups %} + + {% endfor %} +
{{group|safe}}
+
+
+ +
+
+
+
+ + +{% include "chlamdb/style_menu.html" %} + diff --git a/webapp/templates/chlamdb/home.html b/webapp/templates/chlamdb/home.html index 323e6ed47..41640f5e1 100644 --- a/webapp/templates/chlamdb/home.html +++ b/webapp/templates/chlamdb/home.html @@ -252,6 +252,19 @@

Custom plots

+
+
+ +
+ +
+
+
+

{{metadata.group_metadata.object_name_plural}}

+

{{metadata.group_metadata.overview_description|safe}}

+
+
+


diff --git a/webapp/templates/chlamdb/menu.html b/webapp/templates/chlamdb/menu.html index e0b9f6304..5c9e7bacc 100644 --- a/webapp/templates/chlamdb/menu.html +++ b/webapp/templates/chlamdb/menu.html @@ -106,6 +106,10 @@ Custom plots +
  • + {{metadata.group_metadata.object_name_plural}} +
  • +