diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..5a59bba47c6f5ff8b9d0e573e748420de4d98d27 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,11 @@ +ippisite/ippidb/static/academicons-1.8.6/* linguist-vendored +ippisite/ippidb/static/bootstrap-slider-master/* linguist-vendored +ippisite/ippidb/static/bootstrap/* linguist-vendored +ippisite/ippidb/static/chartjs/* linguist-vendored +ippisite/ippidb/static/fontawesome/* linguist-vendored +ippisite/ippidb/static/fonts/* linguist-vendored +ippisite/ippidb/static/jquery/* linguist-vendored +ippisite/ippidb/static/marvinjs-18/* linguist-vendored +ippisite/ippidb/static/smilesdrawer/* linguist-vendored +ippisite/ippidb/static/typeahead/* linguist-vendored +ippisite/ippidb/static/url-polyfill/* linguist-vendored \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3fd6a2438cc99ab5a8c25b128319d59622cc7cfc..34b597969b7b0ac464bc046798e0438184c6e44f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,7 @@ stages: - test - deploy + test-style: image: python:3.6 stage: test @@ -9,6 +10,7 @@ test-style: - cd ippisite - pip install flake8 - flake8 --config=.flake8 + test-ansible: image: python:3.5 stage: test @@ -18,6 +20,7 @@ test-ansible: - whoami - ansible-playbook system.yaml --syntax-check - ansible-playbook deploy.yaml --syntax-check + test-centos7: services: - redis @@ -56,6 +59,27 @@ test-centos7: - python3.6 manage.py test - coverage run --source='.' manage.py test - coverage report + - coverage html + - pip3.6 install sphinx sphinx-argparse sphinxcontrib.bibtex sphinx_rtd_theme + - cd docs + - make html + artifacts: + paths: + - ippisite/htmlcov + - ippisite/docs/build/html + +pages: + stage: deploy + dependencies: + - test-centos7 + script: + - 'mkdir -p public/$CI_COMMIT_REF_NAME' + - 'mv ippisite/htmlcov public/$CI_COMMIT_REF_NAME/' + - 'mv ippisite/docs/build/html/ public/$CI_COMMIT_REF_NAME/' + artifacts: + paths: + - public + deploy-webserver-targetcentric: image: python:3.5 stage: deploy @@ -78,6 +102,7 @@ deploy-webserver-targetcentric: --extra-vars "deploy_user_name=ippidb repo_api_token=JZS-4cH7bWkFkHa2rAVf marvinjs_apikey=$MARVINJS_APIKEY_targetcentric galaxy_base_url=$GALAXY_BASE_URL_targetcentric galaxy_apikey=$GALAXY_APIKEY_targetcentric galaxy_compoundproperties_workflowid=$GALAXY_COMPOUNDPROPERTIES_WORKFLOWID_targetcentric secret_key=$SECRET_KEY_targetcentric dbname=$DBNAME_targetcentric dbuser=$DBUSER_targetcentric dbpassword=$DBPASSWORD_targetcentric dbhost=$DBHOST_targetcentric dbport=$DBPORT_targetcentric http_port=$HTTP_PORT_targetcentric branch=targetcentric" only: - targetcentric + deploy-webserver-test: image: python:3.5 stage: deploy @@ -97,9 +122,10 @@ deploy-webserver-test: - cd ansible - whoami - ansible-playbook -vvv -i ./hosts_master deploy.yaml - --extra-vars "deploy_user_name=ippidb repo_api_token=JZS-4cH7bWkFkHa2rAVf marvinjs_apikey=$MARVINJS_APIKEY_master galaxy_base_url=$GALAXY_BASE_URL_master galaxy_apikey=$GALAXY_APIKEY_master galaxy_compoundproperties_workflowid=$GALAXY_COMPOUNDPROPERTIES_WORKFLOWID_master secret_key=$SECRET_KEY_master dbname=$DBNAME_master dbuser=$DBUSER_master dbpassword=$DBPASSWORD_master dbhost=$DBHOST_master dbport=$DBPORT_master http_port=$HTTP_PORT_master branch=$CI_COMMIT_REF_NAME" + --extra-vars "deploy_user_name=ippidb repo_api_token=JZS-4cH7bWkFkHa2rAVf marvinjs_apikey=$MARVINJS_APIKEY_master galaxy_base_url=$GALAXY_BASE_URL_master galaxy_apikey=$GALAXY_APIKEY_master galaxy_compoundproperties_workflowid=$GALAXY_COMPOUNDPROPERTIES_WORKFLOWID_master secret_key=$SECRET_KEY_master dbname=$DBNAME_master dbuser=$DBUSER_master dbpassword=$DBPASSWORD_master dbhost=$DBHOST_master dbport=$DBPORT_master http_port=$HTTP_PORT_master branch=$CI_COMMIT_REF_NAME gacode=$GACODE_master" only: - master + deploy-webserver-production: image: python:3.5 stage: deploy @@ -119,6 +145,6 @@ deploy-webserver-production: - cd ansible - whoami - ansible-playbook -vvv -i ./hosts_release deploy.yaml - --extra-vars "deploy_user_name=ippidb repo_api_token=JZS-4cH7bWkFkHa2rAVf marvinjs_apikey=$MARVINJS_APIKEY_release galaxy_base_url=$GALAXY_BASE_URL_release galaxy_apikey=$GALAXY_APIKEY_release galaxy_compoundproperties_workflowid=$GALAXY_COMPOUNDPROPERTIES_WORKFLOWID_release secret_key=$SECRET_KEY_release dbname=$DBNAME_release dbuser=$DBUSER_release dbpassword=$DBPASSWORD_release dbhost=$DBHOST_release dbport=$DBPORT_release http_port=$HTTP_PORT_release branch=$CI_COMMIT_REF_NAME" + --extra-vars "deploy_user_name=ippidb repo_api_token=JZS-4cH7bWkFkHa2rAVf marvinjs_apikey=$MARVINJS_APIKEY_release galaxy_base_url=$GALAXY_BASE_URL_release galaxy_apikey=$GALAXY_APIKEY_release galaxy_compoundproperties_workflowid=$GALAXY_COMPOUNDPROPERTIES_WORKFLOWID_release secret_key=$SECRET_KEY_release dbname=$DBNAME_release dbuser=$DBUSER_release dbpassword=$DBPASSWORD_release dbhost=$DBHOST_release dbport=$DBPORT_release http_port=$HTTP_PORT_release branch=$CI_COMMIT_REF_NAME gacode=$GACODE_release" only: - release diff --git a/ansible/celery.service b/ansible/celery.service index 7c3374d1b34723773f75e4c0aa02ce4253312896..9db00b330cad7acbfbaa4ce382487fc56ddab23f 100644 --- a/ansible/celery.service +++ b/ansible/celery.service @@ -1,19 +1,21 @@ [Unit] -Description=Celery Service +Description=Celery Service for iPPI-DB running on port {{ http_port }} After=network.target [Service] Type=forking -User=celery +User=celery-{{ http_port }} Group=ippidb EnvironmentFile=-/etc/default/ippidb-{{ http_port }}-celeryd WorkingDirectory=/home/ippidb/ippidb-web-{{ http_port }}/ippisite ExecStart=/bin/sh -c '${CELERY_BIN} multi start ${CELERYD_NODES} \ + -Q ${CELERYD_QUEUE} \ -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} \ --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS}' ExecStop=/bin/sh -c '${CELERY_BIN} multi stopwait ${CELERYD_NODES} \ --pidfile=${CELERYD_PID_FILE}' ExecReload=/bin/sh -c '${CELERY_BIN} multi restart ${CELERYD_NODES} \ + -Q ${CELERYD_QUEUE} \ -A ${CELERY_APP} --pidfile=${CELERYD_PID_FILE} \ --logfile=${CELERYD_LOG_FILE} --loglevel=${CELERYD_LOG_LEVEL} ${CELERYD_OPTS}' diff --git a/ansible/celeryd b/ansible/celeryd index 0f9f367a637ee253a82b5d24531d0700e1eee6a4..1ce118adaf50ca45fb3b116eb7a2f314d492a201 100644 --- a/ansible/celeryd +++ b/ansible/celeryd @@ -6,8 +6,9 @@ CELERYD_OPTS="--time-limit=3000 --concurrency=1 --max-tasks-per-child=1" CELERYD_LOG_FILE="/var/ippidb-{{ http_port }}-celery/celery%n%I.log" CELERYD_PID_FILE="/var/ippidb-{{ http_port }}-celery/celery%n.pid" CELERYD_LOG_LEVEL="DEBUG" -CELERYD_USER="celery" +CELERYD_USER="celery-{{ http_port }}" CELERYD_GROUP="ippidb" +CELERYD_QUEUE="celery-{{ http_port }}" CELERY_CREATE_DIRS=1 SYSTEMD_LOG_LEVEL=debug DJANGO_SETTINGS_MODULE=ippisite.{{ ansible_hostname }}_settings diff --git a/ansible/deploy.yaml b/ansible/deploy.yaml index 64d6571448b04ec01e54d533c526f1146513f461..346a6d21bfc032a9ef37451742a3e7b04ea133f9 100644 --- a/ansible/deploy.yaml +++ b/ansible/deploy.yaml @@ -10,7 +10,7 @@ selinux: state: disabled - name: Create celery user - user: name=celery groups={{ deploy_user_name }} append=yes state=present createhome=yes + user: name=celery-{{ http_port }} groups={{ deploy_user_name }} append=yes state=present createhome=yes become: true register: newuser # Install basic non-virtualenv requirements @@ -130,11 +130,11 @@ - name: stop "generic" httpd service if relevant systemd: state=stopped name=httpd - name: stop iPPIDB service if relevant - systemd: state=stopped name=ippidb-web + systemd: state=stopped name=ippidb{{ http_port }}-web #ignore fail (i.e. when service does not exist yet) ignore_errors: yes - name: stop celery service - systemd: state=stopped name=celery enabled=true + systemd: state=stopped name=celery-{{ http_port }} enabled=true ignore_errors: yes # # Set up celery service @@ -160,7 +160,7 @@ - name: copy celery systemd service template: src: celery.service - dest: /lib/systemd/system/celery.service + dest: /lib/systemd/system/celery-{{ http_port }}.service force: yes owner: root group: root @@ -214,6 +214,19 @@ } } marker: "# {mark} ANSIBLE MANAGED DATABASE SETTINGS" + - name: Configure the CELERY QUEUE to submit tasks to from Django + blockinfile: + path: "{{ checkout_path }}/ippisite/ippisite/{{ ansible_hostname }}_settings.py" + block: | + CELERY_TASK_DEFAULT_QUEUE = "celery-{{ http_port }}" + marker: "# {mark} ANSIBLE MANAGED CELERY DEFAULT TASK QUEUE" + - name: Add database settings to iPPI-DB settings + blockinfile: + path: "{{ checkout_path }}/ippisite/ippisite/{{ ansible_hostname }}_settings.py" + block: | + GA_CODE = "{{ gacode }}" + marker: "# {mark} ANSIBLE MANAGED GOOGLE ANALYTICS ID" + when: gacode is defined - name: Add email/debug settings to iPPI-DB settings blockinfile: path: "{{ checkout_path }}/ippisite/ippisite/{{ ansible_hostname }}_settings.py" @@ -282,7 +295,7 @@ BABEL_LIBDIR: "/usr/lib64/openbabel/" - name: create mod_wsgi configuration django_manage: - command: "runmodwsgi --setup-only --port={{ http_port }} --user ippidb --group wheel --server-root={{ service_conf_path }}" + command: "runmodwsgi --setup-only --port={{ http_port }} --user ippidb --group wheel --server-root={{ service_conf_path }} --url-alias /media {{ checkout_path }}/ippisite/media" app_path: "{{ checkout_path }}/ippisite" settings: "ippisite.{{ ansible_hostname }}_settings" environment: @@ -338,4 +351,4 @@ # Start celery service # - name: start celery service if relevant - systemd: state=started name=celery enabled=true daemon_reload=true + systemd: state=started name=celery-{{ http_port }} enabled=true daemon_reload=true diff --git a/ippisite/docs/source/conf.py b/ippisite/docs/source/conf.py index 56faa9fd2f94c12ab8d1c36cbc3c7ff26a87640a..2a36bfceca8a59f6951aaaa54dda5e4e70c9656f 100644 --- a/ippisite/docs/source/conf.py +++ b/ippisite/docs/source/conf.py @@ -18,6 +18,10 @@ import os import sys +import matplotlib +# avoid using tkinter with matplotlib +matplotlib.use('agg') + sys.path.insert(0, os.path.abspath('../..')) import django os.environ['DJANGO_SETTINGS_MODULE'] = 'ippisite.settings' @@ -208,5 +212,7 @@ napoleon_use_rtype = False # More legible # The suffix of source filenames. autosummary_generate = True - exclude_patterns = ['_build'] + +#do not try to import tkinter for sphinx +autodoc_mock_imports = ['_tkinter'] \ No newline at end of file diff --git a/ippisite/ippidb/admin.py b/ippisite/ippidb/admin.py index 45385a302e94dc4d7baa6b881daa52ed54d19646..10ced876180be13ee0d6aabc6dda5946331ea94d 100644 --- a/ippisite/ippidb/admin.py +++ b/ippisite/ippidb/admin.py @@ -27,6 +27,7 @@ from .models import ( Ppi, ProteinDomainComplex, Contribution, + Job, ) from .tasks import launch_validate_contributions @@ -60,6 +61,62 @@ class ViewOnSiteModelAdmin(admin.ModelAdmin): ) +@admin.register(Job) +class JobModelAdmin(admin.ModelAdmin): + date_hierarchy = "task_result__date_done" + list_display = ( + "task_result_task_name", + "task_result_task_id", + "task_result_status", + "task_result_date_created", + "task_result_date_done", + ) + list_filter = ( + "task_result__status", + "task_result__date_done", + "task_result__task_name", + ) + readonly_fields = ( + "task_result_task_name", + "task_result_task_id", + "task_result_status", + "task_result_date_created", + "task_result_date_done", + ) + search_fields = ( + "task_result__task_name", + "task_result__task_id", + "task_result__status", + ) + fields = ( + ("task_result_task_name", "task_result_task_id"), + "task_result_status", + ("task_result_date_created", "task_result_date_done"), + ("std_out", "std_err"), + ) + + def task_result_task_id(self, x): + return x.task_result.task_id + + def task_result_task_name(self, x): + return x.task_result.task_name + + def task_result_date_done(self, x): + return x.task_result.date_done + + def task_result_status(self, x): + return x.task_result.status + + def task_result_date_created(self, x): + return x.task_result.date_created + + task_result_task_id.short_description = "task_id" + task_result_task_name.short_description = "task_name" + task_result_date_done.short_description = "date_done" + task_result_status.short_description = "status" + task_result_date_created.short_description = "date_created" + + @admin.register(Bibliography) class BibliographyModelAdmin(ViewOnSiteModelAdmin): list_display = ("authors_list", "title", "journal_name", "biblio_year", "id_source") @@ -134,7 +191,7 @@ class ContributionModelAdmin(ViewOnSiteModelAdmin): def validate_contributions(self, request, queryset): ids = [id for id in queryset.values_list("id", flat=True)] - launch_validate_contributions.delay(ids) + launch_validate_contributions(ids) self.message_user( request, f"validation started for contributions(s) " diff --git a/ippisite/ippidb/forms.py b/ippisite/ippidb/forms.py index 4e3bea06d5c1dec611d54aee50906ae33e440f5e..45fd7a56747c5177228710cbbbc652c40061cd6a 100644 --- a/ippisite/ippidb/forms.py +++ b/ippisite/ippidb/forms.py @@ -465,9 +465,9 @@ class PpiModelForm(ModelForm): label=_("Total number of pockets in the complex"), required=True ) family_name = CharFieldDataList( - # data_class=models.PpiFamily, + data_class=models.PpiFamily, data_list=[], - # data_attr='name', + data_attr="name", label="PPI Family", max_length=30, required=True, @@ -839,7 +839,7 @@ class BaseInlineNestedFormSet(forms.BaseInlineFormSet): class CompoundActivityResultForm(ModelForm): compound_name = forms.ChoiceField(choices=(), required=True) activity_mol = forms.DecimalField( - label="Activity", required=True, max_digits=12, decimal_places=10, min_value=0, + label="Activity", required=True, max_digits=15, decimal_places=10, min_value=0, ) activity_unit = forms.CharField( label="Activity unit", @@ -937,10 +937,20 @@ class CompoundActivityResultForm(ModelForm): class CompoundActivityResultBaseInlineNestedFormSet(BaseInlineNestedFormSet): __compound_names = [] + # pIC50, pEC50, etc. activity types are labelled below as IC50, EC50, etc. + # specifically because the user enters them in these forms as the former + # value but they are converted to and stored in the latter. + __activity_types = [ + ("pIC50", "IC50 (half maximal inhibitory concentration)"), + ("pEC50", "EC50 (half maximal effective concentration)"), + ("pKd", "Kd (dissociation constant)"), + ("pKi", "Ki (inhibition constant)"), + ] def add_fields(self, form, index): super().add_fields(form, index) form.fields["compound_name"].choices = self.__compound_names + form.fields["activity_type"].choices = self.__activity_types def set_modulation_type(self, modulation_type): for form in self.forms: @@ -1074,7 +1084,7 @@ class TestActivityDescriptionForm(forms.ModelForm): def clean(self): cleaned_data = super().clean() - if "test_type" in cleaned_data and cleaned_data["test_type"] == "CELL": + if "test_type" in cleaned_data and cleaned_data["test_type"] != "CELL": cleaned_data["cell_line_name"] = "" return cleaned_data @@ -1088,7 +1098,10 @@ class TestActivityDescriptionForm(forms.ModelForm): """ # right if hasattr(self, "cleaned_data"): - if "cell_line_name" in self.cleaned_data: + if ( + "cell_line_name" in self.cleaned_data + and self.cleaned_data["cell_line_name"] != "" + ): cell_line, created = models.CellLine.objects.get_or_create( name=self.cleaned_data["cell_line_name"] ) diff --git a/ippisite/ippidb/management/commands/compute_compound_properties.py b/ippisite/ippidb/management/commands/compute_compound_properties.py deleted file mode 100644 index 22153cacb941501cc32c490117c2e4f4201edaee..0000000000000000000000000000000000000000 --- a/ippisite/ippidb/management/commands/compute_compound_properties.py +++ /dev/null @@ -1,321 +0,0 @@ -import argparse -from datetime import datetime -from itertools import islice -import json -import re -import time -import tempfile - -from bioblend.galaxy import GalaxyInstance -from django.conf import settings -from django.core.management import BaseCommand -from django.forms.models import model_to_dict -import pandas as pd -import requests - -from ippidb.models import Compound -from ippidb.utils import smi2sdf - -# disable insecure HTTP request warnings (used by bioblend) -import urllib3 - -urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) - -BASE_URL = settings.GALAXY_BASE_URL -KEY = settings.GALAXY_APIKEY -WORKFLOW_ID = settings.GALAXY_COMPOUNDPROPERTIES_WORKFLOWID - - -class GalaxyCompoundPropertiesRunner(object): - def __init__(self, galaxy_instance): - self.galaxy_instance = galaxy_instance - - def compute_properties_for_sdf_file(self, sdf_file_path): - # create a history to store the workflow results - now = datetime.now() - date_time = now.strftime("%Y/%m/%d-%H:%M:%S") - history_name = "compoundpropertiesjobrun_%s" % date_time - history = self.galaxy_instance.histories.create_history(name=history_name) - history_id = history["id"] - if history["state"] not in ["new", "ok"]: - raise Exception( - f'Error creating history "{history_name}" (id {history_id})' - ) - # launch data upload job - upload_response = self.galaxy_instance.tools.upload_file( - path=sdf_file_path, file_type="sdf", history_id=history_id - ) - upload_data_id = upload_response["outputs"][0]["id"] - upload_job = upload_response["jobs"][0] - upload_job_id = upload_job["id"] - # monitor data upload until completed or on error - while upload_job["state"] not in ["ok"]: - time.sleep(2) - upload_job = self.galaxy_instance.jobs.show_job(upload_job_id) - if upload_job["state"] in ["error", "deleted", "discarded"]: - data = self.galaxy_instance.datasets.show_dataset(upload_data_id) - raise Exception( - f"Error during Galaxy data upload job - name : " - f"{data['name']}, id : {upload_data_id}, " - f"error : {data['misc_info']}" - ) - # check uploaded dataset status - data = self.galaxy_instance.datasets.show_dataset(upload_data_id) - if data["state"] not in ["ok"]: - raise Exception( - f"Error during Galaxy data upload result - name : " - f"{data['name']}, id : {upload_data_id}, " - f"error : {data['misc_info']}" - ) - # submit compound properties computation job - dataset_map = {"0": {"src": "hda", "id": upload_data_id}} - workflow_job = self.galaxy_instance.workflows.invoke_workflow( - WORKFLOW_ID, inputs=dataset_map, history_id=history_id - ) - workflow_job_id = workflow_job["id"] - while workflow_job["state"] not in ["ok", "scheduled"]: - time.sleep(2) - workflow_job = self.galaxy_instance.workflows.show_invocation( - "dad6103ff71ca4fe", workflow_job_id - ) - if workflow_job["state"] in ["error", "deleted", "discarded"]: - raise Exception( - f"Error during Galaxy workflow job - name : " - f"id : {workflow_job_id}, " - ) - datasets = self.galaxy_instance.histories.show_history( - history_id, contents=True - ) - actual_result_dataset = None - for dataset in datasets: - if dataset["extension"] == "json": - actual_result_dataset = dataset - if actual_result_dataset is None: - raise Exception( - f"Result for galaxy workflow invocation {workflow_job_id} not found in" - f" history {history_id}" - ) - dataset = self.galaxy_instance.datasets.show_dataset( - actual_result_dataset["id"] - ) - while dataset["state"] not in ["ok"]: - time.sleep(2) - dataset = self.galaxy_instance.datasets.show_dataset( - actual_result_dataset["id"] - ) - download_url = dataset["download_url"] - contents_resp = requests.get(BASE_URL + download_url, verify=False) - contents = contents_resp.json() - return contents - - -def idrange_type(s, pat=re.compile(r"^(\d+)-(\d+)$")): - m = pat.match(s) - if not m: - raise argparse.ArgumentTypeError( - "please specify ID range as [start number]-[endnumber]" - ) - return (int(m.groups()[0]), int(m.groups()[1])) - - -def dec(decimal_places): - def func(number): - return round(float(number), decimal_places) - - return func - - -def chunks(data, size=10): - it = iter(data) - for i in range(0, len(data), size): - yield {k: data[k] for k in islice(it, size)} - - -class Command(BaseCommand): - - help = "Compute compound physicochemical properties" - - def add_arguments(self, parser): - parser.add_argument( - "mode", choices=["update", "compare", "print"], default="update" - ) - selection = parser.add_mutually_exclusive_group(required=True) - selection.add_argument( - "--all", action="store_true", help="Process all compounds in the database" - ) - selection.add_argument( - "--ids", - nargs="+", - type=int, - help="Process the compounds for the specified IDs", - ) - selection.add_argument( - "--idrange", - type=idrange_type, - help="Process the compounds for the specified ID range", - ) - parser.add_argument( - "--json", - type=argparse.FileType("r"), - help="Process precomputed results stored in a JSON file", - ) - parser.add_argument( - "--xls", type=argparse.FileType("w"), help="Store results in Excel file" - ) - - def handle(self, *args, **options): - # select the compounds that need to be processed - smiles_dict = {} - compounds = [] - pc_properties = {} - already_done_ids = [] - if options["json"] is not None: - pc_properties_dict = json.load(open(options["json"].name, "r")) - ids = [ - int(key) - for key, item in pc_properties_dict.items() - if "IUPAC" not in item - ] - already_done_ids = [ - int(key) for key, item in pc_properties_dict.items() if "IUPAC" in item - ] - if options["all"] is True: - ids = Compound.objects.all().values("id") - elif options["ids"]: - ids = Compound.objects.filter(id__in=options["ids"]).values("id") - elif options["idrange"]: - ids = Compound.objects.filter( - id__gte=options["idrange"][0], id__lte=options["idrange"][1] - ).values("id") - else: - compounds = Compound.objects.filter(iupac_name__isnull=True).values("id") - ids = [row["id"] for row in ids] - ids = list(set(ids) - set(already_done_ids)) - compounds = Compound.objects.filter(id__in=ids) - for c in compounds: - smiles_dict[c.id] = c.canonical_smile - # create or reuse existing JSON file to save new results - if options["json"]: - json_file = options["json"].name - else: - json_fh = tempfile.NamedTemporaryFile(mode="w", delete=False) - json.dump({c.id: {} for c in compounds}, json_fh) - json_file = json_fh.name - json_fh.close() - self.stderr.write(self.style.SUCCESS(f"Compound properties file: {json_file}")) - if len(compounds) > 0: - self.stderr.write(f"Now processing {len(compounds)} compounds") - # set up Galaxy computation environment - gi = GalaxyInstance(url=BASE_URL, key=KEY, verify=False) - gi.nocache = True - runner = GalaxyCompoundPropertiesRunner(gi) - chunk_size = 3 - for smiles_dict_chunk in chunks(smiles_dict, chunk_size): - # create SDF file for the selection - sdf_string = smi2sdf(smiles_dict_chunk) - fh = tempfile.NamedTemporaryFile(mode="w", delete=False) - fh.write(sdf_string) - fh.close() - self.stderr.write( - self.style.SUCCESS( - f"Galaxy input SDF file for compounds {smiles_dict_chunk.keys()}: {fh.name}" - ) - ) - # run computations on Galaxy - pc_properties = runner.compute_properties_for_sdf_file(fh.name) - new_pc_properties_dict = { - compound["Name"]: compound for compound in pc_properties - } - pc_properties_dict = json.load(open(json_file, "r")) - pc_properties_dict.update(new_pc_properties_dict) - fh = open(json_file, "w") - json.dump(pc_properties_dict, fh, indent=4) - fh.close() - self.stderr.write( - self.style.SUCCESS( - f"Properties added for compounds {smiles_dict_chunk.keys()} in JSON file: {json_file}" - ) - ) - # report and update database - property_mapping = { - "CanonicalSmile": ("canonical_smile", str), - "IUPAC": ("iupac_name", str), - "TPSA": ("tpsa", dec(2)), - "NbMultBonds": ("nb_multiple_bonds", int), - "BalabanIndex": ("balaban_index", dec(2)), - "NbDoubleBonds": ("nb_double_bonds", int), - "RDF070m": ("rdf070m", dec(2)), - "SumAtomPolar": ("sum_atom_polar", dec(2)), - "SumAtomVolVdW": ("sum_atom_vol_vdw", dec(2)), - "MolecularWeight": ("molecular_weight", dec(2)), - "NbCircuits": ("nb_circuits", int), - "NbAromaticsSSSR": ("nb_aromatic_sssr", int), - "NbAcceptorH": ("nb_acceptor_h", int), - "NbF": ("nb_f", int), - "Ui": ("ui", dec(2)), - "NbO": ("nb_o", int), - "NbCl": ("nb_cl", int), - "NbBonds": ("nb_bonds", int), - "LogP": ("a_log_p", dec(2)), - "RandicIndex": ("randic_index", dec(2)), - "NbBondsNonH": ("nb_bonds_non_h", int), - "NbAromaticsEther": ("nb_aromatic_ether", int), - "NbChiralCenters": ("nb_chiral_centers", int), - "NbBenzLikeRings": ("nb_benzene_like_rings", int), - "RotatableBondFraction": ("rotatable_bond_fraction", dec(2)), - "LogD": ("log_d", dec(2)), - "WienerIndex": ("wiener_index", int), - "NbN": ("nb_n", int), - "NbC": ("nb_c", int), - "NbAtom": ("nb_atom", int), - "NbAromaticsBonds": ("nb_aromatic_bonds", int), - "MeanAtomVolVdW": ("mean_atom_vol_vdw", dec(2)), - "AromaticRatio": ("aromatic_ratio", dec(2)), - "NbAtomNonH": ("nb_atom_non_h", int), - "NbDonorH": ("nb_donor_h", int), - "NbI": ("nb_i", int), - "NbRotatableBonds": ("nb_rotatable_bonds", int), - "NbRings": ("nb_rings", int), - "NbCsp2": ("nb_csp2", int), - "NbCsp3": ("nb_csp3", int), - "NbBr": ("nb_br", int), - "GCMolarRefractivity": ("gc_molar_refractivity", dec(2)), - "NbAliphaticsAmines": ("nb_aliphatic_amines", int), - } - properties_list = [] - computed_properties = ["id"] + [ - value[0] for key, value in property_mapping.items() - ] - ippidb_convs = {value[0]: value[1] for key, value in property_mapping.items()} - ippidb_convs["id"] = int - pc_properties_dict = json.load(open(json_file)) - for cid, item in pc_properties_dict.items(): - compound = Compound.objects.get(id=cid) - ippidb_dict = model_to_dict(Compound.objects.get(id=cid)) - ippidb_dict = { - key: ippidb_convs[key](value) - for key, value in ippidb_dict.items() - if key in computed_properties and value is not None - } - ippidb_dict["source"] = "iPPI-DB" - galaxy_dict = {"id": int(cid), "source": "Galaxy"} - for galaxy_prop, prop in property_mapping.items(): - ippidb_prop = prop[0] - ippidb_conv = prop[1] - try: - galaxy_dict[ippidb_prop] = ippidb_conv(item[galaxy_prop]) - except ValueError as ve: - self.stderr.write( - self.style.ERROR( - f"Error setting property {ippidb_prop} to {item[galaxy_prop]}" - f" in compound {compound.id} \ndetails:{ve}" - ) - ) - properties_list.append(ippidb_dict) - properties_list.append(galaxy_dict) - properties_df = pd.DataFrame(properties_list) - properties_df.set_index(["id", "source"], inplace=True) - properties_df.sort_index(inplace=True) - if options["xls"]: - properties_df.to_excel(options["xls"].name) - self.stderr.write(self.style.SUCCESS("All done!")) diff --git a/ippisite/ippidb/management/commands/lle_le.py b/ippisite/ippidb/management/commands/lle_le.py index 0ac9cbad35a80f4c91e14cb65d06b45a1df30251..73aac5064dd9e80689d8c4fa32d46084ae9d5955 100644 --- a/ippisite/ippidb/management/commands/lle_le.py +++ b/ippisite/ippidb/management/commands/lle_le.py @@ -1,8 +1,6 @@ -import json - from django.core.management import BaseCommand -from ippidb.models import Compound, LeLleBiplotData +from ippidb.tasks import generate_le_lle_plot class Command(BaseCommand): @@ -11,28 +9,7 @@ class Command(BaseCommand): def handle(self, *args, **options): self.stdout.write(self.style.SUCCESS("Generating the LE vs. LLE biplot...")) - le_lle_data = [] - LeLleBiplotData.objects.all().delete() - self.stdout.write(self.style.SUCCESS("Successfully flushed LE-LLE biplot data")) - for comp in Compound.objects.validated(): - if comp.le is not None: - le = round(comp.le, 7) - lle = round(comp.lle, 7) - le_lle_data.append( - { - "x": le, - "y": lle, - "id": comp.id, - "family_name": comp.best_activity_ppi_family_name, - "smiles": comp.canonical_smile, - } - ) - else: - self.stdout.write(self.style.WARNING("compound %s has no LE" % comp.id)) - le_lle_json = json.dumps(le_lle_data, separators=(",", ":")) - new = LeLleBiplotData() - new.le_lle_biplot_data = le_lle_json - new.save() + generate_le_lle_plot() self.stdout.write( self.style.SUCCESS("Successfully generated LE-LLE biplot data") ) diff --git a/ippisite/ippidb/management/commands/pca.py b/ippisite/ippidb/management/commands/pca.py index 330d4452dc7b08414105f05d80c2cc9a671dc315..89beebb30b1b7542a0f93674e75b20196ec0db22 100644 --- a/ippisite/ippidb/management/commands/pca.py +++ b/ippisite/ippidb/management/commands/pca.py @@ -1,26 +1,6 @@ -import json -import io -import base64 -import itertools - from django.core.management import BaseCommand -from django.forms.models import model_to_dict -import seaborn as sns -import matplotlib.pyplot as plt -import numpy as np -import pandas as pd -from sklearn.decomposition import PCA -from sklearn.preprocessing import StandardScaler - -from ippidb.models import Compound, PcaBiplotData - -def plot_circle(): - theta = np.linspace(0, 2 * np.pi, 100) - r = np.sqrt(1.0) - x1 = r * np.cos(theta) - x2 = r * np.sin(theta) - return x1, x2 +from ippidb.tasks import generate_pca_plot class Command(BaseCommand): @@ -29,124 +9,5 @@ class Command(BaseCommand): def handle(self, *args, **options): self.stdout.write(self.style.SUCCESS("Generating the PCA biplot...")) - pca_data = [] - features = [ - "molecular_weight", - "a_log_p", - "nb_donor_h", - "nb_acceptor_h", - "tpsa", - "nb_rotatable_bonds", - "nb_benzene_like_rings", - "fsp3", - "nb_chiral_centers", - "nb_csp3", - "nb_atom", - "nb_bonds", - "nb_atom_non_h", - "nb_rings", - "nb_multiple_bonds", - "nb_aromatic_bonds", - "aromatic_ratio", - ] - PcaBiplotData.objects.all().delete() - self.stdout.write(self.style.SUCCESS("Successfully flushed PCA biplot data")) - values_list = [] - for comp in Compound.objects.validated(): - values = model_to_dict(comp, fields=features + ["id", "family"]) - values["family"] = comp.best_activity_ppi_family_name - values_list.append(values) - df = pd.DataFrame(values_list) - # drop compounds with undefined property values - df.dropna(how="any", inplace=True) - # prepare correlation circle figure - plt.switch_backend("Agg") - fig_, ax = plt.subplots(figsize=(6, 6)) - x1, x2 = plot_circle() - plt.plot(x1, x2) - ax.set_aspect(1) - ax.yaxis.set_label_coords(-0.1, 0.5) - ax.xaxis.set_label_coords(0.5, -0.08) - # do not process the data unless there are compounds in the dataframe - if len(df.index) > 0: - x = df.loc[:, features].values - y = df.loc[:, ["family"]].values - x = StandardScaler().fit_transform(x) - n = x.shape[0] # retrieve number of individuals - p = x.shape[1] # retrieve number of variables - pca = PCA(n_components=p) - principal_components = pca.fit_transform(x) - # compute correlation circle - variance_ratio = pd.Series(pca.explained_variance_ratio_) - coef = np.transpose(pca.components_) - cols = ["PC-" + str(x) for x in range(len(variance_ratio))] - pc_infos = pd.DataFrame(coef, columns=cols, index=features) - # we might remove the line below if the PCA remains grayscale - pal = itertools.cycle( # noqa: F841 - sns.color_palette("dark", len(features)) - ) - # compute the length of each feature arrow in the correlation circle - pc_infos["DIST"] = pc_infos[["PC-0", "PC-1"]].pow(2).sum(1).pow(0.5) - # store the maximal length for normalization purposes - best_distance = max(pc_infos["DIST"]) - # compute corvar for the correlation circle - eigval = (float(n) - 1) / float(n) * pca.explained_variance_ - sqrt_eigval = np.sqrt(eigval) - sqrt_eigval = np.sqrt(eigval) - corvar = np.zeros((p, p)) - for k in range(p): - corvar[:, k] = pca.components_[k, :] * sqrt_eigval[k] - for idx in range(len(pc_infos["PC-0"])): - x = corvar[idx, 0] # use corvar to plot the variable map - y = corvar[idx, 1] # use corvar to plot the variable map - color = "black" - # alpha is the feature length normalized - # to the longest feature's length - alpha = pc_infos["DIST"][idx] / best_distance - plt.arrow(0.0, 0.0, x, y, head_width=0.02, color="black", alpha=alpha) - plt.annotate( - Compound._meta.get_field(pc_infos.index[idx]).verbose_name, - xy=(x, y), - xycoords="data", - xytext=np.asarray((x, y)) + (0.02, -0.02), - fontsize=6, - color=color, - alpha=alpha, - ) - plt.xlabel("PC-1 (%s%%)" % str(variance_ratio[0])[:4].lstrip("0.")) - plt.ylabel("PC-2 (%s%%)" % str(variance_ratio[1])[:4].lstrip("0.")) - plt.xlim((-1, 1)) - plt.ylim((-1, 1)) - principal_df = pd.DataFrame(data=principal_components) - # only select the two first dimensions for the plot, and rename them to x and y - principal_df = principal_df.iloc[:, 0:2] - principal_df = principal_df.rename(columns={0: "x", 1: "y"}) - final_df = pd.concat([principal_df, df[["family", "id"]]], axis=1) - for index, row in final_df.iterrows(): - smiles = Compound.objects.get(id=row.id).canonical_smile - pca_data.append( - { - "x": row.x, - "y": row.y, - "id": row.id, - "family_name": row.family, - "smiles": smiles, - } - ) - else: - pca_data = [] - # save correlation circle PNG - my_string_io_bytes = io.BytesIO() - plt.savefig(my_string_io_bytes, format="png", dpi=600, bbox_inches="tight") - my_string_io_bytes.seek(0) - # figdata_png is the correlation circle figure, base 64-encoded - figdata_png = base64.b64encode(my_string_io_bytes.read()) - pca_data_cc = { - "data": pca_data, - "correlation_circle": figdata_png.decode("utf-8"), - } - pca_json = json.dumps(pca_data_cc, separators=(",", ":")) - new = PcaBiplotData() - new.pca_biplot_data = pca_json - new.save() + generate_pca_plot() self.stdout.write(self.style.SUCCESS("Successfully generated PCA biplot data")) diff --git a/ippisite/ippidb/migrations/0041_job.py b/ippisite/ippidb/migrations/0041_job.py new file mode 100644 index 0000000000000000000000000000000000000000..58db99c1390c1eec38a2cedaa8a2a532b0a2534e --- /dev/null +++ b/ippisite/ippidb/migrations/0041_job.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.1 on 2020-03-18 23:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ippidb', '0040_compound_families'), + ] + + operations = [ + migrations.CreateModel( + name='Job', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='task name', max_length=125)), + ('status', models.CharField(choices=[('INIT', 'init'), ('RUNNING', 'running'), ('SUCCESS', 'success'), ('ERROR', 'error')], db_index=True, default='INIT', help_text='The status of this job.', max_length=30, verbose_name='job status')), + ('std_out', models.TextField(blank=True, help_text='task standard output', null=True)), + ('std_err', models.TextField(blank=True, help_text='task error output', null=True)), + ], + ), + ] diff --git a/ippisite/ippidb/migrations/0042_auto_20200319_1032.py b/ippisite/ippidb/migrations/0042_auto_20200319_1032.py new file mode 100644 index 0000000000000000000000000000000000000000..da4735659484fcba18ef7b2906a5fa3d19065cdc --- /dev/null +++ b/ippisite/ippidb/migrations/0042_auto_20200319_1032.py @@ -0,0 +1,23 @@ +# Generated by Django 2.2.1 on 2020-03-19 10:32 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ippidb', '0041_job'), + ] + + operations = [ + migrations.AlterField( + model_name='job', + name='std_err', + field=models.TextField(blank=True, default='', help_text='task error output', null=True), + ), + migrations.AlterField( + model_name='job', + name='std_out', + field=models.TextField(blank=True, default='', help_text='task standard output', null=True), + ), + ] diff --git a/ippisite/ippidb/migrations/0043_job_task_id.py b/ippisite/ippidb/migrations/0043_job_task_id.py new file mode 100644 index 0000000000000000000000000000000000000000..1c5ccd79f1f6656197e846ba42e3e228e6506b9f --- /dev/null +++ b/ippisite/ippidb/migrations/0043_job_task_id.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.1 on 2020-03-20 17:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ippidb', '0042_auto_20200319_1032'), + ] + + operations = [ + migrations.AddField( + model_name='job', + name='task_id', + field=models.CharField(blank=True, help_text='task id', max_length=50, null=True), + ), + ] diff --git a/ippisite/ippidb/migrations/0044_auto_20200323_1110.py b/ippisite/ippidb/migrations/0044_auto_20200323_1110.py new file mode 100644 index 0000000000000000000000000000000000000000..09d458a4c876f69cfb6c924b899bbb8535800765 --- /dev/null +++ b/ippisite/ippidb/migrations/0044_auto_20200323_1110.py @@ -0,0 +1,34 @@ +# Generated by Django 2.2.1 on 2020-03-23 11:10 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('django_celery_results', '0007_remove_taskresult_hidden'), + ('ippidb', '0043_job_task_id'), + ] + + operations = [ + migrations.RemoveField( + model_name='job', + name='name', + ), + migrations.RemoveField( + model_name='job', + name='status', + ), + migrations.RemoveField( + model_name='job', + name='task_id', + ), + migrations.AddField( + model_name='job', + name='task_result', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='django_celery_results.TaskResult'), + preserve_default=False, + ), + ] diff --git a/ippisite/ippidb/migrations/0045_auto_20200323_1235.py b/ippisite/ippidb/migrations/0045_auto_20200323_1235.py new file mode 100644 index 0000000000000000000000000000000000000000..48fe4e5376c4fb923297bc57cd72de0681f1ddbd --- /dev/null +++ b/ippisite/ippidb/migrations/0045_auto_20200323_1235.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.1 on 2020-03-23 12:35 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ippidb', '0044_auto_20200323_1110'), + ] + + operations = [ + migrations.AlterField( + model_name='job', + name='task_result', + field=models.OneToOneField(default=None, on_delete=django.db.models.deletion.CASCADE, to='django_celery_results.TaskResult'), + ), + ] diff --git a/ippisite/ippidb/migrations/0046_compoundjob.py b/ippisite/ippidb/migrations/0046_compoundjob.py new file mode 100644 index 0000000000000000000000000000000000000000..d348a47d372117e6b6e0e0b44cb278c9c11cbdf5 --- /dev/null +++ b/ippisite/ippidb/migrations/0046_compoundjob.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.1 on 2020-03-29 17:52 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ippidb', '0045_auto_20200323_1235'), + ] + + operations = [ + migrations.CreateModel( + name='CompoundJob', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ippidb.Compound')), + ('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ippidb.Job')), + ], + ), + ] diff --git a/ippisite/ippidb/migrations/0047_auto_20200416_1359.py b/ippisite/ippidb/migrations/0047_auto_20200416_1359.py new file mode 100644 index 0000000000000000000000000000000000000000..b01f69c00c98eb09e2f47cffdf594cb4e94b0f35 --- /dev/null +++ b/ippisite/ippidb/migrations/0047_auto_20200416_1359.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.1 on 2020-04-16 13:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('ippidb', '0046_compoundjob'), + ] + + operations = [ + migrations.AlterField( + model_name='compound', + name='iupac_name', + field=models.TextField(blank=True, null=True, verbose_name='IUPAC name'), + ), + ] diff --git a/ippisite/ippidb/migrations/0048_auto_20200417_1001.py b/ippisite/ippidb/migrations/0048_auto_20200417_1001.py new file mode 100644 index 0000000000000000000000000000000000000000..8107f6adff28c7da94e20229659503c3158363f2 --- /dev/null +++ b/ippisite/ippidb/migrations/0048_auto_20200417_1001.py @@ -0,0 +1,27 @@ +# Generated by Django 2.2.1 on 2020-04-17 10:01 + +from django.db import migrations + +def remove_compound_drugbank_duplicates(apps, schema_editor): + DrugbankCompoundTanimoto = apps.get_model("ippidb", "DrugbankCompoundTanimoto") + db_alias = schema_editor.connection.alias + last = None, None + for dct in DrugbankCompoundTanimoto.objects.using(db_alias).order_by('compound','drugbank_compound'): + if dct.compound == last[0] and dct.drugbank_compound == last[1]: + dct.delete() + else: + last = (dct.compound, dct.drugbank_compound) + +class Migration(migrations.Migration): + + dependencies = [ + ('ippidb', '0047_auto_20200416_1359'), + ] + + operations = [ + migrations.RunPython(remove_compound_drugbank_duplicates), + migrations.AlterUniqueTogether( + name='drugbankcompoundtanimoto', + unique_together={('compound', 'drugbank_compound')}, + ), + ] diff --git a/ippisite/ippidb/migrations/0049_auto_20200419_2005.py b/ippisite/ippidb/migrations/0049_auto_20200419_2005.py new file mode 100644 index 0000000000000000000000000000000000000000..a6794849e3b3abc5ebf1cf1977e81ea37e144496 --- /dev/null +++ b/ippisite/ippidb/migrations/0049_auto_20200419_2005.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.1 on 2020-04-19 20:05 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ippidb', '0048_auto_20200417_1001'), + ] + + operations = [ + migrations.AlterField( + model_name='testactivitydescription', + name='cell_line', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='ippidb.CellLine'), + ), + ] diff --git a/ippisite/ippidb/models.py b/ippisite/ippidb/models.py index ddf55fdbf85643a96037f666acb241ca99989ac4..8f01f1afa9841144ce999e6d4790943fd7a11a47 100644 --- a/ippisite/ippidb/models.py +++ b/ippisite/ippidb/models.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import operator import re +import sys from django.conf import settings from django.contrib.auth import get_user_model @@ -16,6 +17,9 @@ from django.db.models import Max, Count, F, Q, Case, When, Subquery, OuterRef from django.db.models.functions import Cast from django.urls import reverse from django.utils.translation import ugettext_lazy as _ +from django_celery_results.models import TaskResult +from django.dispatch import receiver +from django.db.models.signals import post_save from .utils import FingerPrinter, smi2inchi, smi2inchikey from .ws import ( @@ -713,9 +717,7 @@ class Compound(AutoFillableModel): chembl_id = models.CharField( verbose_name="Chembl ID", max_length=30, blank=True, null=True ) - iupac_name = models.CharField( - verbose_name="IUPAC name", max_length=255, blank=True, null=True - ) + iupac_name = models.TextField(verbose_name="IUPAC name", blank=True, null=True) ligand_id = models.CharField("PDB Ligand ID", max_length=3, blank=True, null=True) pubs = models.IntegerField( verbose_name="Number of publications", null=True, blank=True @@ -832,6 +834,7 @@ class Compound(AutoFillableModel): tanimoto=tanimoto, ) ) + DrugbankCompoundTanimoto.objects.filter(compound=self).delete() DrugbankCompoundTanimoto.objects.bulk_create(dbcts) @property @@ -854,18 +857,16 @@ class Compound(AutoFillableModel): pfam_ids.add(bound_complex.complex.domain.pfam_id) return pfam_ids - @property - def best_pXC50_activity(self): - return self.compoundactivityresult_set.aggregate(Max("activity"))[ - "activity__max" - ] - @property def best_pXC50_compound_activity_result(self): best_pXC50_activity = self.best_activity if best_pXC50_activity is None: return None - return self.compoundactivityresult_set.filter(activity=best_pXC50_activity)[0] + best_cars = self.compoundactivityresult_set.filter(activity=best_pXC50_activity) + if len(best_cars) > 0: + return best_cars[0] + else: + return None @property def bioch_tests_count(self): @@ -926,6 +927,9 @@ class Compound(AutoFillableModel): ): self.add_error("common_name", "A compound with this name already exists") + def get_jobs(self): + return CompoundJob.objects.filter(compound=self) + class CompoundTanimoto(models.Model): canonical_smiles = models.TextField("Canonical Smile") @@ -1035,7 +1039,7 @@ class TestActivityDescription(models.Model): verbose_name="Total number of active compounds" ) cell_line = models.ForeignKey( - CellLine, on_delete=models.CASCADE, blank=True, null=True + CellLine, on_delete=models.SET_NULL, null=True ) def get_complexes(self): @@ -1279,6 +1283,9 @@ class DrugbankCompoundTanimoto(models.Model): drugbank_compound = models.ForeignKey(DrugBankCompound, models.CASCADE) tanimoto = models.DecimalField("Tanimoto value", max_digits=5, decimal_places=4) + class Meta: + unique_together = (("compound", "drugbank_compound"),) + class Contribution(models.Model): contributor = models.ForeignKey(to=get_user_model(), on_delete=models.PROTECT) @@ -1321,16 +1328,6 @@ def update_compound_cached_properties(compounds_queryset=None): .annotate(_best_activity=Max("compoundactivityresult__activity")) .values("_best_activity")[:1] ), - best_activity_ppi_family_name=Subquery( - CompoundActivityResult.objects.filter(compound_id=OuterRef("id")) - .filter(activity=OuterRef("best_activity")) - .annotate( - _best_activity_ppi_family_name=F( - "test_activity_description__ppi__family__name" - ) - ) - .values("_best_activity_ppi_family_name")[:1] - ), le=Subquery( compounds_queryset.filter(id=OuterRef("id")) .annotate( @@ -1638,6 +1635,18 @@ def update_compound_cached_properties(compounds_queryset=None): .values("_tests_av")[:1] ), ) + compounds_queryset.update( + best_activity_ppi_family_name=Subquery( + CompoundActivityResult.objects.filter(compound_id=OuterRef("id")) + .filter(activity=OuterRef("best_activity")) + .annotate( + _best_activity_ppi_family_name=F( + "test_activity_description__ppi__family__name" + ) + ) + .values("_best_activity_ppi_family_name")[:1] + ), + ) compounds_queryset.update( lipinsky_score=Subquery( compounds_queryset.filter(id=OuterRef("id")) @@ -1691,3 +1700,46 @@ def update_compound_cached_properties(compounds_queryset=None): .values("_lipinsky")[:1] ) ) + + +class Job(models.Model): + task_result = models.OneToOneField( + TaskResult, on_delete=models.CASCADE, default=None + ) + std_out = models.TextField( + null=True, default="", blank=True, help_text="task standard output" + ) + std_err = models.TextField( + null=True, default="", blank=True, help_text="task error output" + ) + + def update_output(self, message, output="std_out"): + """ + Save log information in std_out or std_err, + limit the log information size saved to 100 lines max + """ + if output == "std_out": + print(message, file=sys.stdout) + split_std_out = self.std_out.split("\n") + split_std_out = split_std_out + str(message).split("\n") + self.std_out = "\n".join(split_std_out[-100:]) + self.save() + elif output == "std_err": + print(message, file=sys.stderr) + split_std_err = self.std_err.split("\n") + split_std_err = split_std_err + str(message).split("\n") + self.std_err = "\n".join(split_std_err[-100:]) + self.save() + else: + raise Exception("output doesn't exist") + + +class CompoundJob(models.Model): + compound = models.ForeignKey(Compound, on_delete=models.CASCADE) + job = models.ForeignKey(Job, on_delete=models.CASCADE) + + +@receiver(post_save, sender=TaskResult) +def post_save_taskresult(sender, instance, created, *args, **kwargs): + if created: + Job.objects.create(task_result=instance) diff --git a/ippisite/ippidb/static/js/ippidb-charts.js b/ippisite/ippidb/static/js/ippidb-charts.js index e9396ead328c39d1e939bf9b1214a68de8d4a441..1cb0f55cbcf48a7393c9bcf89e9b23d9b8c5f6ed 100644 --- a/ippisite/ippidb/static/js/ippidb-charts.js +++ b/ippisite/ippidb/static/js/ippidb-charts.js @@ -101,27 +101,18 @@ var prepareCompoundFamilyBiplotData = function (compoundId, compoundFamily, plot } var getPpiFamilyColor = function (ppiName) { - return { - 'BCL2-Like / BAX': '#0E5C97', - 'Beta-catenin / TCF-4': '#FAB362', - 'Bromodomain / Histone': '#C9862D', - 'CD4 / gp120': '#FE8E85', - 'CD80 / CD28': '#DB0016', - 'E2 / E1': '#C4ADD1', - 'FAK / VEGFR3': '#6700FF', - 'IL2 / IL2R': '#FE6C00', - 'LEDGF / IN': '#971D19', - 'LFA / ICAM': '#9DD87E', - 'MDM2-Like / P53': '#96C5DC', - 'MENIN / MLL': '#EDBBFF', - 'Myc / Max': '#C04DC4', - 'NRP / VEGF': '#A8184B', - 'UPAR / UPA': '#CBDCFF', - 'VEGF / VEGFR': '#FED215', - 'WDR5/MLL': '#F5F6F7', - 'XIAP / Smac': '#128C6D', - 'ZipA / ftsZ': '#FEFE01', - }[ppiName]; + var hash = 0; + if (ppiName == null || ppiName.length === 0) return hash; + for (var i = 0; i < ppiName.length; i++) { + hash = ppiName.charCodeAt(i) + ((hash << 5) - hash); + hash = hash & hash; + } + var color = '#'; + for (var i = 0; i < 3; i++) { + var value = (hash >> (i * 8)) & 255; + color += ('00' + value.toString(16)).substr(-2); + } + return color; } var preparePerFamilyBiplotData = function (plotData) { diff --git a/ippisite/ippidb/tasks.py b/ippisite/ippidb/tasks.py index becdf53d629f528b16f3b222823eb255bb781e48..5776ea4e8a231db1ffc2b08f9f538b84ea2b82bb 100644 --- a/ippisite/ippidb/tasks.py +++ b/ippisite/ippidb/tasks.py @@ -4,11 +4,15 @@ import tempfile import io import base64 import itertools +import time +import random -from celery import shared_task -import seaborn as sns +from celery import task, states, chain, group +from ippisite.decorator import MonitorTask import matplotlib.pyplot as plt + +import seaborn as sns import numpy as np import pandas as pd from sklearn.decomposition import PCA @@ -19,7 +23,9 @@ from django.forms.models import model_to_dict from .models import ( Compound, + CompoundJob, Contribution, + Job, update_compound_cached_properties, LeLleBiplotData, PcaBiplotData, @@ -27,6 +33,8 @@ from .models import ( from .utils import smi2sdf from .gx import GalaxyCompoundPropertiesRunner +plt.switch_backend("Agg") + def dec(decimal_places): def func(number): @@ -123,9 +131,8 @@ def compute_compound_properties(compound_ids): compound.save() -def compute_drugbank_similarity(compound_ids): - compounds = Compound.objects.filter(id__in=compound_ids) - for c in compounds: +def compute_drugbank_similarity(qs): + for c in qs: c.save(autofill=True) pass @@ -206,6 +213,8 @@ def generate_pca_plot(): df = pd.DataFrame(values_list) # drop compounds with undefined property values df.dropna(how="any", inplace=True) + # reset index so that it can be joined with PCA results + df.reset_index(inplace=True) # prepare correlation circle figure plt.switch_backend("Agg") fig_, ax = plt.subplots(figsize=(6, 6)) @@ -267,6 +276,7 @@ def generate_pca_plot(): principal_df = principal_df.iloc[:, 0:2] principal_df = principal_df.rename(columns={0: "x", 1: "y"}) final_df = pd.concat([principal_df, df[["family", "id"]]], axis=1) + final_df.dropna(how="any", inplace=True) for index, row in final_df.iterrows(): smiles = Compound.objects.get(id=row.id).canonical_smile pca_data.append( @@ -297,67 +307,126 @@ def generate_pca_plot(): print("Successfully generated PCA biplot data") -@shared_task -def validate_compounds(compound_ids): +@task(base=MonitorTask, bind=True) +def launch_test_command_caching(self): """ - This task will perform all computations and validate the compound - It also regenerates the LE-LLE and PCA plots + Test task of IppidbTask """ - compute_compound_properties(compound_ids) - update_compound_cached_properties(Compound.objects.filter(id__in=compound_ids)) - compute_drugbank_similarity(compound_ids) + self.write(std_out="Before first sleep, state={}".format(self.state)) + time.sleep(30) + self.update_state(state=states.STARTED) + self.write(std_out="After first sleep, state={}".format(self.state)) + num = random.random() + if num > 0.5: + raise Exception("ERROR: {} is greater than 0.5".format(num)) + + +@task(base=MonitorTask, bind=True) +def run_compute_compound_properties(self, compound_id): + self.update_state(state=states.STARTED) + cj = CompoundJob() + cj.compound = Compound.objects.get(id=compound_id) + cj.job = Job.objects.get(task_result__task_id=self.task_id) + cj.save() + self.write(std_out=f"Starting computation of compound properties for {compound_id}") + compute_compound_properties([compound_id]) + self.write(std_out=f"Finished computation of compound properties for {compound_id}") + + +@task(base=MonitorTask, bind=True) +def run_update_compound_cached_properties(self, compound_ids=None): + if compound_ids: + qs = Compound.objects.filter(id__in=compound_ids) + else: + qs = Compound.objects.validated() + self.update_state(state=states.STARTED) + self.write( + std_out=f"Starting caching of compound properties for {compound_ids or 'all compounds'}" + ) + update_compound_cached_properties(qs) + self.write( + std_out=f"Finished caching of compound properties for {compound_ids or 'all compounds'}" + ) + + +@task(base=MonitorTask, bind=True) +def run_compute_drugbank_similarity(self, compound_ids=None): + if compound_ids: + qs = Compound.objects.filter(id__in=compound_ids) + else: + qs = Compound.objects.validated() + self.update_state(state=states.STARTED) + self.write( + std_out=f"Starting computing Drugbank similarity for {compound_ids or 'all compounds'}" + ) + compute_drugbank_similarity(qs) + self.write( + std_out=f"Finished computing Drugbank similarity for {compound_ids or 'all compounds'}" + ) + + +@task(base=MonitorTask, bind=True) +def run_validate(self, compound_ids): + self.update_state(state=states.STARTED) + self.write(std_out=f"Starting validation of compounds {compound_ids}") validate(compound_ids) - generate_le_lle_plot() - generate_pca_plot() + self.write(std_out=f"Finished validation of compounds {compound_ids}") -@shared_task -def launch_validate_contributions(contribution_ids): - """ - This task will perform, for a given set of contributions, - all computations on the compounds and validate the contributions - It also regenerates the LE-LLE and PCA plots - """ - for cont in Contribution.objects.filter(id__in=contribution_ids): - try: - for compound_action in cont.ppi.compoundaction_set.all(): - compound = compound_action.compound - compute_compound_properties([compound.id]) - update_compound_cached_properties( - Compound.objects.filter(id__in=[compound.id]) - ) - compute_drugbank_similarity([compound.id]) - validate([compound.id]) - except Exception: - print(compound.id) +@task(base=MonitorTask, bind=True) +def run_le_lle_plot(self): + self.update_state(state=states.STARTED) + self.write(std_out=f"Starting computing LE-LLE plot") generate_le_lle_plot() + self.write(std_out=f"Finished computing LE-LLE plot") + + +@task(base=MonitorTask, bind=True) +def run_pca_plot(self): + self.update_state(state=states.STARTED) + self.write(std_out=f"Starting computing PCA plot") generate_pca_plot() + self.write(std_out=f"Finished computing PCA plot") -@shared_task -def launch_compound_properties_caching(): - """ - This task will perform, for all already validated compounds, - the caching of the properties. - """ - validated_compounds = Compound.objects.validated() - update_compound_cached_properties(validated_compounds) +def launch_validate_contributions(contribution_ids): + contribution_jobs = [] + for cont in Contribution.objects.filter(id__in=contribution_ids): + compound_ids = [ + compound_action.compound.id + for compound_action in cont.ppi.compoundaction_set.all() + ] + # build a group of jobs, one job for each compound properties computation + run_compounds_properties_computation_group = group( + [ + run_compute_compound_properties.si(compound_id) + for compound_id in compound_ids + ] + ) + # build the "main" job + compounds_properties_computation_group = chain( + run_compounds_properties_computation_group, + run_update_compound_cached_properties.si(compound_ids), + run_compute_drugbank_similarity.si(compound_ids), + run_validate.si(compound_ids), + ) + contribution_jobs.append(compounds_properties_computation_group) + # compounds_properties_computation_group.delay() + run_contribution_jobs = group(contribution_jobs) + workflow = chain(run_contribution_jobs, run_le_lle_plot.si(), run_pca_plot.si()) + workflow.delay() -@shared_task -def launch_drugbank_similarity_computing(): +def launch_update_compound_cached_properties(): """ - This task will perform, for all already validated compounds, - the computing of drugbank similarity. + This task will launch the caching of properties on all compounds """ - validated_compounds = Compound.objects.validated() - compute_drugbank_similarity(validated_compounds) + run_update_compound_cached_properties.delay() -@shared_task def launch_plots_computing(): """ This task will perform the computing of LE-LLE and PCA plots. """ - generate_le_lle_plot() - generate_pca_plot() + workflow = chain(run_le_lle_plot.si(), run_pca_plot.si()) + workflow.delay() diff --git a/ippisite/ippidb/templates/about-general.html b/ippisite/ippidb/templates/about-general.html index ed2ee10e8e7abfa8a044f58d180be82cb694e684..5bdc1267f2001912b62cc5c09a2de32711286c6c 100644 --- a/ippisite/ippidb/templates/about-general.html +++ b/ippisite/ippidb/templates/about-general.html @@ -32,4 +32,42 @@ </div> </div> +<h2 id="terms-privacy" class="mt-5 text-center">Terms and Privacy</h2> + +<h5 class="row justify-content-center">What personal data is collected?</h5> + +<div class="row justify-content-center"> + <div class="col-md-auto"> + We collect some of your personal data: + <ul class="list-styled"> + <li>IP address, date and time of visit to the service, operating system, browser user agent, for all users</li> + <li>ORCID ID or email for registered users</li> + </ul> +</div> +</div> + + +<h5 class="row justify-content-center">Cookies and analytics</h5> + +<div class="row justify-content-center"> + <div class="col-md-auto"> + <p>We use cookies to manage sessions and user authentication across this website.</p> + + {% if gacode %} + + <p>We use <a href="https://analytics.google.com" target="_blank" class="ml-1 mr-1">Google Analytics</a> to collect statistics about iPPI-DB.</p> + <p>These statistics are not used to track users individually, but rather to monitor the performance and the usage of the website.</p> + <p>Therefore, some data regarding your use of this website is sent to this third party, including:</p> + <ul class="list-styled"> + <li>IP address</li> + <li>Date and time of a visit to the service</li> + <li>Operating system</li> + <li>Browser</li> + <li>Amount of data transmitted</li> + </ul> + {% endif %} + </div> +</div> + + {% endblock %} diff --git a/ippisite/ippidb/templates/about-le-lle.html b/ippisite/ippidb/templates/about-le-lle.html index b22b7944fae92f0ca6294e5eae22475edb4146a2..5527f56dd4e222567342a704995b95421384ac8b 100644 --- a/ippisite/ippidb/templates/about-le-lle.html +++ b/ippisite/ippidb/templates/about-le-lle.html @@ -6,7 +6,7 @@ {% block pagetitle %}Efficiencies: iPPI-DB biplot LE versus LLE{% endblock%} {% block view_content %} - +{% if le_lle_biplot_data %} <div class="row"> <div class="col-md-12"> <canvas id="le_lle_biplot"></canvas> @@ -21,4 +21,9 @@ <script> drawCompoundsBiplotChart('le_lle_biplot', preparePerFamilyBiplotData({{ le_lle_biplot_data | safe }}), 'pharmacology', {'legend':{'position':'right'}}); </script> +{% else %} + <div class="alert alert-danger" role="alert"> + The LE-LLE data are not currently available + </div> +{% endif %} {% endblock %} diff --git a/ippisite/ippidb/templates/about-pca.html b/ippisite/ippidb/templates/about-pca.html index 104fbd1099d896233e0185174f9dfd3506629fa4..ec381448ac030c6c4743782429700a21a05846e0 100644 --- a/ippisite/ippidb/templates/about-pca.html +++ b/ippisite/ippidb/templates/about-pca.html @@ -5,6 +5,7 @@ {% block pagetitle %}PHYSICOCHEMISTRY{% endblock%} {% block view_content %} +{% if pca_biplot_data %} <div class="row"> <canvas id="pca_biplot"></canvas> </div> @@ -17,4 +18,9 @@ <script> drawCompoundsBiplotChart('pca_biplot', preparePerFamilyBiplotData({{ pca_biplot_data | safe }}.data), 'physicochemical', { 'legend': { 'position': 'right' } }); </script> +{% else %} + <div class="alert alert-danger" role="alert"> + The PCA data are not currently available + </div> +{% endif %} {% endblock %} \ No newline at end of file diff --git a/ippisite/ippidb/templates/base.html b/ippisite/ippidb/templates/base.html index adf6495e81c32ec4296e1dc3930bdde8dc4d50eb..492d83ef61524dfb716b77162f4a9d42c67e8d32 100644 --- a/ippisite/ippidb/templates/base.html +++ b/ippisite/ippidb/templates/base.html @@ -6,6 +6,19 @@ <head> <title>IPPI-DB {% block title %}{% endblock %}</title> + {% if gacode %} + <!-- Global site tag (gtag.js) - Google Analytics --> + <script async src="https://www.googletagmanager.com/gtag/js?id={{ gacode }}"></script> + <script> + window.dataLayer = window.dataLayer || []; + function gtag(){dataLayer.push(arguments);} + gtag('js', new Date()); + + gtag('config', '{{ gacode }}'); + </script> + {% endif %} + + <link rel="stylesheet" href="/static/bootstrap/css/bootstrap.min.css"> <link rel="stylesheet" href="/static/css/fonts.css"> <link rel="stylesheet" href="/static/css/main.css"> @@ -32,6 +45,23 @@ </head> <body {% if debug %}class="debug"{%endif%} style="min-height:100vh; display:flex; flex-direction:column;"> + {% if gacode %} + <div id="consent" class="fixed-bottom pl-5 pr-5 pt-2 pb-2 text-light bg-dark text-center"> + By using the site you are agreeing to the use of third party cookies for statistical purposes. You can read more about our policy <a href="/about-general/#terms-privacy"><b>here</b></a>. + <button id="consentBtn" class="btn btn-dark ml-2" type="button" aria-label="Accept">Accept + </button> + </div> + <script> + if(localStorage.getItem('consent') == 'true'){ + $('#consent').hide(); + } + var consent = function(){ + localStorage.setItem('consent', 'true'); + $('#consent').hide(); + } + $('#consentBtn').click(consent); + </script> + {% endif %} <div class="jumbotron logoJum"> <div class="container"> <a href="/" id="home"> diff --git a/ippisite/ippidb/templates/citation.html b/ippisite/ippidb/templates/citation.html deleted file mode 100644 index f5e6e1b434a7460005e4ee54ee62dba8154f6864..0000000000000000000000000000000000000000 --- a/ippisite/ippidb/templates/citation.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "index.html" %} - - -{% block title %}How to cite?{% endblock %} - -{% block content %} -<div class="inner-wrap"> - <div id="main-wrapper" class="page"> - <div id="main"> - <div id="content" class="main-content"> - <div class="section"> - <main role="main"> - <h1 class="page-title"> {% block pagetitle %}HOW TO CITE?{% endblock%} </h1> - <div class="tabs"></div> - <div class="main__inner"> - <div class="region region-content"> - <div class="page-intro"> - </div> - <div class="color-wrap"> - <div class="region region-content"> - <div class="block block-system block-system-main"> - <div class="content"> - <div class="view-content"> -{% block view_content %} -<div> - -</div> -{% endblock %} - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </main><!-- .site-main --> - </div> - </div> - </div><!-- div main --> - </div> -</div> -{% endblock %} diff --git a/ippisite/ippidb/templates/compound_form_content.html b/ippisite/ippidb/templates/compound_form_content.html index 38bdb0b6e503d4d766170b589c24ea1526c47121..98254065db4d7bbbbc209b40de73418956951245 100644 --- a/ippisite/ippidb/templates/compound_form_content.html +++ b/ippisite/ippidb/templates/compound_form_content.html @@ -41,7 +41,7 @@ {{form.common_name.label}} </label> </div> - {%if form.is_a_ligand != None %} + {%if form.show_is_a_ligand != None %} <div class="input-inline"> <input type="text" name="{{form.ligand_id.html_name}}" {%if form.ligand_id.value %}value="{{form.ligand_id.value}}" {%endif%} diff --git a/ippisite/ippidb/templates/credits.html b/ippisite/ippidb/templates/credits.html deleted file mode 100644 index 99412a4aed01daca7e8d9147384188d62b439fd1..0000000000000000000000000000000000000000 --- a/ippisite/ippidb/templates/credits.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "index.html" %} - - -{% block title %}Credits{% endblock %} - -{% block content %} -<div class="inner-wrap"> - <div id="main-wrapper" class="page"> - <div id="main"> - <div id="content" class="main-content"> - <div class="section"> - <main role="main"> - <h1 class="page-title"> {% block pagetitle %}CREDITS{% endblock%} </h1> - <div class="tabs"></div> - <div class="main__inner"> - <div class="region region-content"> - <div class="page-intro"> - </div> - <div class="color-wrap"> - <div class="region region-content"> - <div class="block block-system block-system-main"> - <div class="content"> - <div class="view-content"> -{% block view_content %} -<div> - -</div> -{% endblock %} - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </main><!-- .site-main --> - </div> - </div> - </div><!-- div main --> - </div> -</div> -{% endblock %} diff --git a/ippisite/ippidb/templates/ippidb/refcompoundbiblio_table.html b/ippisite/ippidb/templates/ippidb/refcompoundbiblio_table.html index 02b9d8614641a0c7c69f4caf8345fe40394cc36e..bfc18ca1fe3b751aed83646235f45981703e4107 100644 --- a/ippisite/ippidb/templates/ippidb/refcompoundbiblio_table.html +++ b/ippisite/ippidb/templates/ippidb/refcompoundbiblio_table.html @@ -7,6 +7,7 @@ <th>{{sample|verbose_name:'compound_name'}}</th> <th>{{sample.compound|verbose_name:'canonical_smile'}}</th> <th>{{sample.compound|verbose_name:'iupac_name'}}</th> + <th>{{sample.compound|verbose_name:'ligand_id'}}</th> <th><i class="fa fa-link"></i> </tr> {%endwith%} @@ -19,7 +20,8 @@ <canvas width="250" height="250" data-smiles="{{o.compound.canonical_smile}}"></canvas> {{o.compound.canonical_smile}} </td> - <td>{{o.compound.iupac_name}}</td> + <td>{{o.compound.iupac_name | default_if_none:""}}</td> + <td><a href="https://www.rcsb.org/ligand/{{ o.compound.ligand_id }}" target="_blank">{{ o.compound.ligand_id | default_if_none:""}}</a></td> <td> <a target="_blank" href="{% url 'compound_card' pk=o.compound.pk %}"><i class="fa fa-link"></i></a> </td> diff --git a/ippisite/ippidb/templates/news.html b/ippisite/ippidb/templates/news.html deleted file mode 100644 index d629e62d04d10d3055cf75b853c61e917af59f66..0000000000000000000000000000000000000000 --- a/ippisite/ippidb/templates/news.html +++ /dev/null @@ -1,42 +0,0 @@ -{% extends "index.html" %} - - -{% block title %}Latest news{% endblock %} - -{% block content %} -<div class="inner-wrap"> - <div id="main-wrapper" class="page"> - <div id="main"> - <div id="content" class="main-content"> - <div class="section"> - <main role="main"> - <h1 class="page-title"> {% block pagetitle %}LATEST NEWS{% endblock%} </h1> - <div class="tabs"></div> - <div class="main__inner"> - <div class="region region-content"> - <div class="page-intro"> - </div> - <div class="color-wrap"> - <div class="region region-content"> - <div class="block block-system block-system-main"> - <div class="content"> - <div class="view-content"> -{% block view_content %} -<div> - -</div> -{% endblock %} - </div> - </div> - </div> - </div> - </div> - </div> - </div> - </main><!-- .site-main --> - </div> - </div> - </div><!-- div main --> - </div> -</div> -{% endblock %} diff --git a/ippisite/ippidb/tests/__init__.py b/ippisite/ippidb/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_2380_002_e-9.yaml b/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_2380_002_e-9.yaml new file mode 100644 index 0000000000000000000000000000000000000000..842a1db3b1005febf7a9428c0043816b08c0011d --- /dev/null +++ b/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_2380_002_e-9.yaml @@ -0,0 +1,43 @@ +activity_tests: +- cell_line_name: '' + compound_activity_results: + - activity_mol: 2380.002 + activity_type: pIC50 + activity_unit: 1e-9 + compound_name: toto + modulation_type: I + is_primary: true + nb_active_compounds: 2 + protein_bound_construct: F + protein_complex: '2' + test_modulation_type: I + test_name: test + test_type: BIOCH +complex: +- cc_nb: 1 + complex_type: Partner + domain_pfam_acc: PF05053 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: '' + uniprot_id: O00255 +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF05965 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: Q03164 +complexChoice: inhibited +complexType: Inhib_Hetero2merAB +compounds: +- common_name: super + compound_name: toto + molecule_smiles: CCC +diseases: +- ;toto;1; +- ;titi;sfddf; +- ;tata;3232; +family_name: Menin (PF05053) +id_source: '15072770' +in_silico: true +pdb_id: 3u85 +source: PM diff --git a/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_4_9846073_e-6.yaml b/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_4_9846073_e-6.yaml new file mode 100644 index 0000000000000000000000000000000000000000..0a2e61b2d45c324e1ce6c7f00592a3ce046abe87 --- /dev/null +++ b/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_4_9846073_e-6.yaml @@ -0,0 +1,43 @@ +activity_tests: +- cell_line_name: '' + compound_activity_results: + - activity_mol: 4.9846073 + activity_type: pIC50 + activity_unit: 1e-6 + compound_name: toto + modulation_type: I + is_primary: true + nb_active_compounds: 2 + protein_bound_construct: F + protein_complex: '2' + test_modulation_type: I + test_name: test + test_type: BIOCH +complex: +- cc_nb: 1 + complex_type: Partner + domain_pfam_acc: PF05053 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: '' + uniprot_id: O00255 +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF05965 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: Q03164 +complexChoice: inhibited +complexType: Inhib_Hetero2merAB +compounds: +- common_name: super + compound_name: toto + molecule_smiles: CCC +diseases: +- ;toto;1; +- ;titi;sfddf; +- ;tata;3232; +family_name: Menin (PF05053) +id_source: '15072770' +in_silico: true +pdb_id: 3u85 +source: PM diff --git a/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_639_39406_e-3.yaml b/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_639_39406_e-3.yaml new file mode 100644 index 0000000000000000000000000000000000000000..459d512dd9b0f3557f5bfb3e50dcaf0140eae7a0 --- /dev/null +++ b/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_639_39406_e-3.yaml @@ -0,0 +1,43 @@ +activity_tests: +- cell_line_name: '' + compound_activity_results: + - activity_mol: 639.39406 + activity_type: pIC50 + activity_unit: 1e-3 + compound_name: toto + modulation_type: I + is_primary: true + nb_active_compounds: 2 + protein_bound_construct: F + protein_complex: '2' + test_modulation_type: I + test_name: test + test_type: BIOCH +complex: +- cc_nb: 1 + complex_type: Partner + domain_pfam_acc: PF05053 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: '' + uniprot_id: O00255 +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF05965 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: Q03164 +complexChoice: inhibited +complexType: Inhib_Hetero2merAB +compounds: +- common_name: super + compound_name: toto + molecule_smiles: CCC +diseases: +- ;toto;1; +- ;titi;sfddf; +- ;tata;3232; +family_name: Menin (PF05053) +id_source: '15072770' +in_silico: true +pdb_id: 3u85 +source: PM diff --git a/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_6_85_e-0.yaml b/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_6_85_e-0.yaml new file mode 100644 index 0000000000000000000000000000000000000000..b61f094338e568128575ea14d577a93e7f9b7088 --- /dev/null +++ b/ippisite/ippidb/tests/test_activity_computation_and_storage_pIC50_6_85_e-0.yaml @@ -0,0 +1,43 @@ +activity_tests: +- cell_line_name: '' + compound_activity_results: + - activity_mol: 6.85 + activity_type: pIC50 + activity_unit: 1e-0 + compound_name: toto + modulation_type: I + is_primary: true + nb_active_compounds: 2 + protein_bound_construct: F + protein_complex: '2' + test_modulation_type: I + test_name: test + test_type: BIOCH +complex: +- cc_nb: 1 + complex_type: Partner + domain_pfam_acc: PF05053 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: '' + uniprot_id: O00255 +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF05965 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: Q03164 +complexChoice: inhibited +complexType: Inhib_Hetero2merAB +compounds: +- common_name: super + compound_name: toto + molecule_smiles: CCC +diseases: +- ;toto;1; +- ;titi;sfddf; +- ;tata;3232; +family_name: Menin (PF05053) +id_source: '15072770' +in_silico: true +pdb_id: 3u85 +source: PM diff --git a/ippisite/ippidb/tests/test_basic_entry.yaml b/ippisite/ippidb/tests/test_basic_entry.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c1baf06500ae82f25c3fa1063b0373433336c7f8 --- /dev/null +++ b/ippisite/ippidb/tests/test_basic_entry.yaml @@ -0,0 +1,43 @@ +activity_tests: +- cell_line_name: '' + compound_activity_results: + - activity_mol: 6.85 + activity_type: pIC50 + activity_unit: 1e-6 + compound_name: toto + modulation_type: I + is_primary: true + nb_active_compounds: 2 + protein_bound_construct: F + protein_complex: '2' + test_modulation_type: I + test_name: test + test_type: BIOCH +complex: +- cc_nb: 1 + complex_type: Partner + domain_pfam_acc: PF05053 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: '' + uniprot_id: O00255 +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF05965 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: Q03164 +complexChoice: inhibited +complexType: Inhib_Hetero2merAB +compounds: +- common_name: super + compound_name: toto + molecule_smiles: CCC +diseases: +- ;toto;1; +- ;titi;sfddf; +- ;tata;3232; +family_name: Menin (PF05053) +id_source: '15072770' +in_silico: true +pdb_id: 3u85 +source: PM diff --git a/ippisite/ippidb/tests/test_complex_no_pfam.yaml b/ippisite/ippidb/tests/test_complex_no_pfam.yaml new file mode 100644 index 0000000000000000000000000000000000000000..03e6c2cf616df4f93a3aefccbeb06d7a7c47b682 --- /dev/null +++ b/ippisite/ippidb/tests/test_complex_no_pfam.yaml @@ -0,0 +1,40 @@ +activity_tests: +- cell_line_name: '' + compound_activity_results: + - activity_mol: 6.85 + activity_type: pIC50 + activity_unit: 1e-3 + compound_name: toto + modulation_type: I + is_primary: true + nb_active_compounds: 2 + protein_bound_construct: F + protein_complex: '1' + test_modulation_type: I + test_name: test + test_type: BIOCH +complex: +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF00400 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: P61964 +- cc_nb: 1 + complex_type: Partner + domain_pfam_acc: null + ppc_copy_nb: 1 + ppp_copy_nb_per_p: '' + uniprot_id: Q03164 +complexChoice: inhibited +complexType: Inhib_Hetero2merAB +compounds: +- common_name: super + compound_name: toto + molecule_smiles: C +diseases: [] +family_name: WD40 (PF00400) +id_source: '26958703' +in_silico: true +pdb_id: 3emh +source: PM diff --git a/ippisite/ippidb/tests/test_entry_28.yaml b/ippisite/ippidb/tests/test_entry_28.yaml new file mode 100644 index 0000000000000000000000000000000000000000..d22b2cb8274900a4497432bd1b2b68df806fe256 --- /dev/null +++ b/ippisite/ippidb/tests/test_entry_28.yaml @@ -0,0 +1,41 @@ +activity_tests: +- cell_line_name: '' + compound_activity_results: + - activity_mol: 3.59 + activity_type: pKd + activity_unit: 1e-9 + compound_name: '2' + modulation_type: I + is_primary: true + nb_active_compounds: 1 + protein_bound_construct: F + protein_complex: '1' + test_modulation_type: B + test_name: biolayer interferometry assay + test_type: BIOCH +complex: +- cc_nb: 1 + complex_type: Partner + domain_pfam_acc: null + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: Q60795 +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF01344 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: Q9Z2X8 +complexChoice: inhibited +complexType: Inhib_Hetero2merAB +compounds: +- common_name: '' + compound_name: '2' + molecule_smiles: c1cc2c(cc1)c(ccc2N(S(=O)(=O)c1ccc(cc1)OC)CC(=O)O)N(S(=O)(=O)c1ccc(cc1)OC)CC(=O)O +diseases: +- ;cancer;MONDO:0004992; +family_name: KEAP1 / NRF2 +id_source: '24512214 ' +in_vitro: true +pdb_id: 3wn7 +source: PM diff --git a/ippisite/ippidb/tests/test_simple_heterodimer.yaml b/ippisite/ippidb/tests/test_simple_heterodimer.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c3c5f7c7e926a4ff22dea3dc30fe61645921323b --- /dev/null +++ b/ippisite/ippidb/tests/test_simple_heterodimer.yaml @@ -0,0 +1,51 @@ +activity_tests: +- cell_line_name: '' + compound_activity_results: + - activity_mol: 6.85 + activity_type: pIC50 + activity_unit: 1e-3 + compound_name: 16d + modulation_type: I + is_primary: true + nb_active_compounds: 2 + protein_bound_construct: F + protein_complex: '1' + test_modulation_type: I + test_name: test + test_type: BIOCH +complex: +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF00400 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: P61964 +- cc_nb: 1 + complex_type: Partner + domain_pfam_acc: null + ppc_copy_nb: 1 + ppp_copy_nb_per_p: '' + uniprot_id: Q03164 +complexChoice: inhibited +complexType: Inhib_Hetero2merAB +compounds: +- common_name: officially 16d + compound_name: 16d + ligand_id: ABC + molecule_smiles: CN1CCN(CC1)C1=C(C=C(C=C1)C1=CC(=CC=C1)CN1CCOCC1)NC(=O)C1=CNC(C=C1C(F)(F)F)=O +- common_name: officially 14a + compound_name: 14a + is_macrocycle: false + molecule_iupac: N-(2-(4-Methylpiperazin-1-yl)-5-nitrophenyl)-6-oxo-4-(trifluoromethyl)-1,6-dihydropyridine-3-carboxamide +- common_name: stuck + compound_name: lili + is_macrocycle: false + molecule_smiles: C1CCC1 +diseases: [] +family_name: WD40 (PF00400) +id_source: '26958703' +in_silico: true +in_vitro: true +pdb_id: 3emh +source: PM +xray: true diff --git a/ippisite/ippidb/tests/test_simple_heterodimer_208.yaml b/ippisite/ippidb/tests/test_simple_heterodimer_208.yaml new file mode 100644 index 0000000000000000000000000000000000000000..13859df89dc9adc196434a2d499cd82418d011a2 --- /dev/null +++ b/ippisite/ippidb/tests/test_simple_heterodimer_208.yaml @@ -0,0 +1,57 @@ +source: DO +id_source: '10.1021/jm300608w' +in_vitro: true +in_cellulo: true +xray: true +pdb_id: 1g5j +complexChoice: inhibited +complexType: Inhib_Hetero2merAB +complex: + - cc_nb: 1 + complex_type: Partner + domain_pfam_acc: null + ppc_copy_nb: 1 + ppp_copy_nb_per_p: '' + uniprot_id: Q92934 + - cc_nb: 1 + complex_type: Bound + domain_pfam_acc: null + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: Q07817 +family_name: BCL-XL / Bad +diseases: +- ;cancer;MONDO:0004992; +compounds: + - common_name: + compound_name: compound 12 + ligand_id: 03B + molecule_smiles: CN(C)CC[C@H](CSc1ccccc1)Nc2ccc(cc2[N+]([O-])=O)[S](=O)(=O)Nc3ccc(cc3)N4CCN(CC4)c5cccc(c5)c6c(n(C)c(C)c6C(O)=O)c7ccc(CI)cc7 +activity_tests: + - test_type: BIOCH + test_name: Fluorescence Polarization + protein_bound_construct: U + test_modulation_type: I + nb_active_compounds: 1 + cell_line_name: '' + protein_complex: '1' + compound_activity_results: + - activity_mol: 6 + activity_type: pIC50 + activity_unit: 1e-9 + compound_name: 'compound 12' + modulation_type: I + - test_type: CELL + test_name: cell growth inhibition + protein_bound_construct: U + test_modulation_type: I + nb_active_compounds: 1 + cell_line_name: 'Human small-cell lung cancer' + protein_complex: '1' + compound_activity_results: + - activity_mol: 61 + activity_type: pIC50 + activity_unit: 1e-9 + compound_name: 'compound 12' + modulation_type: I + \ No newline at end of file diff --git a/ippisite/ippidb/tests/test_simple_stabilized_heterodimer.yaml b/ippisite/ippidb/tests/test_simple_stabilized_heterodimer.yaml new file mode 100644 index 0000000000000000000000000000000000000000..1d79dd1e8f4868957a4919195a9feecb6eae92fd --- /dev/null +++ b/ippisite/ippidb/tests/test_simple_stabilized_heterodimer.yaml @@ -0,0 +1,44 @@ +activity_tests: +- cell_line_name: '' + compound_activity_results: + - activity_mol: 0.25 + activity_type: pIC50 + activity_unit: 1e-6 + compound_name: FC + modulation_type: S + is_primary: true + nb_active_compounds: 2 + protein_bound_construct: F + protein_complex: '1' + test_modulation_type: I + test_name: pull down + test_type: BIOCH +complex: +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF00244 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: P31947 +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: null + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: P03372 +complexChoice: stabilized +complexType: Stab_Hetero2merAB +compounds: +- common_name: fusicoccin + compound_name: FC + ligand_id: FSC + molecule_smiles: COC[C@H]1CC[C@H]2[C@@H](C)[C@@H](O)[C@H](O[C@H]3O[C@H](COC(C)(C)C=C)[C@@H](O)[C@H](OC(C)=O)[C@H]3O)C3=C(C[C@H](O)[C@]3(C)\C=C1/2)[C@H](C)COC(C)=O +diseases: +- ;breast cancer;MONDO:0007254; +family_name: 14-3-3 / ER +id_source: '23676274' +in_cellulo: true +in_vitro: true +pdb_id: 4jc3 +source: PM +xray: true diff --git a/ippisite/ippidb/tests/test_stabilizer_204.yaml b/ippisite/ippidb/tests/test_stabilizer_204.yaml new file mode 100644 index 0000000000000000000000000000000000000000..59cd362476b8f5d6fe7f041faa8fec36b90a23c2 --- /dev/null +++ b/ippisite/ippidb/tests/test_stabilizer_204.yaml @@ -0,0 +1,40 @@ +source: PM +id_source: '23808890' +in_vitro: true +xray: true +pdb_id: 4ihl +complexChoice: stabilized +complexType: Stab_Hetero2merAB +complex: + - cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF00244 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: P63104 + - cc_nb: 1 + complex_type: Bound + domain_pfam_acc: null + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: P04049 +family_name: 14-3-3 / C-RAF +diseases: + - ;melanoma;(MONDO_0005105); +compounds: + - common_name: Cotylenin A + compound_name: Cotylenin A + molecule_smiles: COC[C@H]1O[C@H](O[C@H]2[C@H](O)[C@H](C)[C@@H]3CC[C@](O)(COC)C3=C[C@@]3(C)CCC(C(C)C)=C23)[C@H](O)[C@H]2O[C@H]3O[C@@]12O[C@@]3(C)[C@@H]1CO1 +activity_tests: +- test_type: BIOCH + test_name: Fluorescence Polarization + protein_bound_construct: F + test_modulation_type: B + nb_active_compounds: 1 + protein_complex: '1' + compound_activity_results: + - activity_mol: 20 + activity_type: pKd + activity_unit: 1e-10 + compound_name: Cotylenin A + modulation_type: S \ No newline at end of file diff --git a/ippisite/ippidb/tests/test_with_all_tests.yaml b/ippisite/ippidb/tests/test_with_all_tests.yaml new file mode 100644 index 0000000000000000000000000000000000000000..3b8fefd311af4176a2e92deb7d9f5cef535b9cd5 --- /dev/null +++ b/ippisite/ippidb/tests/test_with_all_tests.yaml @@ -0,0 +1,72 @@ +activity_tests: +- cell_line_name: '' + compound_activity_results: + - activity_mol: 6.85 + activity_type: pIC50 + activity_unit: 1e-3 + compound_name: toto + modulation_type: I + is_primary: true + nb_active_compounds: 2 + protein_bound_construct: F + protein_complex: '2' + test_modulation_type: I + test_name: test + test_type: BIOCH +complex: +- cc_nb: 1 + complex_type: Partner + domain_pfam_acc: PF05053 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: '' + uniprot_id: O00255 +- cc_nb: 1 + complex_type: Bound + domain_pfam_acc: PF05965 + ppc_copy_nb: 1 + ppp_copy_nb_per_p: 1 + uniprot_id: Q03164 +complexChoice: inhibited +complexType: Inhib_Hetero2merAB +compounds: +- common_name: super + compound_name: toto + molecule_smiles: CCC +cytotox: true +cytotox_tests: +- cell_line_name: '1043SK ' + compound_concentration: '2' + compound_cytotox_results: + - compound_name: toto + toxicity: 'True' + test_name: test +diseases: +- '' +- ;titi;sfddf; +- ;tutu;5456456; +- ;tutu;5456456; +- '' +family_name: Menin (PF05053) +id_source: '15072770' +in_silico: true +pdb_id: 3u85 +pharmacokinetic: true +pharmacokinetic_tests: +- administration_mode: IV + cell_line_name: '1043SK ' + compound_pk_results: + - auc: '1' + c_max: '5.2' + clearance: '1.2' + compound_name: toto + oral_bioavailability: '22' + t_demi: '5' + t_max: '5' + tolerated: 'True' + voldistribution: '5.5' + concentration: '1.2' + dose: '3.5' + dose_interval: '2' + organism: 1 + test_name: test +source: PM diff --git a/ippisite/ippidb/tests.py b/ippisite/ippidb/tests/tests.py similarity index 85% rename from ippisite/ippidb/tests.py rename to ippisite/ippidb/tests/tests.py index 49a365999ad7d392c71193e9e0d5dcf4699520bc..83715ed56fc6432998ddf76a662df71c6b599720 100644 --- a/ippisite/ippidb/tests.py +++ b/ippisite/ippidb/tests/tests.py @@ -8,13 +8,24 @@ from django.contrib.auth import get_user_model from django.core.management import call_command from django.test import TestCase from django.urls import reverse -from openbabel import vectorUnsignedInt, OBFingerprint import requests -from ippidb import ws, models -from ippidb.ws import get_uniprot_info -from .models import ( +from ippidb import models +from ippidb.ws import ( + get_uniprot_info, + get_doi_info, + get_pfam_info, + get_google_patent_info, + PatentNotFound, + get_pubmed_info, + get_pdb_uniprot_mapping, + get_pdb_pfam_mapping, + EntryNotFoundError, + convert_iupac_to_smiles_and_inchi, + convert_smiles_to_iupac, +) +from ippidb.models import ( Compound, CompoundTanimoto, create_tanimoto, @@ -24,14 +35,14 @@ from .models import ( Contribution, LeLleBiplotData, PcaBiplotData, + DrugBankCompound, + Protein, ) -from .models import DrugBankCompound, Protein -from .utils import FingerPrinter, mol2smi, smi2mol, smi2inchi, smi2inchikey class MolSmiTestCase(TestCase): """ - Test MOL to SMILES and SMILES to MOL format conversion functions + Test MOL to SMILES and SMILES to MOL format conversion iPPI-DB web services """ def setUp(self): @@ -44,12 +55,6 @@ class MolSmiTestCase(TestCase): " 0 0 0 0 0 0 0 0\nM END\n" ) - def test_mol2smi(self): - self.assertEqual(mol2smi(self.mol), self.smiles) - - def test_smi2mol2smi(self): - self.assertTrue(re.compile(self.mol).match(smi2mol(self.smiles))) - def test_view_smi2mol_valid(self): url = reverse("smi2mol") response = self.client.get(url, {"smiString": self.smiles}) @@ -89,94 +94,6 @@ class MolSmiTestCase(TestCase): self.assertEqual(response.status_code, 400) -class SmiInchi(TestCase): - """ - Test INCHI and INCHIKEY generation functions - """ - - def setUp(self): - self.smiles_str = ( - "CC(C)C(=O)C1=C(C(=C(C(=C1)C(=O)C2=CC=C(C=C2)" "OC3=CC=CC=C3)O)O)O" - ) - self.inchi_str = ( - "InChI=1S/C23H20O6/c1-13(2)19(24)17-12-18(22" - "(27)23(28)21(17)26)20(25)14-8-10-16(11-9-14)29-15-6-4-3-" - "5-7-15/h3-13,26-28H,1-2H3" - ) - self.inchikey_str = "CVVQMBDTMYUWTR-UHFFFAOYSA-N" - - def test_smi2inchi(self): - self.assertEqual(smi2inchi(self.smiles_str), self.inchi_str) - - def test_smi2inchikey(self): - self.assertEqual(smi2inchikey(self.smiles_str), self.inchikey_str) - - -class FingerPrinterTestCase(TestCase): - """ - Test FingerPrinter class - """ - - def setUp(self): - self.fingerprinter = FingerPrinter("FP4") - self.smiles = "CC" - self.fp = vectorUnsignedInt([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) - self.smiles_dict = {1: "CC", 2: "CCC"} - self.fp_dict = { - 1: vectorUnsignedInt([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), - 2: vectorUnsignedInt([3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), - } - self.tanimoto_dict = {1: 1.0, 2: 0.5} - - def test_fingerprints_available(self): - # test that all necessary fingerprints are available - self.assertIsNotNone(OBFingerprint.FindFingerprint("FP2")) - self.assertIsNotNone(OBFingerprint.FindFingerprint("FP4")) - self.assertIsNotNone(OBFingerprint.FindFingerprint("ECFP4")) - - def assertEqualVUI(self, vui_one, vui_two): - return self.assertEqual(list(vui_one), list(vui_two)) - - def assertEqualVUIdict(self, vuidict_one, vuidict_two): - vuidict_one = {id_: list(vui) for id_, vui in vuidict_one.items()} - vuidict_two = {id_: list(vui) for id_, vui in vuidict_two.items()} - return self.assertEqual(vuidict_one, vuidict_two) - - def test_fp(self): - self.assertEqualVUI(self.fingerprinter.fp(self.smiles), self.fp) - - def test_fp_dict(self): - self.assertEqualVUIdict( - self.fingerprinter.fp_dict(self.smiles_dict), self.fp_dict - ) - - def test_tanimoto_fps(self): - self.assertEqual( - self.fingerprinter.tanimoto_fps(self.smiles, self.fp_dict), - self.tanimoto_dict, - ) - - def test_tanimoto_smiles(self): - self.assertEqual( - self.fingerprinter.tanimoto_smiles(self.smiles, self.smiles_dict), - self.tanimoto_dict, - ) - - -class FingerPrinterTestCaseCompound1ECFP4(TestCase): - def setUp(self): - self.fingerprinter = FingerPrinter("ECFP4") - self.smiles = "CC(C)C(=O)c1cc(C(=O)c2ccc(Oc3ccccc3)cc2)c(O)c(O)c1O" - self.smiles_dict = {1: "CC(C)C(=O)c1cc(C(=O)c2ccc(Oc3ccccc3)cc2)c(O)" "c(O)c1O"} - self.tanimoto_dict = {1: 1.0} - - def test_tanimoto_smiles(self): - self.assertEqual( - self.fingerprinter.tanimoto_smiles(self.smiles, self.smiles_dict), - self.tanimoto_dict, - ) - - def create_dummy_compound(id_, smiles): c = Compound() c.id = id_ @@ -634,7 +551,7 @@ class TestGetDoiInfo(TestCase): def test_get_doi_info(self): try: - resp = ws.get_doi_info("10.1073/pnas.0805139105") + resp = get_doi_info("10.1073/pnas.0805139105") except requests.exceptions.HTTPError as he: # skip this test if the DOI server throws an error # (that happens) @@ -657,7 +574,7 @@ class TestGetDoiInfo(TestCase): class TestGetPfamInfo(TestCase): def test_get_pfam_info(self): target = {"id": "bZIP_1", "description": "bZIP transcription factor"} - resp = ws.get_pfam_info("PF00170") + resp = get_pfam_info("PF00170") self.assertEqual(resp, target) @@ -672,13 +589,13 @@ class TestGetGooglePatentInfo(TestCase): " McGrew, Robert T. Bell, " "Steven Joseph Rich, Cisco Technology Inc", } - resp = ws.get_google_patent_info("US8856504") + resp = get_google_patent_info("US8856504") self.assertEqual(resp, target) def test_entry_not_found(self): self.assertRaises( - ws.PatentNotFound, - ws.get_google_patent_info, + PatentNotFound, + get_google_patent_info, "US8856504US8856504US885US8856504US8856504", ) @@ -704,7 +621,7 @@ class TestGetPubMEDIdInfo(TestCase): "authors_list": "Brancotte B, Biton A, Bernard-Pierrot I, " "Radvanyi F, Reyal F, Cohen-Boulakia S", } - resp = ws.get_pubmed_info("21349868") + resp = get_pubmed_info("21349868") self.assertEqual(resp, target) @@ -826,13 +743,13 @@ class TestGetPDBUniProtMapping(TestCase): def test_find_info(self): target = sorted(["Q03164", "O00255"]) - resp = ws.get_pdb_uniprot_mapping("3u85") + resp = get_pdb_uniprot_mapping("3u85") resp = sorted(resp) self.assertEqual(resp, target) self.assertEqual(len(resp), len(set(resp))) def test_entry_not_found(self): - self.assertRaises(ws.EntryNotFoundError, ws.get_pdb_uniprot_mapping, "Xu85") + self.assertRaises(EntryNotFoundError, get_pdb_uniprot_mapping, "Xu85") class TestGetPDBPfamMapping(TestCase): @@ -844,11 +761,11 @@ class TestGetPDBPfamMapping(TestCase): target = { "PF05053": {"identifier": "Menin", "description": "Menin", "name": "Menin"} } - resp = ws.get_pdb_pfam_mapping("3u85") + resp = get_pdb_pfam_mapping("3u85") self.assertDictEqual(resp, target) def test_entry_not_found(self): - self.assertRaises(ws.EntryNotFoundError, ws.get_pdb_uniprot_mapping, "Xu85") + self.assertRaises(EntryNotFoundError, get_pdb_pfam_mapping, "Xu85") class TestConvertIUPACToSMILESAndMore(TestCase): @@ -888,7 +805,7 @@ class TestConvertIUPACToSMILESAndMore(TestCase): ), ] for iupac, dict_expected in pairs: - dict_returned = ws.convert_iupac_to_smiles_and_inchi(iupac) + dict_returned = convert_iupac_to_smiles_and_inchi(iupac) self.assertEqual(dict_expected["smiles"], dict_returned["smiles"]) self.assertEqual(dict_expected["inchi"], dict_returned["inchi"]) self.assertEqual(dict_expected["stdinchi"], dict_returned["stdinchi"]) @@ -896,13 +813,11 @@ class TestConvertIUPACToSMILESAndMore(TestCase): def test_invalid_entry(self): self.assertRaises( - ws.EntryNotFoundError, - ws.convert_iupac_to_smiles_and_inchi, + EntryNotFoundError, + convert_iupac_to_smiles_and_inchi, "3-{1-oxo-6-[4-(piperid", ) - self.assertRaises( - ws.EntryNotFoundError, ws.convert_iupac_to_smiles_and_inchi, None - ) + self.assertRaises(EntryNotFoundError, convert_iupac_to_smiles_and_inchi, None) class TestConvertSMILESToIUPAC(TestCase): @@ -914,7 +829,7 @@ class TestConvertSMILESToIUPAC(TestCase): smiles_to_iupacs = {"CCC": "propane"} for smiles, expected_iupac in smiles_to_iupacs.items(): self.assertEqual( - ws.convert_smiles_to_iupac(smiles).lower(), expected_iupac.lower() + convert_smiles_to_iupac(smiles).lower(), expected_iupac.lower() ) diff --git a/ippisite/ippidb/tests/tests_contribute.py b/ippisite/ippidb/tests/tests_contribute.py new file mode 100644 index 0000000000000000000000000000000000000000..5ecf48a3b84c3d319a703bbc6b52bce956c8bc9a --- /dev/null +++ b/ippisite/ippidb/tests/tests_contribute.py @@ -0,0 +1,126 @@ +""" +iPPI-DB contribution module tests +""" +from django.contrib.auth import get_user_model +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.test import TestCase +from django.urls import reverse + + +from ippidb import models +from ippidb.forms import PDBForm +from live_settings import live_settings + +import requests_cache + +requests_cache.install_cache("tests_contribute_cache", backend="sqlite") + + +class BibliographyIDTestCase(TestCase): + def test_long_doi(self): + b = models.Bibliography() + b.source = "DO" + b.id_source = "10.1016/j.bmcl.2013.03.013" + b.save(autofill=True) + + +class ContributionViewsAccessTestCase(TestCase): + def setUp(self): + self.url = reverse("admin-session-add") + + self.user = get_user_model().objects.create(username="user") + + self.user_granted = get_user_model().objects.create(username="user_granted") + content_type = ContentType.objects.get_for_model(models.Contribution) + permission = Permission.objects.get( + codename="add_contribution", content_type=content_type + ) + self.user_granted.user_permissions.add(permission) + + def test_force_auth(self): + response = self.client.get(self.url) + self.assertRedirects( + response, + # expected_url=reverse('login') + "?next=" + url + expected_url="/accounts/login/?next=%s" % self.url, + ) + + def test_403_without_perm_and_access_closed(self): + self.client.force_login(self.user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + + def test_ok_with_perm_and_access_closed(self): + live_settings.open_access_to_contribution = "False" + self.test_ok_with_perm_and_access_closed_by_default() + + def test_ok_with_perm_and_access_closed_by_default(self): + self.client.force_login(self.user_granted) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 302) + + def test_ok_without_perm_and_access_open(self): + live_settings.open_access_to_contribution = "True" + self.client.force_login(self.user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 302) + + def test_ok_with_perm_and_access_open(self): + live_settings.open_access_to_contribution = "True" + self.client.force_login(self.user_granted) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 302) + + def test_ok_with_superuser(self): + self.user.is_superuser = True + self.user.save() + self.client.force_login(self.user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 302) + + def test_ko_with_staff(self): + self.user.is_staff = True + self.user.save() + self.client.force_login(self.user) + response = self.client.get(self.url) + self.assertEqual(response.status_code, 403) + + +class PDBFormTestCase(TestCase): + """ + Test the PDB form data validation + """ + + def test_valid(self): + form = PDBForm({"pdb_id": "3wn7"}) + self.assertTrue(form.is_valid(), "valid PDB code should be accepted") + + def test_invalid_PDB_format(self): + form = PDBForm({"pdb_id": "33wn7"}) + self.assertFalse(form.is_valid(), "5 char long PDB code should be rejected") + self.assertTrue( + form.has_error("pdb_id", code="max_length"), + "5 char long PDB code should be rejected with max_length error code", + ) + + def test_invalid_PDB_entry(self): + form = PDBForm({"pdb_id": "9aa9"}) + self.assertFalse( + form.is_valid(), "inexistent PDB (9aa9) code should be rejected" + ) + self.assertTrue( + form.has_error("pdb_id", code="not_found"), + "inexistent PDB (9aa9) code should be rejected with not_found error code", + ) + + def test_invalid_nouniprot(self): + form = PDBForm({"pdb_id": "3wn8"}) + self.assertFalse( + form.is_valid(), + "PDB code with no Uniprot mapping (3wn8) should be rejected", + ) + self.assertTrue( + form.has_error("pdb_id", code="no_mapping"), + "PDB code with no Uniprot mapping (3wn8) should be rejected with no_mappping error code", + ) diff --git a/ippisite/ippidb/tests/tests_contribute_e2e.py b/ippisite/ippidb/tests/tests_contribute_e2e.py new file mode 100644 index 0000000000000000000000000000000000000000..7ace673400148b6341cc7604d64a28cd0512863c --- /dev/null +++ b/ippisite/ippidb/tests/tests_contribute_e2e.py @@ -0,0 +1,628 @@ +""" +iPPI-DB contribution module "end to end" tests +""" +import math +import os +from decimal import Decimal +from tempfile import NamedTemporaryFile +import yaml +import glob + +from django.contrib.auth import get_user_model +from django.db.models import Value +from django.db.models.functions import Concat +from django.test import TestCase +from django.urls import reverse +from parameterized import parameterized +import requests_cache + +from ippidb import models +from ippidb.admin import grant_contribution_permission +from ippidb.ws import get_uniprot_info + + +requests_cache.install_cache("tests_contribute_cache", backend="sqlite") + + +def compute_ppi_name(entry_data): + """ + return the expected PPI name from a given contribution entry_data + """ + bound_uniprots = [ + item["uniprot_id"] + for item in entry_data["complex"] + if item["complex_type"] == "Bound" + ] + partner_uniprots = [ + item["uniprot_id"] + for item in entry_data["complex"] + if item["complex_type"] == "Partner" + ] + bound_protein_names = [ + get_uniprot_info(uniprot_id)["short_name"] for uniprot_id in bound_uniprots + ] + partner_protein_names = [ + get_uniprot_info(uniprot_id)["short_name"] for uniprot_id in partner_uniprots + ] + bound_protein_names.sort() + partner_protein_names.sort() + bound_str = ",".join(bound_protein_names) + partner_str = ",".join(partner_protein_names) + name = bound_str + if partner_str != "": + name += " / " + partner_str + return name + + +class ContributionE2ETestCase(TestCase): + """ + This class tests the Contribution Wizard by interacting with it + in the same way a user would do. + """ + + def setUp(self): + login = "contributor" + password = "12345" + User = get_user_model() + User.objects.create_user(username=login, password=password) + self.client.login(username=login, password=password) + grant_contribution_permission(None, None, User.objects.all()) + symmetry = models.Symmetry() + symmetry.code = "AS" + symmetry.description = "asymmetric" + symmetry.save() + models.Taxonomy.objects.create(taxonomy_id=9606, name="Homo sapiens") + + @staticmethod + def get_step_url(step_id): + step_url = reverse("admin-session-add") + if step_id is not None: + step_url += step_id + "/" + return step_url + + def _process_contribution_wizard(self, entry_data): + future_expected_equals = [ + ( + models.Bibliography.objects.count, + models.Bibliography.objects.count() + 1, + "Bibliography count", + ), + ( + models.Contribution.objects.count, + models.Contribution.objects.count() + 1, + "Contribution count", + ), + ( + models.Compound.objects.count, + models.Compound.objects.count() + len(entry_data["compounds"]), + "Compounds count", + ), + ( + models.Compound.objects.validated().count, + models.Compound.objects.validated().count(), + "Validated Compounds count should remains the same", + ), + ( + models.CompoundAction.objects.count, + models.CompoundAction.objects.count() + len(entry_data["compounds"]), + "Compound Actions count", + ), + ( + models.TestActivityDescription.objects.count, + models.TestActivityDescription.objects.count() + + len(entry_data.get("activity_tests", [])), + "Activity tests count", + ), + ( + models.TestCytotoxDescription.objects.count, + models.TestCytotoxDescription.objects.count() + + len(entry_data.get("cytotox_tests", [])), + "Cytotox tests count", + ), + ( + models.TestPKDescription.objects.count, + models.TestPKDescription.objects.count() + + len(entry_data.get("pharmacokinetic_tests", [])), + "Pharmacokinetic tests count", + ), + ( + lambda: models.PpiFamily.objects.get().name, + entry_data.get("family_name"), + "Ppi family name", + ), + ( + lambda: models.Ppi.objects.get().family.name, + entry_data.get("family_name"), + "Ppi family name from PPI", + ), + ( + lambda: models.Ppi.objects.get().name, + compute_ppi_name(entry_data), + "Ppi name", + ), + ] + for activity_tests in entry_data["activity_tests"]: + for compound_activity_results in activity_tests[ + "compound_activity_results" + ]: + if ( + "activity_mol" in compound_activity_results + and compound_activity_results["activity_mol"] != "" + ): + compound_activity_results["activity"] = -math.log10( + Decimal(str(compound_activity_results["activity_mol"])) + * Decimal(str(compound_activity_results["activity_unit"])) + ) + + self._process_contribution_wizard_without_sanity_check(entry_data) + + for fcn, results, msg in future_expected_equals: + self.assertEqual(fcn(), results, msg=msg) + post_validation_expected_equals = [ + ( + models.Compound.objects.validated().count, + models.Compound.objects.count(), + "Validated Compounds count should have increased", + ) + ] + contribution_to_be_validated = models.Contribution.objects.get(validated=False) + contribution_to_be_validated.validated = True + contribution_to_be_validated.save() + for fcn, results, msg in post_validation_expected_equals: + self.assertEqual(fcn(), results, msg=msg) + + def _process_contribution_wizard_without_sanity_check( + self, entry_data, error_expected_in_step=None + ): + """ + The contribution add "wizard" + returns a 200 + """ + wizard_data = self._generate_wizard_data(entry_data) + for step in wizard_data: + step_url = self.get_step_url(step.get("step-id")) + response = self.client.get(step_url) + # post form data + if step.get("form-data") is not None: + err_msg = ( + f"Response code not ok when getting form " + f"for {step.get('step-id')} at {step_url}" + ) + self.assertEqual(response.status_code, 200, err_msg) + form_data = { + step["step-id"] + "-" + param_name: value + for param_name, value in step.get("form-data").items() + } + form_data["ippi_wizard-current_step"] = step["step-id"] + response = self.client.post(step_url, form_data) + error_is_now = ( + error_expected_in_step == step.get("step-id") + and response.status_code != 302 + ) + if ( + response.status_code != 302 + and step.get("step-id") != "done" + or error_is_now + ): + file_path = self.write_in_tmp_file(response) + err_msg = ( + f"Response code not ok when getting form for " + f"{step.get('step-id')} at {step_url}. Server" + f" response is stored in {file_path}" + ) + if error_is_now: + self.assertEqual(response.status_code, 200, err_msg) + return + self.assertEqual(response.status_code, 302, err_msg) + # check redirect to next step (if there is one) + if step.get("next") is not None: + redirect_url = self.get_step_url(step.get("next")) + self.assertEqual( + response.url, + redirect_url, + f"wrong redirection URL after step {step['step-id']}", + ) + self.assertEqual( + response.url, + reverse("contribution-detail", kwargs={"contribution_pk": 1}), + f"wrong final URL, should be the contribution permanent URL", + ) + + def _generate_wizard_data(self, entry_data): + """ + Generate the wizard form data dynamically based on iPPI-DB entry data + """ + + def get_id_form(): + return { + "source": entry_data["source"], + "id_source": entry_data["id_source"], + } + + def get_bibliography_form(): + ret = { + "source": entry_data["source"], + "id_source": entry_data["id_source"], + "title": "Opportunistic+amoebae:+challenges" + "+in+prophylaxis+and+treatment.", + "journal_name": "Drug+resistance+updates+:+reviews+and+" + "commentaries+in+antimicrobial+and+anticancer" + "+chemotherapy", + "authors_list": "Schuster+FL,+Visvesvara+GS", + "biblio_year": "2004", + } + for k in [ + "cytotox", + "xray", + "in_silico", + "in_vitro", + "in_cellulo", + "in_vivo", + "pharmacokinetic", + ]: + if entry_data.get(k, False): + ret[k] = "on" + return ret + + def get_pdb_form(): + return {"pdb_id": entry_data["pdb_id"]} + + def get_complex_type_form(): + return { + "complexType": entry_data["complexType"], + "complexChoice": entry_data["complexChoice"], + } + + def get_complex_form(): + data = { + "TOTAL_FORMS": len(entry_data["complex"]), + "INITIAL_FORMS": len(entry_data["complex"]), + "MIN_NUM_FORMS": 0, + "MAX_NUM_FORMS": 1000, + } + for idx, entry_complex in enumerate(entry_data["complex"]): + data["{}-protein".format(idx)] = models.Protein.objects.get( + uniprot_id=entry_complex["uniprot_id"] + ).id + data["{}-complex_type".format(idx)] = entry_complex["complex_type"] + if entry_complex["domain_pfam_acc"] is not None: + try: + data["{}-domain".format(idx)] = models.Domain.objects.get( + pfam_acc=entry_complex["domain_pfam_acc"] + ).id + except models.Domain.DoesNotExist as dne: + print( + "No domain in DB for PFAM ACC {}".format( + entry_complex["domain_pfam_acc"] + ) + ) + raise dne + else: + data["{}-domain".format(idx)] = "" + data["{}-ppc_copy_nb".format(idx)] = entry_complex["ppc_copy_nb"] + data["{}-cc_nb".format(idx)] = entry_complex["cc_nb"] + data["{}-ppp_copy_nb_per_p".format(idx)] = entry_complex[ + "ppp_copy_nb_per_p" + ] + return data + + def get_ppi_form(): + return { + "family": "", + "pdb_id": entry_data["pdb_id"], + "family_name": entry_data["family_name"], + "symmetry": 1, # FIXME + "pockets_nb": 1, # FIXME + "selected_diseases": "\n".join(entry_data["diseases"]), + } + + def get_compound_form(): + data = { + "TOTAL_FORMS": len(entry_data["compounds"]), + # FIXME there should be a way to list more than one compound + "INITIAL_FORMS": len(entry_data["compounds"]), + "MIN_NUM_FORMS": 1, + "MAX_NUM_FORMS": 1000, + } + for idx, compound_data_item in enumerate(entry_data["compounds"]): + data[f"{idx}-common_name"] = (compound_data_item["common_name"],) + data[f"{idx}-compound_name"] = (compound_data_item["compound_name"],) + if "molecule_smiles" in compound_data_item: + data[f"{idx}-molecule_smiles"] = ( + compound_data_item["molecule_smiles"], + ) + elif "molecule_iupac" in compound_data_item: + # if a IUPAC is provided for tests assume convert it to SMILES + # molecule_smiles = convert_iupac_to_smiles( + # compound_data_item["molecule_iupac"] + # ) + data[f"{idx}-molecule_iupac"] = compound_data_item["molecule_iupac"] + data[f"{idx}-is_macrocycle"] = compound_data_item.get( + "is_macrocycle", False + ) + if "ligand_id" in compound_data_item: + data[f"{idx}-ligand_id"] = compound_data_item["ligand_id"] + return data + + def get_activity_description_form(): + data = { + "TOTAL_FORMS": len(entry_data.get("activity_tests", [])), + "INITIAL_FORMS": 0, + "MIN_NUM_FORMS": 0, + "MAX_NUM_FORMS": 1000, + } + for idx, activity_test in enumerate(entry_data.get("activity_tests", [])): + data[f"{idx}-ppi"] = "" + data[f"{idx}-test_name"] = activity_test["test_name"] + data[f"{idx}-is_primary"] = activity_test.get("is_primary", False) + data[f"{idx}-protein_bound_construct"] = activity_test[ + "protein_bound_construct" + ] + data[f"{idx}-test_type"] = activity_test["test_type"] + data[f"{idx}-test_modulation_type"] = activity_test[ + "test_modulation_type" + ] + data[f"{idx}-nb_active_compounds"] = activity_test[ + "nb_active_compounds" + ] + if "cell_line_name" in activity_test: + data[f"{idx}-cell_line_name"] = activity_test["cell_line_name"] + data[f"{idx}-protein_complex"] = activity_test["protein_complex"] + data[ + f"{idx}-compoundactivityresult_set-activity-results-TOTAL_FORMS" + ] = 1 # len(activity_test.get("compound_activity_results",[])) + data[ + f"{idx}-compoundactivityresult_set-activity-results-INITIAL_FORMS" + ] = 0 + data[ + f"{idx}-compoundactivityresult_set-activity-results-MIN_NUM_FORMS" + ] = 0 + data[ + f"{idx}-compoundactivityresult_set-activity-results-MAX_NUM_FORMS" + ] = 1000 + for nidx, compound_activity_result in enumerate( + activity_test["compound_activity_results"] + ): + data[ + f"{idx}-compoundactivityresult_set-activity-results-{nidx}-compound_name" + ] = compound_activity_result["compound_name"] + data[ + f"{idx}-compoundactivityresult_set-activity-results-{nidx}-activity_type" + ] = compound_activity_result["activity_type"] + try: + data[ + f"{idx}-compoundactivityresult_set-activity-results-{nidx}-activity_mol" + ] = compound_activity_result["activity_mol"] + data[ + f"{idx}-compoundactivityresult_set-activity-results-{nidx}-activity_unit" + ] = compound_activity_result["activity_unit"] + except KeyError: + pass + data[ + f"{idx}-compoundactivityresult_set-activity-results-{nidx}-modulation_type" + ] = compound_activity_result["modulation_type"] + # "id": "", + # "test_activity_description": "", + return data + + def get_cytotox_description_form(): + data = { + "TOTAL_FORMS": len(entry_data.get("cytotox_tests", [])), + "INITIAL_FORMS": 0, + "MIN_NUM_FORMS": 1, + "MAX_NUM_FORMS": 1000, + } + for idx, cytotox_test in enumerate(entry_data.get("cytotox_tests", [])): + data[f"{idx}-test_name"] = cytotox_test["test_name"] + data[f"{idx}-compound_concentration"] = cytotox_test[ + "compound_concentration" + ] + data[f"{idx}-cell_line_name"] = cytotox_test["cell_line_name"] + + data[ + f"{idx}-compoundcytotoxicityresult_set-cytotox-results-TOTAL_FORMS" + ] = len(cytotox_test.get("compound_cytotox_results", [])) + data[ + f"{idx}-compoundcytotoxicityresult_set-cytotox-results-INITIAL_FORMS" + ] = 0 + data[ + f"{idx}-compoundcytotoxicityresult_set-cytotox-results-MIN_NUM_FORMS" + ] = 0 + data[ + f"{idx}-compoundcytotoxicityresult_set-cytotox-results-MAX_NUM_FORMS" + ] = 1000 + for nidx, result in enumerate(cytotox_test["compound_cytotox_results"]): + data[ + f"{idx}-compoundcytotoxicityresult_set-cytotox-results-{nidx}-compound_name" + ] = result["compound_name"] + data[ + f"{idx}-compoundcytotoxicityresult_set-cytotox-results-{nidx}-toxicity" + ] = result["toxicity"] + return data + + def get_pk_description_form(): + data = { + "TOTAL_FORMS": len(entry_data.get("pharmacokinetic_tests", [])), + "INITIAL_FORMS": 0, + "MIN_NUM_FORMS": 1, + "MAX_NUM_FORMS": 1000, + } + for idx, pk_test in enumerate(entry_data.get("pharmacokinetic_tests", [])): + data[f"{idx}-test_name"] = pk_test["test_name"] + data[f"{idx}-organism"] = pk_test["organism"] + data[f"{idx}-administration_mode"] = pk_test["administration_mode"] + data[f"{idx}-concentration"] = pk_test["concentration"] + data[f"{idx}-dose"] = pk_test["dose"] + data[f"{idx}-dose_interval"] = pk_test["dose_interval"] + data[f"{idx}-cell_line_name"] = pk_test["cell_line_name"] + + data[f"{idx}-compoundpkresult_set-pk-results-TOTAL_FORMS"] = len( + pk_test.get("compound_pk_results", []) + ) + data[f"{idx}-compoundpkresult_set-pk-results-INITIAL_FORMS"] = 0 + data[f"{idx}-compoundpkresult_set-pk-results-MIN_NUM_FORMS"] = 0 + data[f"{idx}-compoundpkresult_set-pk-results-MAX_NUM_FORMS"] = 1000 + for nidx, result in enumerate(pk_test["compound_pk_results"]): + data[ + f"{idx}-compoundpkresult_set-pk-results-{nidx}-tolerated" + ] = result["tolerated"] + data[f"{idx}-compoundpkresult_set-pk-results-{nidx}-auc"] = result[ + "auc" + ] + data[ + f"{idx}-compoundpkresult_set-pk-results-{nidx}-clearance" + ] = result["clearance"] + data[ + f"{idx}-compoundpkresult_set-pk-results-{nidx}-c_max" + ] = result["c_max"] + data[ + f"{idx}-compoundpkresult_set-pk-results-{nidx}-oral_bioavailability" + ] = result["oral_bioavailability"] + data[ + f"{idx}-compoundpkresult_set-pk-results-{nidx}-t_demi" + ] = result["t_demi"] + data[ + f"{idx}-compoundpkresult_set-pk-results-{nidx}-t_max" + ] = result["t_max"] + data[ + f"{idx}-compoundpkresult_set-pk-results-{nidx}-voldistribution" + ] = result["voldistribution"] + data[ + f"{idx}-compoundpkresult_set-pk-results-{nidx}-compound_name" + ] = result["compound_name"] + return data + + form_callables = { + None: lambda: None, + "IdForm": get_id_form, + "BibliographyForm": get_bibliography_form, + "PDBForm": get_pdb_form, + "ProteinDomainComplexTypeForm": get_complex_type_form, + "ProteinDomainComplexForm": get_complex_form, + "PpiForm": get_ppi_form, + "CompoundForm": get_compound_form, + "ActivityDescriptionFormSet": get_activity_description_form, + } + if entry_data.get("cytotox", False): + form_callables[ + "TestCytotoxDescriptionFormSet" + ] = get_cytotox_description_form + if entry_data.get("pharmacokinetic", False): + form_callables["TestPKDescriptionFormSet"] = get_pk_description_form + + # Adding a step for "done" in order to also save the data in the db + form_callables["done"] = lambda: None + + form_ids = iter(list(form_callables.keys())[1:]) + for step in form_callables.keys(): + item = { + "step-id": step, + "form-data": form_callables[step](), + "next": next(form_ids, None), + } + yield item + + @parameterized.expand( + [ + entry_path + for entry_path in glob.glob( + os.path.join(os.path.dirname(__file__), "*.yaml") + ) + ] + ) + def test_entry(self, entry_path): + # load the test data + entry_data = yaml.load(open(entry_path, "r"), Loader=yaml.FullLoader) + # process the wizard + self._process_contribution_wizard(entry_data) + # test diseases + input_diseases = set(entry_data["diseases"]) + input_diseases.discard("") + self.assertSetEqual( + input_diseases, + set( + models.Ppi.objects.get(pdb_id=entry_data["pdb_id"]) + .diseases.annotate( + raw=Concat(Value(";"), "name", Value(";"), "identifier", Value(";")) + ) + .values_list("raw", flat=True) + ), + ) + # test compounds + for c in entry_data["compounds"]: + if "molecule_smiles" in c: + comp = models.Compound.objects.get(canonical_smile=c["molecule_smiles"]) + elif "molecule_iupac" in c: + comp = models.Compound.objects.get(iupac_name=c["molecule_iupac"]) + # test ligand ID has been stored if the X ray data option is checked + if entry_data.get("xray", False) is True: + self.assertEqual(comp.ligand_id, c.get("ligand_id", None)) + # test activity + bibliography = models.Bibliography.objects.get( + id_source=entry_data["id_source"].strip() + ) + for activity_test in entry_data["activity_tests"]: + tad_filters = { + "test_type": activity_test["test_type"], + "test_name": activity_test["test_name"], + "protein_bound_construct": activity_test["protein_bound_construct"], + "test_modulation_type": activity_test["test_modulation_type"], + "nb_active_compounds": activity_test["nb_active_compounds"], + } + if ( + "cell_line_name" in activity_test + and activity_test["cell_line_name"] != "" + ): + tad_filters["cell_line__name"] = activity_test["cell_line_name"] + for tad in models.TestActivityDescription.objects.all(): + print( + tad.test_type, + tad.test_name, + tad.protein_bound_construct, + tad.test_modulation_type, + tad.nb_active_compounds, + tad.cell_line, + ) + test_activity_description = models.TestActivityDescription.objects.get( + **tad_filters + ) + for compound_activity_results in activity_test["compound_activity_results"]: + compound = models.RefCompoundBiblio.objects.get( + bibliography=bibliography, + compound_name=compound_activity_results["compound_name"], + ).compound + results = models.CompoundActivityResult.objects.get( + activity_type=compound_activity_results["activity_type"], + modulation_type=compound_activity_results["modulation_type"], + compound=compound, + test_activity_description=test_activity_description, + ) + self.assertEqual( + results.activity_type, compound_activity_results["activity_type"] + ) + self.assertEqual( + results.modulation_type, + compound_activity_results["modulation_type"], + ) + self.assertEqual( + Decimal(compound_activity_results["activity"]).quantize( + Decimal(10) + ** -models.CompoundActivityResult._meta.get_field( + "activity" + ).decimal_places + ), + results.activity, + ) + + def write_in_tmp_file(self, response): + """ + Write an HTTP response contents into a temporary file for debug + + :param response: response that will be saved + :type respons: django.http.HttpResponse + :return: path to the temporary file created + :rtype: str + """ + with NamedTemporaryFile(delete=False, suffix=".html") as f: + f.write(response.content) + return f.name diff --git a/ippisite/ippidb/tests/tests_utils.py b/ippisite/ippidb/tests/tests_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..5c73f34e75dc2ebeb17c8b63d4eb9470b7da2a0b --- /dev/null +++ b/ippisite/ippidb/tests/tests_utils.py @@ -0,0 +1,143 @@ +""" +iPPI-DB unit tests +""" +import re + +from django.test import TestCase +from openbabel import vectorUnsignedInt, OBFingerprint + +from ippidb.utils import FingerPrinter, mol2smi, smi2mol, smi2inchi, smi2inchikey, smi2sdf + + +class MolSmiTestCase(TestCase): + """ + Test MOL to SMILES and SMILES to MOL format conversion functions + """ + + def setUp(self): + self.smiles = "C" + # the MOL version is also a valid regexp to validate arbitrary name in + # the openbabel-generated version + self.mol = ( + "\n OpenBabel[0-9]{11}D\n\n 1 0 0 0 0 0 0 0 " + " 0 0999 V2000\n 1.0000 0.0000 0.0000 C 0 0 0 0 " + " 0 0 0 0 0 0 0 0\nM END\n" + ) + + def test_mol2smi(self): + self.assertEqual(mol2smi(self.mol), self.smiles) + + def test_smi2mol2smi(self): + self.assertTrue(re.compile(self.mol).match(smi2mol(self.smiles))) + + +class SmiInchiTestCase(TestCase): + """ + Test INCHI and INCHIKEY generation functions + """ + + def setUp(self): + self.smiles_str = ( + "CC(C)C(=O)C1=C(C(=C(C(=C1)C(=O)C2=CC=C(C=C2)" "OC3=CC=CC=C3)O)O)O" + ) + self.inchi_str = ( + "InChI=1S/C23H20O6/c1-13(2)19(24)17-12-18(22" + "(27)23(28)21(17)26)20(25)14-8-10-16(11-9-14)29-15-6-4-3-" + "5-7-15/h3-13,26-28H,1-2H3" + ) + self.inchikey_str = "CVVQMBDTMYUWTR-UHFFFAOYSA-N" + + def test_smi2inchi(self): + self.assertEqual(smi2inchi(self.smiles_str), self.inchi_str) + + def test_smi2inchikey(self): + self.assertEqual(smi2inchikey(self.smiles_str), self.inchikey_str) + + +class FingerPrinterTestCase(TestCase): + """ + Test FingerPrinter class + """ + + def setUp(self): + self.fingerprinter = FingerPrinter("FP4") + self.smiles = "CC" + self.fp = vectorUnsignedInt([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + self.smiles_dict = {1: "CC", 2: "CCC"} + self.fp_dict = { + 1: vectorUnsignedInt([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + 2: vectorUnsignedInt([3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]), + } + self.tanimoto_dict = {1: 1.0, 2: 0.5} + + def test_fingerprints_available(self): + # test that all necessary fingerprints are available + self.assertIsNotNone(OBFingerprint.FindFingerprint("FP2")) + self.assertIsNotNone(OBFingerprint.FindFingerprint("FP4")) + self.assertIsNotNone(OBFingerprint.FindFingerprint("ECFP4")) + + def assertEqualVUI(self, vui_one, vui_two): + return self.assertEqual(list(vui_one), list(vui_two)) + + def assertEqualVUIdict(self, vuidict_one, vuidict_two): + vuidict_one = {id_: list(vui) for id_, vui in vuidict_one.items()} + vuidict_two = {id_: list(vui) for id_, vui in vuidict_two.items()} + return self.assertEqual(vuidict_one, vuidict_two) + + def test_fp(self): + self.assertEqualVUI(self.fingerprinter.fp(self.smiles), self.fp) + + def test_fp_dict(self): + self.assertEqualVUIdict( + self.fingerprinter.fp_dict(self.smiles_dict), self.fp_dict + ) + + def test_tanimoto_fps(self): + self.assertEqual( + self.fingerprinter.tanimoto_fps(self.smiles, self.fp_dict), + self.tanimoto_dict, + ) + + def test_tanimoto_smiles(self): + self.assertEqual( + self.fingerprinter.tanimoto_smiles(self.smiles, self.smiles_dict), + self.tanimoto_dict, + ) + + +class FingerPrinterTestCaseCompound1ECFP4(TestCase): + def setUp(self): + self.fingerprinter = FingerPrinter("ECFP4") + self.smiles = "CC(C)C(=O)c1cc(C(=O)c2ccc(Oc3ccccc3)cc2)c(O)c(O)c1O" + self.smiles_dict = {1: "CC(C)C(=O)c1cc(C(=O)c2ccc(Oc3ccccc3)cc2)c(O)" "c(O)c1O"} + self.tanimoto_dict = {1: 1.0} + + def test_tanimoto_smiles(self): + self.assertEqual( + self.fingerprinter.tanimoto_smiles(self.smiles, self.smiles_dict), + self.tanimoto_dict, + ) + + +class ConvertSMILESToSDFTestCase(TestCase): + """ + Test converting a smiles to SDF using openbabel + """ + + def setUp(self): + self.smiles = "C" + # the MOL version is also a valid regexp to validate arbitrary name in + # the openbabel-generated version + self.sdf = ( + r"1\n" + r" OpenBabel[0-9]{11}D\n" + r"\n" + r" 1 0 0 0 0 0 0 0 0 0999 V2000\n" + r" 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n" + r"M END\n" + r"\$\$\$\$\n" + ) + + def test_valid(self): + result = smi2sdf({1: self.smiles}) + self.assertTrue(re.compile(self.sdf).search(result)) diff --git a/ippisite/ippidb/tests/tests_views.py b/ippisite/ippidb/tests/tests_views.py new file mode 100644 index 0000000000000000000000000000000000000000..f228aaef42dd633cc91ead91a11bab1cd000832b --- /dev/null +++ b/ippisite/ippidb/tests/tests_views.py @@ -0,0 +1,39 @@ +""" +iPPI-DB views tests +""" +from django.test import TestCase +from django.urls import reverse +from parameterized import parameterized + +tested_urlpatterns = [ + "index", + "general", + "pharmacology", + "le_lle", + "physicochemistry", + "pca", + "compound_list", + # "redirect_compound_card", + # "compound_card", + "tutorials", + # "admin-session", + # "ippidb_step", + # "admin-session-add", + # "admin-session-view", + # "biblio-view", + # "ppi-view", + # "contribution-detail", + "admin-session-update", + # "mol2smi", + # "smi2mol", + # "smi2iupac", + # "iupac2smi", + # "getoutputjob", +] + + +class ViewTest(TestCase): + @parameterized.expand([urlpattern for urlpattern in tested_urlpatterns]) + def test_url(self, url_name): + response = self.client.get(reverse(url_name)) + self.assertEqual(response.status_code, 200) diff --git a/ippisite/ippidb/tests_contribute.py b/ippisite/ippidb/tests_contribute.py deleted file mode 100644 index cd123dda54d8d512d8a3e4838a42199bdc60101d..0000000000000000000000000000000000000000 --- a/ippisite/ippidb/tests_contribute.py +++ /dev/null @@ -1,1170 +0,0 @@ -""" -iPPI-DB contribution module tests -""" -import math -from decimal import Decimal -from tempfile import NamedTemporaryFile - -from django.contrib.auth import get_user_model -from django.contrib.auth.models import Permission -from django.contrib.contenttypes.models import ContentType -from django.db.models import Value -from django.db.models.functions import Concat -from django.test import TestCase -from django.urls import reverse - - -from ippidb import models -from ippidb.admin import grant_contribution_permission -from ippidb.forms import PDBForm -from ippidb.ws import convert_iupac_to_smiles, get_uniprot_info -from live_settings import live_settings - -import requests_cache - -requests_cache.install_cache("tests_contribute_cache", backend="sqlite") - - -class BibliographyIDTestCase(TestCase): - def test_long_doi(self): - b = models.Bibliography() - b.source = "DO" - b.id_source = "10.1016/j.bmcl.2013.03.013" - b.save(autofill=True) - - -class ContributionViewsAccessTestCase(TestCase): - def setUp(self): - self.url = reverse("admin-session-add") - - self.user = get_user_model().objects.create(username="user") - - self.user_granted = get_user_model().objects.create(username="user_granted") - content_type = ContentType.objects.get_for_model(models.Contribution) - permission = Permission.objects.get( - codename="add_contribution", content_type=content_type - ) - self.user_granted.user_permissions.add(permission) - - def test_force_auth(self): - response = self.client.get(self.url) - self.assertRedirects( - response, - # expected_url=reverse('login') + "?next=" + url - expected_url="/accounts/login/?next=%s" % self.url, - ) - - def test_403_without_perm_and_access_closed(self): - self.client.force_login(self.user) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) - - def test_ok_with_perm_and_access_closed(self): - live_settings.open_access_to_contribution = "False" - self.test_ok_with_perm_and_access_closed_by_default() - - def test_ok_with_perm_and_access_closed_by_default(self): - self.client.force_login(self.user_granted) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 302) - - def test_ok_without_perm_and_access_open(self): - live_settings.open_access_to_contribution = "True" - self.client.force_login(self.user) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 302) - - def test_ok_with_perm_and_access_open(self): - live_settings.open_access_to_contribution = "True" - self.client.force_login(self.user_granted) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 302) - - def test_ok_with_superuser(self): - self.user.is_superuser = True - self.user.save() - self.client.force_login(self.user) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 302) - - def test_ko_with_staff(self): - self.user.is_staff = True - self.user.save() - self.client.force_login(self.user) - response = self.client.get(self.url) - self.assertEqual(response.status_code, 403) - - -class PDBFormTestCase(TestCase): - """ - Test the PDB form data validation - """ - - def test_valid(self): - form = PDBForm({"pdb_id": "3wn7"}) - self.assertTrue(form.is_valid(), "valid PDB code should be accepted") - - def test_invalid_PDB_format(self): - form = PDBForm({"pdb_id": "33wn7"}) - self.assertFalse(form.is_valid(), "5 char long PDB code should be rejected") - self.assertTrue( - form.has_error("pdb_id", code="max_length"), - "5 char long PDB code should be rejected with max_length error code", - ) - - def test_invalid_PDB_entry(self): - form = PDBForm({"pdb_id": "9aa9"}) - self.assertFalse( - form.is_valid(), "inexistent PDB (9aa9) code should be rejected" - ) - self.assertTrue( - form.has_error("pdb_id", code="not_found"), - "inexistent PDB (9aa9) code should be rejected with not_found error code", - ) - - def test_invalid_nouniprot(self): - form = PDBForm({"pdb_id": "3wn8"}) - self.assertFalse( - form.is_valid(), "PDB code with no Uniprot mapping (3wn8) should be rejected" - ) - self.assertTrue( - form.has_error("pdb_id", code="no_mapping"), - "PDB code with no Uniprot mapping (3wn8) should be rejected with no_mappping error code", - ) - - -class ContributionViewsTestCase(TestCase): - """ - This class tests the Contribution Wizard by interacting with it - in the same way a user would do. - """ - - def setUp(self): - login = "contributor" - password = "12345" - User = get_user_model() - User.objects.create_user(username=login, password=password) - self.client.login(username=login, password=password) - grant_contribution_permission(None, None, User.objects.all()) - symmetry = models.Symmetry() - symmetry.code = "AS" - symmetry.description = "asymmetric" - symmetry.save() - models.Taxonomy.objects.create(taxonomy_id=9606, name="Homo sapiens") - - @staticmethod - def get_step_url(step_id): - step_url = reverse("admin-session-add") - if step_id is not None: - step_url += step_id + "/" - return step_url - - def _process_contribution_wizard(self, entry_data): - def compute_ppi_name(entry_data): - bound_uniprots = [ - item["uniprot_id"] - for item in entry_data["complex"] - if item["complex_type"] == "Bound" - ] - partner_uniprots = [ - item["uniprot_id"] - for item in entry_data["complex"] - if item["complex_type"] == "Partner" - ] - bound_protein_names = [ - get_uniprot_info(uniprot_id)["short_name"] - for uniprot_id in bound_uniprots - ] - partner_protein_names = [ - get_uniprot_info(uniprot_id)["short_name"] - for uniprot_id in partner_uniprots - ] - bound_protein_names.sort() - partner_protein_names.sort() - bound_str = ",".join(bound_protein_names) - partner_str = ",".join(partner_protein_names) - name = bound_str - if partner_str != "": - name += " / " + partner_str - return name - - future_expected_equals = [ - ( - models.Bibliography.objects.count, - models.Bibliography.objects.count() + 1, - "Bibliography count", - ), - ( - models.Contribution.objects.count, - models.Contribution.objects.count() + 1, - "Contribution count", - ), - ( - models.Compound.objects.count, - models.Compound.objects.count() + len(entry_data["compounds"]), - "Compounds count", - ), - ( - models.Compound.objects.validated().count, - models.Compound.objects.validated().count(), - "Validated Compounds count should remains the same", - ), - ( - models.CompoundAction.objects.count, - models.CompoundAction.objects.count() + len(entry_data["compounds"]), - "Compound Actions count", - ), - ( - models.TestActivityDescription.objects.count, - models.TestActivityDescription.objects.count() - + len(entry_data.get("activity_tests", [])), - "Activity tests count", - ), - ( - models.TestCytotoxDescription.objects.count, - models.TestCytotoxDescription.objects.count() - + len(entry_data.get("cytotox_tests", [])), - "Cytotox tests count", - ), - ( - models.TestPKDescription.objects.count, - models.TestPKDescription.objects.count() - + len(entry_data.get("pharmacokinetic_tests", [])), - "Pharmacokinetic tests count", - ), - ( - lambda: models.PpiFamily.objects.get().name, - entry_data.get("family_name"), - "Ppi family name", - ), - ( - lambda: models.Ppi.objects.get().family.name, - entry_data.get("family_name"), - "Ppi family name from PPI", - ), - ( - lambda: models.Ppi.objects.get().name, - compute_ppi_name(entry_data), - "Ppi name", - ), - ] - for activity_tests in entry_data["activity_tests"]: - for compound_activity_results in activity_tests[ - "compound_activity_results" - ]: - if ( - "activity_mol" in compound_activity_results - and compound_activity_results["activity_mol"] != "" - ): - compound_activity_results["activity"] = -math.log10( - Decimal(str(compound_activity_results["activity_mol"])) - * Decimal(str(compound_activity_results["activity_unit"])) - ) - - self._process_contribution_wizard_without_sanity_check(entry_data) - - for fcn, results, msg in future_expected_equals: - self.assertEqual(fcn(), results, msg=msg) - post_validation_expected_equals = [ - ( - models.Compound.objects.validated().count, - models.Compound.objects.count(), - "Validated Compounds count should have increased", - ) - ] - contribution_to_be_validated = models.Contribution.objects.get(validated=False) - contribution_to_be_validated.validated = True - contribution_to_be_validated.save() - for fcn, results, msg in post_validation_expected_equals: - self.assertEqual(fcn(), results, msg=msg) - - def _process_contribution_wizard_without_sanity_check( - self, entry_data, error_expected_in_step=None - ): - """ - The contribution add "wizard" - returns a 200 - """ - wizard_data = self._generate_wizard_data(entry_data) - for step in wizard_data: - # import json - # print(json.dumps(step, indent=4)) - step_url = self.get_step_url(step.get("step-id")) - response = self.client.get(step_url) - # post form data - if step.get("form-data") is not None: - err_msg = ( - f"Response code not ok when getting form " - f"for {step.get('step-id')} at {step_url}" - ) - self.assertEqual(response.status_code, 200, err_msg) - form_data = { - step["step-id"] + "-" + param_name: value - for param_name, value in step.get("form-data").items() - } - form_data["ippi_wizard-current_step"] = step["step-id"] - response = self.client.post(step_url, form_data) - error_is_now = ( - error_expected_in_step == step.get("step-id") - and response.status_code != 302 - ) - if ( - response.status_code != 302 - and step.get("step-id") != "done" - or error_is_now - ): - file_path = self.write_in_tmp_file(response) - err_msg = ( - f"Response code not ok when getting form for " - f"{step.get('step-id')} at {step_url}. Server" - f" response is stored in {file_path}" - ) - if error_is_now: - self.assertEqual(response.status_code, 200, err_msg) - return - self.assertEqual(response.status_code, 302, err_msg) - # check redirect to next step (if there is one) - if step.get("next") is not None: - redirect_url = self.get_step_url(step.get("next")) - self.assertEqual( - response.url, - redirect_url, - f"wrong redirection URL after step {step['step-id']}", - ) - self.assertEqual( - response.url, - reverse("contribution-detail", kwargs={"contribution_pk": 1}), - f"wrong final URL, should be the contribution permanent URL", - ) - - def _generate_wizard_data(self, entry_data): - """ - Generate the wizard form data dynamically based on iPPI-DB entry data - """ - - def get_id_form(): - return { - "source": entry_data["source"], - "id_source": entry_data["id_source"], - } - - def get_bibliography_form(): - ret = { - "source": entry_data["source"], - "id_source": entry_data["id_source"], - "title": "Opportunistic+amoebae:+challenges" - "+in+prophylaxis+and+treatment.", - "journal_name": "Drug+resistance+updates+:+reviews+and+" - "commentaries+in+antimicrobial+and+anticancer" - "+chemotherapy", - "authors_list": "Schuster+FL,+Visvesvara+GS", - "biblio_year": "2004", - } - for k in [ - "cytotox", - "xray", - "in_silico", - "in_vitro", - "in_cellulo", - "in_vivo", - "pharmacokinetic", - ]: - if entry_data.get(k, False): - ret[k] = "on" - return ret - - def get_pdb_form(): - return {"pdb_id": entry_data["pdb_id"]} - - def get_complex_type_form(): - return { - "complexType": entry_data["complexType"], - "complexChoice": entry_data["complexChoice"], - } - - def get_complex_form(): - data = { - "TOTAL_FORMS": len(entry_data["complex"]), - "INITIAL_FORMS": len(entry_data["complex"]), - "MIN_NUM_FORMS": 0, - "MAX_NUM_FORMS": 1000, - } - for idx, entry_complex in enumerate(entry_data["complex"]): - data["{}-protein".format(idx)] = models.Protein.objects.get( - uniprot_id=entry_complex["uniprot_id"] - ).id - data["{}-complex_type".format(idx)] = entry_complex["complex_type"] - if entry_complex["domain_pfam_acc"] is not None: - try: - data["{}-domain".format(idx)] = models.Domain.objects.get( - pfam_acc=entry_complex["domain_pfam_acc"] - ).id - except models.Domain.DoesNotExist as dne: - print( - "No domain in DB for PFAM ACC {}".format( - entry_complex["domain_pfam_acc"] - ) - ) - raise dne - else: - data["{}-domain".format(idx)] = "" - data["{}-ppc_copy_nb".format(idx)] = entry_complex["ppc_copy_nb"] - data["{}-cc_nb".format(idx)] = entry_complex["cc_nb"] - data["{}-ppp_copy_nb_per_p".format(idx)] = entry_complex[ - "ppp_copy_nb_per_p" - ] - return data - - def get_ppi_form(): - return { - "family": "", - "pdb_id": entry_data["pdb_id"], - "family_name": entry_data["family_name"], - "symmetry": 1, # FIXME - "pockets_nb": 1, # FIXME - "selected_diseases": "\n".join(entry_data["diseases"]), - } - - def get_compound_form(): - data = { - "TOTAL_FORMS": len(entry_data["compounds"]), - # FIXME there should be a way to list more than one compound - "INITIAL_FORMS": len(entry_data["compounds"]), - "MIN_NUM_FORMS": 1, - "MAX_NUM_FORMS": 1000, - } - for idx, compound_data_item in enumerate(entry_data["compounds"]): - data[f"{idx}-common_name"] = (compound_data_item["common_name"],) - data[f"{idx}-compound_name"] = (compound_data_item["compound_name"],) - if "molecule_smiles" in compound_data_item: - data[f"{idx}-molecule_smiles"] = ( - compound_data_item["molecule_smiles"], - ) - elif "molecule_iupac" in compound_data_item: - # if a IUPAC is provided for tests assume convert it to SMILES - molecule_smiles = convert_iupac_to_smiles( - compound_data_item["molecule_iupac"] - ) - data[f"{idx}-molecule_smiles"] = molecule_smiles - data[f"{idx}-is_macrocycle"] = compound_data_item.get( - "is_macrocycle", False - ) - return data - - def get_activity_description_form(): - data = { - "TOTAL_FORMS": len(entry_data.get("activity_tests", [])), - "INITIAL_FORMS": 0, - "MIN_NUM_FORMS": 0, - "MAX_NUM_FORMS": 1000, - } - for idx, activity_test in enumerate(entry_data.get("activity_tests", [])): - data[f"{idx}-ppi"] = "" - data[f"{idx}-test_name"] = activity_test["test_name"] - data[f"{idx}-is_primary"] = activity_test["is_primary"] - data[f"{idx}-protein_bound_construct"] = activity_test[ - "protein_bound_construct" - ] - data[f"{idx}-test_type"] = activity_test["test_type"] - data[f"{idx}-test_modulation_type"] = activity_test[ - "test_modulation_type" - ] - data[f"{idx}-nb_active_compounds"] = activity_test[ - "nb_active_compounds" - ] - data[f"{idx}-cell_line_name"] = activity_test["cell_line_name"] - data[f"{idx}-protein_complex"] = activity_test["protein_complex"] - data[ - f"{idx}-compoundactivityresult_set-activity-results-TOTAL_FORMS" - ] = 1 # len(activity_test.get("compound_activity_results",[])) - data[ - f"{idx}-compoundactivityresult_set-activity-results-INITIAL_FORMS" - ] = 0 - data[ - f"{idx}-compoundactivityresult_set-activity-results-MIN_NUM_FORMS" - ] = 0 - data[ - f"{idx}-compoundactivityresult_set-activity-results-MAX_NUM_FORMS" - ] = 1000 - for nidx, compound_activity_result in enumerate( - activity_test["compound_activity_results"] - ): - data[ - f"{idx}-compoundactivityresult_set-activity-results-{nidx}-compound_name" - ] = compound_activity_result["compound_name"] - data[ - f"{idx}-compoundactivityresult_set-activity-results-{nidx}-activity_type" - ] = compound_activity_result["activity_type"] - try: - data[ - f"{idx}-compoundactivityresult_set-activity-results-{nidx}-activity_mol" - ] = compound_activity_result["activity_mol"] - data[ - f"{idx}-compoundactivityresult_set-activity-results-{nidx}-activity_unit" - ] = compound_activity_result["activity_unit"] - except KeyError: - pass - data[ - f"{idx}-compoundactivityresult_set-activity-results-{nidx}-modulation_type" - ] = compound_activity_result["modulation_type"] - # "id": "", - # "test_activity_description": "", - return data - - def get_cytotox_description_form(): - data = { - "TOTAL_FORMS": len(entry_data.get("cytotox_tests", [])), - "INITIAL_FORMS": 0, - "MIN_NUM_FORMS": 1, - "MAX_NUM_FORMS": 1000, - } - for idx, cytotox_test in enumerate(entry_data.get("cytotox_tests", [])): - data[f"{idx}-test_name"] = cytotox_test["test_name"] - data[f"{idx}-compound_concentration"] = cytotox_test[ - "compound_concentration" - ] - data[f"{idx}-cell_line_name"] = cytotox_test["cell_line_name"] - - data[ - f"{idx}-compoundcytotoxicityresult_set-cytotox-results-TOTAL_FORMS" - ] = len(cytotox_test.get("compound_cytotox_results", [])) - data[ - f"{idx}-compoundcytotoxicityresult_set-cytotox-results-INITIAL_FORMS" - ] = 0 - data[ - f"{idx}-compoundcytotoxicityresult_set-cytotox-results-MIN_NUM_FORMS" - ] = 0 - data[ - f"{idx}-compoundcytotoxicityresult_set-cytotox-results-MAX_NUM_FORMS" - ] = 1000 - for nidx, result in enumerate(cytotox_test["compound_cytotox_results"]): - data[ - f"{idx}-compoundcytotoxicityresult_set-cytotox-results-{nidx}-compound_name" - ] = result["compound_name"] - data[ - f"{idx}-compoundcytotoxicityresult_set-cytotox-results-{nidx}-toxicity" - ] = result["toxicity"] - return data - - def get_pk_description_form(): - data = { - "TOTAL_FORMS": len(entry_data.get("pharmacokinetic_tests", [])), - "INITIAL_FORMS": 0, - "MIN_NUM_FORMS": 1, - "MAX_NUM_FORMS": 1000, - } - for idx, pk_test in enumerate(entry_data.get("pharmacokinetic_tests", [])): - data[f"{idx}-test_name"] = pk_test["test_name"] - data[f"{idx}-organism"] = pk_test["organism"] - data[f"{idx}-administration_mode"] = pk_test["administration_mode"] - data[f"{idx}-concentration"] = pk_test["concentration"] - data[f"{idx}-dose"] = pk_test["dose"] - data[f"{idx}-dose_interval"] = pk_test["dose_interval"] - data[f"{idx}-cell_line_name"] = pk_test["cell_line_name"] - - data[f"{idx}-compoundpkresult_set-pk-results-TOTAL_FORMS"] = len( - pk_test.get("compound_pk_results", []) - ) - data[f"{idx}-compoundpkresult_set-pk-results-INITIAL_FORMS"] = 0 - data[f"{idx}-compoundpkresult_set-pk-results-MIN_NUM_FORMS"] = 0 - data[f"{idx}-compoundpkresult_set-pk-results-MAX_NUM_FORMS"] = 1000 - for nidx, result in enumerate(pk_test["compound_pk_results"]): - data[ - f"{idx}-compoundpkresult_set-pk-results-{nidx}-tolerated" - ] = result["tolerated"] - data[f"{idx}-compoundpkresult_set-pk-results-{nidx}-auc"] = result[ - "auc" - ] - data[ - f"{idx}-compoundpkresult_set-pk-results-{nidx}-clearance" - ] = result["clearance"] - data[ - f"{idx}-compoundpkresult_set-pk-results-{nidx}-c_max" - ] = result["c_max"] - data[ - f"{idx}-compoundpkresult_set-pk-results-{nidx}-oral_bioavailability" - ] = result["oral_bioavailability"] - data[ - f"{idx}-compoundpkresult_set-pk-results-{nidx}-t_demi" - ] = result["t_demi"] - data[ - f"{idx}-compoundpkresult_set-pk-results-{nidx}-t_max" - ] = result["t_max"] - data[ - f"{idx}-compoundpkresult_set-pk-results-{nidx}-voldistribution" - ] = result["voldistribution"] - data[ - f"{idx}-compoundpkresult_set-pk-results-{nidx}-compound_name" - ] = result["compound_name"] - return data - - form_callables = { - None: lambda: None, - "IdForm": get_id_form, - "BibliographyForm": get_bibliography_form, - "PDBForm": get_pdb_form, - "ProteinDomainComplexTypeForm": get_complex_type_form, - "ProteinDomainComplexForm": get_complex_form, - "PpiForm": get_ppi_form, - "CompoundForm": get_compound_form, - "ActivityDescriptionFormSet": get_activity_description_form, - } - if entry_data.get("cytotox", False): - form_callables[ - "TestCytotoxDescriptionFormSet" - ] = get_cytotox_description_form - if entry_data.get("pharmacokinetic", False): - form_callables["TestPKDescriptionFormSet"] = get_pk_description_form - - # Adding a step for "done" in order to also save the data in the db - form_callables["done"] = lambda: None - - form_ids = iter(list(form_callables.keys())[1:]) - for step in form_callables.keys(): - item = { - "step-id": step, - "form-data": form_callables[step](), - "next": next(form_ids, None), - } - yield item - - def get_basic_entry(self): - """ - Basic entry test - """ - return { - "source": "PM", - "id_source": "15072770", - "in_silico": True, - "pdb_id": "3u85", - "diseases": [";toto;1;", ";titi;sfddf;", ";tata;3232;"], - "complexChoice": "inhibited", - "complexType": "Inhib_Hetero2merAB", - "complex": [ - { - "uniprot_id": "O00255", - "complex_type": "Partner", - "domain_pfam_acc": "PF05053", - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": "", - }, - { - "uniprot_id": "Q03164", - "complex_type": "Bound", - "domain_pfam_acc": "PF05965", - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": 1, - }, - ], - "family_name": "Menin (PF05053)", - "compounds": [ - { - "molecule_smiles": "CCC", - "compound_name": "toto", - "common_name": "super", - } - ], - "activity_tests": [ - { - "test_name": "test", - "is_primary": True, - "protein_bound_construct": "F", - "test_type": "BIOCH", - "test_modulation_type": "I", - "nb_active_compounds": 2, - "cell_line_name": "", - # means first in the list above - "protein_complex": "2", - "compound_activity_results": [ - { - "compound_name": "toto", - "activity_type": "pIC50", - "activity_mol": 6.85, - "activity_unit": "1e-6", - "modulation_type": "I", - } - ], - } - ], - } - - def test_basic_entry(self): - self._process_contribution_wizard(self.get_basic_entry()) - - def get_entry_28(self): - """ - Test for the "contribution #28" - """ - return { - "source": "PM", - "id_source": "24512214 ", - "in_vitro": True, - "pdb_id": "3wn7", - "diseases": ["cancer (MONDO:0004992)"], - # FIXME continue here - "complexChoice": "inhibited", - "complexType": "Inhib_Hetero2merAB", - "complex": [ - { - "uniprot_id": "Q60795", - "complex_type": "Partner", - "domain_pfam_acc": None, - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": 1, - }, - { - "uniprot_id": "Q9Z2X8", - "complex_type": "Bound", - "domain_pfam_acc": "PF01344", - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": 1, - }, - ], - "family_name": "KEAP1 / NRF2", - "compounds": [ - { - "molecule_smiles": "c1cc2c(cc1)c(ccc2N(S(=O)(=O)c1ccc(cc1)" - "OC)CC(=O)O)N(S(=O)(=O)c1ccc(cc1)OC)CC(=O)O", - "compound_name": "2", - "common_name": "", - } - ], - "activity_tests": [ - { - "test_name": "biolayer interferometry assay", - "is_primary": True, - "protein_bound_construct": "F", - "test_type": "BIOCH", - "test_modulation_type": "B", - "nb_active_compounds": 1, - "cell_line_name": "", - # means first in the list above - "protein_complex": "1", - "compound_activity_results": [ - { - "compound_name": "2", - "activity_type": "pKd", - "activity_mol": 3.59, - "activity_unit": "1e-9", - "modulation_type": "I", - } - ], - } - ], - } - - def test_entry_28(self): - self._process_contribution_wizard(self.get_entry_28()) - - def _test_activity_computation_and_storage(self, value, power): - basic_entry = self.get_basic_entry() - for activity_tests in basic_entry["activity_tests"]: - for compound_activity_results in activity_tests[ - "compound_activity_results" - ]: - compound_activity_results["activity_unit"] = "1e-%i" % power - try: - del compound_activity_results["activity"] - except KeyError: - pass - self._process_contribution_wizard(basic_entry) - bibliography = models.Bibliography.objects.get( - id_source=basic_entry["id_source"] - ) - for activity_tests in basic_entry["activity_tests"]: - for compound_activity_results in activity_tests[ - "compound_activity_results" - ]: - compound = models.RefCompoundBiblio.objects.get( - bibliography=bibliography, - compound_name=compound_activity_results["compound_name"], - ).compound - results = models.CompoundActivityResult.objects.get( - activity_type=compound_activity_results["activity_type"], - modulation_type=compound_activity_results["modulation_type"], - compound=compound, - ) - self.assertEqual( - Decimal(compound_activity_results["activity"]).quantize( - Decimal(10) - ** -models.CompoundActivityResult._meta.get_field( - "activity" - ).decimal_places - ), - results.activity, - ) - - def test_activity_computation_and_storage(self): - self._test_activity_computation_and_storage(0.685, 0) - - def test_activity_computation_and_storage_3(self): - self._test_activity_computation_and_storage(639.39406, 3) - - def test_activity_computation_and_storage_6(self): - self._test_activity_computation_and_storage(4.9846073, 6) - - def test_activity_computation_and_storage_9(self): - self._test_activity_computation_and_storage(2380.002, 9) - - def test_activity_computation_and_storage_12(self): - self._test_activity_computation_and_storage(1.018, 12) - - def test_no_activity_fails(self): - basic_entry = self.get_basic_entry() - for activity_tests in basic_entry["activity_tests"]: - for compound_activity_results in activity_tests[ - "compound_activity_results" - ]: - compound_activity_results["activity_unit"] = "" - compound_activity_results["activity_mol"] = "" - try: - del compound_activity_results["activity"] - except KeyError: - pass - self._process_contribution_wizard_without_sanity_check( - basic_entry, error_expected_in_step="ActivityDescriptionFormSet", - ) - - def test_with_all_tests(self): - """ - Basic entry test - """ - entry_data = { - "source": "PM", - "id_source": "15072770", - "in_silico": True, - "pharmacokinetic": True, - "cytotox": True, - "pdb_id": "3u85", - "diseases": ["", ";titi;sfddf;", ";tutu;5456456;", ";tutu;5456456;", ""], - "complexChoice": "inhibited", - "complexType": "Inhib_Hetero2merAB", - "complex": [ - { - "uniprot_id": "O00255", - "complex_type": "Partner", - "domain_pfam_acc": "PF05053", - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": "", - }, - { - "uniprot_id": "Q03164", - "complex_type": "Bound", - "domain_pfam_acc": "PF05965", - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": 1, - }, - ], - "family_name": "Menin (PF05053)", - "compounds": [ - { - "molecule_smiles": "CCC", - "compound_name": "toto", - "common_name": "super", - } - ], - "activity_tests": [ - { - "test_name": "test", - "is_primary": True, - "protein_bound_construct": "F", - "test_type": "BIOCH", - "test_modulation_type": "I", - "nb_active_compounds": 2, - "cell_line_name": "", - # means first in the list above - "protein_complex": "2", - "compound_activity_results": [ - { - "compound_name": "toto", - "activity_type": "pIC50", - "activity_mol": 6.85, - "activity_unit": "1e-3", - "modulation_type": "I", - } - ], - } - ], - "pharmacokinetic_tests": [ - { - "test_name": "test", - "organism": 1, - "administration_mode": "IV", - "concentration": "1.2", - "dose": "3.5", - "dose_interval": "2", - "cell_line_name": "1043SK ", - # means position in the list above - "compound_pk_results": [ - { - "tolerated": "True", - "auc": "1", - "clearance": "1.2", - "c_max": "5.2", - "oral_bioavailability": "22", - "t_demi": "5", - "t_max": "5", - "voldistribution": "5.5", - "compound_name": "toto", - } - ], - } - ], - "cytotox_tests": [ - { - "test_name": "test", - "compound_concentration": "2", - "cell_line_name": "1043SK ", - "compound_cytotox_results": [ - {"compound_name": "toto", "toxicity": "True"} - ], - } - ], - } - self._process_contribution_wizard(entry_data) - input_diseases = set(entry_data["diseases"]) - input_diseases.remove("") - self.assertSetEqual( - input_diseases, - set( - models.Ppi.objects.get(pdb_id=entry_data["pdb_id"]) - .diseases.annotate( - raw=Concat(Value(";"), "name", Value(";"), "identifier", Value(";")) - ) - .values_list("raw", flat=True) - ), - ) - - def test_complex_no_pfam(self): - """ - Test that it is possible to not select any domain - for one of the partners in the complex - (see #77) - """ - entry_data = { - "source": "PM", - "id_source": "26958703", - "in_silico": True, - "pdb_id": "3emh", - "diseases": [], - "complexChoice": "inhibited", - "complexType": "Inhib_Hetero2merAB", - "complex": [ - { - "uniprot_id": "P61964", - "complex_type": "Bound", - "domain_pfam_acc": "PF00400", - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": 1, - }, - { - "uniprot_id": "Q03164", - "complex_type": "Partner", - "domain_pfam_acc": None, - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": "", - }, - ], - "family_name": "WD40 (PF00400)", - "compounds": [ - { - "molecule_smiles": "C", - "compound_name": "toto", - "common_name": "super", - } - ], - "activity_tests": [ - { - "test_name": "test", - "is_primary": True, - "protein_bound_construct": "F", - "test_type": "BIOCH", - "test_modulation_type": "I", - "nb_active_compounds": 2, - "cell_line_name": "", - # means position in the list above - "protein_complex": "1", - "compound_activity_results": [ - { - "compound_name": "toto", - "activity_type": "pIC50", - "activity_mol": 6.85, - "activity_unit": "1e-3", - "modulation_type": "I", - } - ], - } - ], - } - self._process_contribution_wizard(entry_data) - - def test_simple_heterodimer(self): - """ - Test provided for simple heterodimer - """ - entry_data = { - "source": "PM", - "id_source": "26958703", - "in_silico": True, - "in_vitro": True, - "xray": True, - "pdb_id": "3emh", - "diseases": [], - "complexChoice": "inhibited", - "complexType": "Inhib_Hetero2merAB", - "complex": [ - { - "uniprot_id": "P61964", - "complex_type": "Bound", - "domain_pfam_acc": "PF00400", - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": 1, - }, - { - "uniprot_id": "Q03164", - "complex_type": "Partner", - "domain_pfam_acc": None, - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": "", - }, - ], - "family_name": "WD40 (PF00400)", - "compounds": [ - { - "molecule_smiles": r"CN1CCN(CC1)C1=C(C=C(C=C1)C1=CC(=CC=C1)" - r"CN1CCOCC1)NC(=O)C1=CNC(C=C1C(F)(F)F)=O", - "compound_name": "16d", - "common_name": "officially 16d", - "ligand_id": "ABC", - }, - { - "molecule_iupac": "N-(2-(4-Methylpiperazin-1-yl)-5-nitrophenyl)" - "-6-oxo-4-(trifluoromethyl)-1,6-dihydropyridine-3-carboxamide", - "compound_name": "14a", - "common_name": "officially 14a", - "is_macrocycle": False, - }, - { - "molecule_smiles": "C1CCC1", - "compound_name": "lili", - "common_name": "stuck", - "is_macrocycle": False, - }, - ], - "activity_tests": [ - { - "test_name": "test", - "is_primary": True, - "protein_bound_construct": "F", - "test_type": "BIOCH", - "test_modulation_type": "I", - "nb_active_compounds": 2, - "cell_line_name": "", - # means position in the list above - "protein_complex": "1", - "compound_activity_results": [ - { - "compound_name": "16d", - "activity_type": "pIC50", - "activity_mol": 6.85, - "activity_unit": "1e-3", - "modulation_type": "I", - } - ], - } - ], - } - - self._process_contribution_wizard(entry_data) - - def test_simple_stabilized_heterodimer(self): - """ - Test provided for stabilized heterodimer - """ - entry_data = { - "source": "PM", - "id_source": "23676274", - "in_cellulo": True, - "in_vitro": True, - "xray": True, - "pdb_id": "4jc3", - "complexChoice": "stabilized", - "complexType": "Stab_Hetero2merAB", - "complex": [ - { - "uniprot_id": "P31947", - "complex_type": "Bound", - "domain_pfam_acc": "PF00244", - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": 1, - }, - { - "uniprot_id": "P03372", - "complex_type": "Bound", - "domain_pfam_acc": None, - "ppc_copy_nb": 1, - "cc_nb": 1, - "ppp_copy_nb_per_p": 1, - }, - ], - "family_name": "14-3-3 / ER", - "diseases": ["breast cancer, MONDO:0007254"], - "compounds": [ - { - "molecule_smiles": r"COC[C@H]1CC[C@H]2[C@@H](C)[C@@H](O)" - r"[C@H](O[C@H]3O[C@H](COC(C)(C)C=C)[C@@H](O)[C@H](OC(C)=O)" - r"[C@H]3O)C3=C(C[C@H](O)[C@]3(C)\C=C1/2)[C@H](C)COC(C)=O", - "compound_name": "FC", - "common_name": "fusicoccin", - "ligand_id": "FSC", - } - ], - "activity_tests": [ - { - "test_name": "pull down", - "is_primary": True, - "protein_bound_construct": "F", - "test_type": "BIOCH", - "test_modulation_type": "I", - "nb_active_compounds": 2, - "cell_line_name": "", - # means position in the list above - "protein_complex": "1", - "compound_activity_results": [ - { - "compound_name": "FC", - "activity_type": "pIC50", - "activity_mol": 0.25, - "activity_unit": "1e-6", - "modulation_type": "S", - } - ], - } - ], - } - - self._process_contribution_wizard(entry_data) - - def write_in_tmp_file(self, response): - """ - Write an HTTP response contents into a temporary file for debug - - :param response: response that will be saved - :type respons: django.http.HttpResponse - :return: path to the temporary file created - :rtype: str - """ - with NamedTemporaryFile(delete=False, suffix=".html") as f: - f.write(response.content) - return f.name diff --git a/ippisite/ippidb/urls.py b/ippisite/ippidb/urls.py index ce56492d0ea9c17baaaf44322c6278b2ea0099b9..7946abc0b36c64aa1ae44a16b3b06f18a23ecf25 100644 --- a/ippisite/ippidb/urls.py +++ b/ippisite/ippidb/urls.py @@ -64,9 +64,7 @@ urlpatterns = [ url(r"^utils/smi2mol$", views.convert_smi2mol, name="smi2mol"), url(r"^utils/smi2iupac$", views.convert_smi2iupac, name="smi2iupac"), url(r"^utils/iupac2smi$", views.convert_iupac2smi, name="iupac2smi"), - url(r"^credits", views.credits, name="credits"), - url(r"^citation", views.citation, name="citation"), - url(r"^news", views.news, name="news"), + url(r"^utils/getoutputjob$", views.get_output_job, name="getoutputjob"), ] if settings.DEBUG: diff --git a/ippisite/ippidb/views/__init__.py b/ippisite/ippidb/views/__init__.py index dd6b454cb1e886bf07b3fb292b39409d15261f7f..af0cb27ee9b02c64288941267454a1f27054cc35 100644 --- a/ippisite/ippidb/views/__init__.py +++ b/ippisite/ippidb/views/__init__.py @@ -21,6 +21,7 @@ from .compound_query import ( convert_smi2iupac, convert_iupac2smi, ) +from .tasks import get_output_job from .about import ( about_general, about_le_lle, @@ -43,18 +44,6 @@ def tutorials(request): return render(request, "tutorials.html") -def credits(request): - return render(request, "credits.html") - - -def citation(request): - return render(request, "citation.html") - - -def news(request): - return render(request, "news.html") - - @login_required def adminSession(request): return render(request, "admin-session.html") @@ -68,6 +57,14 @@ def marvinjs(request): return {"marvinjs_apikey": settings.MARVINJS_APIKEY} +def google_analytics(request): + if hasattr(settings, "GA_CODE"): + ga_code = settings.GA_CODE + else: + ga_code = None + return {"gacode": ga_code} + + __all__ = [ ippidb_wizard_view, admin_session_view, @@ -89,4 +86,5 @@ __all__ = [ about_pca, about_pharmacology, about_physicochemistry, + get_output_job, ] diff --git a/ippisite/ippidb/views/about.py b/ippisite/ippidb/views/about.py index 6833ee73bdf10931d474022acaa39ef33917e01c..726f1e0a1f068bae1e817dea77e49ba4cdf2d1ad 100644 --- a/ippisite/ippidb/views/about.py +++ b/ippisite/ippidb/views/about.py @@ -149,7 +149,10 @@ def about_pharmacology(request): def about_le_lle(request): - context = {"le_lle_biplot_data": LeLleBiplotData.objects.get().le_lle_biplot_data} + try: + context = {"le_lle_biplot_data": LeLleBiplotData.objects.get().le_lle_biplot_data} + except LeLleBiplotData.DoesNotExist: + context = {} return render(request, "about-le-lle.html", context=context) @@ -249,7 +252,10 @@ def about_physicochemistry(request): def about_pca(request): - context = {"pca_biplot_data": PcaBiplotData.objects.get().pca_biplot_data} - pca_biplot = json.loads(PcaBiplotData.objects.get().pca_biplot_data) - context["pca_biplot_cc"] = pca_biplot["correlation_circle"] + try: + context = {"pca_biplot_data": PcaBiplotData.objects.get().pca_biplot_data} + pca_biplot = json.loads(PcaBiplotData.objects.get().pca_biplot_data) + context["pca_biplot_cc"] = pca_biplot["correlation_circle"] + except PcaBiplotData.DoesNotExist: + context = {} return render(request, "about-pca.html", context=context) diff --git a/ippisite/ippidb/views/compound_query.py b/ippisite/ippidb/views/compound_query.py index d5deb70bfb7819c285e64260b909517f2026aff3..1ea45e79385133f29f6f91ba51a59c955de18e19 100644 --- a/ippisite/ippidb/views/compound_query.py +++ b/ippisite/ippidb/views/compound_query.py @@ -463,6 +463,7 @@ class CompoundListView(ListView): "lle", "best_activity", "tests_av", + "families", ] def get(self, request, *args, **kwargs): @@ -502,6 +503,8 @@ class CompoundListView(ListView): for sort_by_option_id in self.sort_by_option_ids: if sort_by_option_id == "pubs": name = "Number of publications" + elif sort_by_option_id == "families": + name = "PPI families" elif sort_by_option_id == "le": name = "Ligand Efficiency" elif sort_by_option_id == "lle": diff --git a/ippisite/ippidb/views/tasks.py b/ippisite/ippidb/views/tasks.py new file mode 100644 index 0000000000000000000000000000000000000000..676851c8d37e8250ca64d912e20a473c47be9305 --- /dev/null +++ b/ippisite/ippidb/views/tasks.py @@ -0,0 +1,27 @@ +from django.http import JsonResponse, HttpResponseBadRequest +from ippidb.models import Job + + +def get_output_job(request): + """ + get a task information + """ + + task_id = request.GET.get("task_id") + if task_id in [None, ""]: + return HttpResponseBadRequest("task_id parameter is mandatory") + try: + job = Job.objects.get(task_result__task_id=task_id) + resp = { + "task_name": job.task_result.task_name, + "task_id": job.task_result.task_id, + "status": job.task_result.status, + "create_time": job.task_result.date_created, + "complete_time": job.task_result.date_done, + "std_out": job.std_out, + "std_err": job.std_err, + "traceback": job.task_result.traceback, + } + return JsonResponse(resp) + except Job.DoesNotExist: + return HttpResponseBadRequest("task with id {} doesn't exist".format(task_id)) diff --git a/ippisite/ippidb/ws.py b/ippisite/ippidb/ws.py index 10332f73938de1dc73d07aefa7eadedef2fcdf6d..190c4ef13c30151ffb7e1f11fc3a67570ede063c 100644 --- a/ippisite/ippidb/ws.py +++ b/ippisite/ippidb/ws.py @@ -77,85 +77,6 @@ def get_pubmed_info(pmid): } -def get_epo_info(patent_number): - """ - Retrieve information about a patent using the EPO website - - WARNING: this is not to be used anymore, the 3.1 version of the EPO service is now - offline - - :param patent_number: patent number - :type patent_number: str - :return: patent metadata (title, journal name, publication year, authors list). - :rtype: dict - """ - resp = requests.get( - f"http://ops.epo.org/3.1/rest-services/published-data/publication/docdb/" - f"{patent_number}/biblio.json" - ) - data = resp.json() - exchange_doc = data["ops:world-patent-data"]["exchange-documents"][ - "exchange-document" - ] - if isinstance(exchange_doc, list): - exchange_doc = exchange_doc[0] - title = [ - el["$"] - for el in exchange_doc["bibliographic-data"]["invention-title"] - if el["@lang"] == "en" - ][0] - authors = [ - i["inventor-name"]["name"]["$"] - for i in exchange_doc["bibliographic-data"]["parties"]["inventors"]["inventor"] - if i["@data-format"] == "original" - ][0] - biblio_year = [ - el["date"]["$"][:4] - for el in exchange_doc["bibliographic-data"]["publication-reference"][ - "document-id" - ] - if el["@document-id-type"] == "epodoc" - ][0] - return { - "title": title, - "journal_name": None, - "biblio_year": biblio_year, - "authors_list": authors, - } - - -def get_google_patent_info_ris(patent_number): - """ - Retrieve information about a patent using the Google RIS web service - - WARNING: this is not to be used anymore, this Google Web service is now offline - - :param patent_number: patent number - :type patent_number: str - :return: patent metadata (title, journal name, publication year, authors list). - :rtype: dict - """ - url = "https://encrypted.google.com/patents/{}.ris".format(patent_number) - resp = requests.get(url) - title = None - authors = [] - biblio_year = None - for line_str in resp.text.split("\n"): - line = line_str.strip().split(" - ") - if line[0] == "A1": - authors.append(line[1]) - elif line[0] == "T1": - title = line[1] - elif line[0] == "Y1": - biblio_year = line[1].split("/")[0] - return { - "title": title, - "journal_name": None, - "biblio_year": biblio_year, - "authors_list": authors, - } - - def get_google_patent_info(patent_number): """ Retrieve information about a patent parsing Dublin Core info in the Google HTML diff --git a/ippisite/ippisite/admin.py b/ippisite/ippisite/admin.py index 0cc216a8a5d375264d1a95b49c0f9da338ab7948..da708f9c9a83053515d11934051eed00e98f83ec 100644 --- a/ippisite/ippisite/admin.py +++ b/ippisite/ippisite/admin.py @@ -2,11 +2,11 @@ from django.contrib import admin from django.urls import path from django.shortcuts import redirect from django.contrib import messages - from ippidb.tasks import ( - launch_compound_properties_caching, - launch_drugbank_similarity_computing, + launch_update_compound_cached_properties, + run_compute_drugbank_similarity, launch_plots_computing, + launch_test_command_caching, ) @@ -35,15 +35,25 @@ class IppidbAdmin(admin.AdminSite): "launch_plots_computing/", self.admin_view(self.launch_plots_computing_view), ), + path("launch_test_command/", self.admin_view(self.launch_test_command),), ] return my_urls + urls + def launch_test_command(self, request): + """ + This view launches the task to test jobs + """ + task = launch_test_command_caching.delay() + print(task.state) + messages.add_message(request, messages.INFO, "Test job launched") + return redirect("/admin/") + def launch_compound_properties_caching_view(self, request): """ This view launches the task to perform, for all already validated compounds, the caching of the properties. """ - launch_compound_properties_caching.delay() + launch_update_compound_cached_properties() messages.add_message( request, messages.INFO, "Compound properties caching launched" ) @@ -54,7 +64,7 @@ class IppidbAdmin(admin.AdminSite): This view launches the task to perform, for all already validated compounds, the computing of drugbank similarity. """ - launch_drugbank_similarity_computing.delay() + run_compute_drugbank_similarity.delay() messages.add_message( request, messages.INFO, "DrugBank similarity computing launched" ) @@ -64,6 +74,6 @@ class IppidbAdmin(admin.AdminSite): """ This view launches the task to perform the computing of LE-LLE and PCA plots. """ - launch_plots_computing.delay() + launch_plots_computing() messages.add_message(request, messages.INFO, "Plots computing launched") return redirect("/admin/") diff --git a/ippisite/ippisite/decorator.py b/ippisite/ippisite/decorator.py new file mode 100644 index 0000000000000000000000000000000000000000..a66e1d0017e27fc177d0766a792dea92491b9f79 --- /dev/null +++ b/ippisite/ippisite/decorator.py @@ -0,0 +1,59 @@ +import traceback + +from celery import Task, states +from django_celery_results.models import TaskResult + +from ippidb.models import Job + + +class AlreadyExistError(Exception): + pass + + +class MonitorTask(Task): + """ + Ippidb custom task + """ + + def __call__(self, *args, **kwargs): + """In celery task this function call the run method, here you can + set some environment variable before the run of the task""" + + tasks = TaskResult.objects.filter( + task_name=self.request.task, + status__in=[states.STARTED, states.PENDING, states.RETRY, states.RECEIVED], + ) + count_tasks = tasks.count() + if not count_tasks: + self.update_state(state=states.PENDING) + return self.run(*args, **kwargs) + else: + message_exc = "Job {} in state {} already exist".format( + tasks[0].task_name, tasks[0].status + ) + raise AlreadyExistError(message_exc) + + def write(self, std_out=None, std_err=None): + job = Job.objects.get(task_result__task_id=self.task_id) + if std_out: + job.update_output(std_out, output="std_out") + elif std_err: + job.update_output(std_err, output="std_err") + + def on_success(self, retval, task_id, args, kwargs): + self.write(std_out="SUCCESS") + super(MonitorTask, self).on_success(retval, task_id, args, kwargs) + + def on_failure(self, exc, task_id, args, kwargs, einfo): + self.write(std_err="".join(traceback.format_tb(einfo.tb))) + super(MonitorTask, self).on_failure(exc, task_id, args, kwargs, einfo) + + def update_state(self, task_id=None, state=None, meta=None, **kwargs): + self.state = state + if task_id is None: + self.task_id = self.request.id + else: + self.task_id = task_id + super(MonitorTask, self).update_state( + task_id=task_id, state=state, meta=meta, **kwargs + ) diff --git a/ippisite/ippisite/settings.py b/ippisite/ippisite/settings.py index e6d355db3a13129f94d7edf4fcf63256031ac491..4756dbcbf554c7a04bce04ed64763e5ea5671574 100644 --- a/ippisite/ippisite/settings.py +++ b/ippisite/ippisite/settings.py @@ -43,6 +43,7 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "django_extensions", + "django_celery_results", "crispy_forms", "live_settings", "ippidb", @@ -84,6 +85,7 @@ TEMPLATES = [ "django.contrib.auth.context_processors.auth", "django.contrib.messages.context_processors.messages", "ippidb.views.marvinjs", + "ippidb.views.google_analytics", "live_settings.context_processors.processors", ] }, @@ -182,3 +184,8 @@ MARVINJS_APIKEY = None GALAXY_BASE_URL = None GALAXY_APIKEY = None GALAXY_COMPOUNDPROPERTIES_WORKFLOWID = None + +# celery setting. +CELERY_RESULT_BACKEND = "django-db" + +GA_CODE = None diff --git a/ippisite/ippisite/settings.template.py b/ippisite/ippisite/settings.template.py index b96e3bde7238b8f9593c22f25dc7904dea7f7b5d..f971d198766ae090eaa5165b86ed3cbde0808f02 100644 --- a/ippisite/ippisite/settings.template.py +++ b/ippisite/ippisite/settings.template.py @@ -45,6 +45,7 @@ INSTALLED_APPS = [ "django.contrib.messages", "django.contrib.staticfiles", "django_extensions", + "django_celery_results", "crispy_forms", "live_settings", "ippidb", @@ -161,3 +162,6 @@ LOGOUT_REDIRECT_URL = "/" # django-crispy-forms ################################################################################ CRISPY_TEMPLATE_PACK = "bootstrap4" + +# celery setting. +CELERY_RESULT_BACKEND = 'django-db' diff --git a/ippisite/requirements-core.txt b/ippisite/requirements-core.txt index d383d7ab590fd4fdf11a62ce8cd9a6cca9432eba..c071c907d2a8828c52aff66ae856f6b7f7958f13 100644 --- a/ippisite/requirements-core.txt +++ b/ippisite/requirements-core.txt @@ -6,6 +6,7 @@ django-extensions django-formtools django-debug-toolbar django-allauth +django-celery-results==1.2.1 # import scripts pandas==0.25.0 openpyxl diff --git a/ippisite/requirements-dev.txt b/ippisite/requirements-dev.txt index c3cf6a52096ef6795aefab61a80765e7ed4e25b1..c8aa1e20adb53df4bf0f96e67253ded5ce33ce92 100644 --- a/ippisite/requirements-dev.txt +++ b/ippisite/requirements-dev.txt @@ -10,6 +10,7 @@ sphinx_rtd_theme coverage # tests requests-cache +parameterized # dependencies to generate graph models using django-extensions pygraphviz pydot diff --git a/ippisite/templates/admin/index.html b/ippisite/templates/admin/index.html index 212e9632b58eaaa9a3c089fa4615f9d2924ba30e..d147b65995fe69eebececa685c1c9d0e2edd2adc 100644 --- a/ippisite/templates/admin/index.html +++ b/ippisite/templates/admin/index.html @@ -59,6 +59,12 @@ <input type="submit" value="Plots generation" name="_save"/> </form> <hr/> + <form method="POST" action="/admin/launch_test_command/" + style="display:block">{% csrf_token %} + <input type="submit" value="test command" name="_save"/> + </form> + <hr/> + </div>