diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d375ed1d3cd450803d42d887cd1d391b64592b8d..3fd6a2438cc99ab5a8c25b128319d59622cc7cfc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,7 +7,7 @@ test-style: script: - apt update && apt install -y apache2-dev graphviz graphviz-dev - cd ippisite - - pip install -r requirements-dev.txt + - pip install flake8 - flake8 --config=.flake8 test-ansible: image: python:3.5 @@ -121,4 +121,4 @@ deploy-webserver-production: - 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" only: - - release \ No newline at end of file + - release diff --git a/ippisite/ippidb/forms.py b/ippisite/ippidb/forms.py index f702879d8c332ddfa0803858c94e97b467dc2303..930375a18ea3f0927729cc2a54bcb9e43b21807c 100644 --- a/ippisite/ippidb/forms.py +++ b/ippisite/ippidb/forms.py @@ -3,6 +3,8 @@ iPPI-DB django forms """ import itertools from collections import OrderedDict +from decimal import Decimal +from math import log10 from django import forms from django.contrib import messages @@ -15,6 +17,7 @@ from django.forms import ( modelformset_factory, formset_factory, inlineformset_factory, + widgets, ) from django.utils.translation import ugettext_lazy as _, ugettext @@ -817,10 +820,42 @@ 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, + ) + activity_unit = forms.CharField( + label="Activity unit", + max_length=5, + required=True, + widget=widgets.Select( + choices=( + (None, "-----"), + ("1", "mol"), + ("1e-3", "mmol"), + ("1e-6", "µmol"), + ("1e-9", "nmol"), + ("1e-12", "pmol"), + ), + ) + ) class Meta: model = models.CompoundActivityResult - fields = ("compound_name", "activity_type", "activity", "modulation_type") + fields = ( + "compound_name", + "modulation_type", + "activity", + "activity_type", + "activity_mol", + "activity_unit", + ) + widgets = { + "activity": widgets.HiddenInput(), + } def has_changed(self): """ @@ -837,6 +872,30 @@ class CompoundActivityResultForm(ModelForm): return False return super().has_changed() + def clean(self): + cleaned_data = super().clean() + if "activity_mol" not in cleaned_data: + self.add_error("activity_mol", "Must be provided") + return cleaned_data + if cleaned_data["activity_mol"] == 0: + self.add_error("activity_mol", "Must be greater than 0") + return cleaned_data + if cleaned_data["activity_mol"] is None: + return cleaned_data + if cleaned_data["activity_unit"] in (None, ""): + self.add_error("activity_unit", "Unit must be provided") + return cleaned_data + try: + d = Decimal(-log10( + Decimal(self.cleaned_data["activity_mol"]) * Decimal(self.cleaned_data["activity_unit"]) + )) + d = d.quantize(Decimal(10) ** -self.instance._meta.get_field("activity").decimal_places) + self.cleaned_data["activity"] = d + except Exception as e: + self.add_error("activity_mol", "Unknown error when computing -log10(activity): %s" % str(e)) + self.add_error("activity_unit", "Unknown error when computing -log10(activity): %s" % str(e)) + return cleaned_data + def save(self, commit=True): # right before an actual save, we set the foreign key that have been created in the meantime from unique # identifier (compound_name) we where provided at the initialization of the form. diff --git a/ippisite/ippidb/tests_contribute.py b/ippisite/ippidb/tests_contribute.py index 385dcefd1a8ac03338abba11909e0c58c6145658..dd93aad11b415ab34ea0da79074871c7eab23746 100644 --- a/ippisite/ippidb/tests_contribute.py +++ b/ippisite/ippidb/tests_contribute.py @@ -1,6 +1,8 @@ """ iPPI-DB contribution module tests """ +import math +from decimal import Decimal from tempfile import NamedTemporaryFile from django.contrib.auth import get_user_model @@ -133,7 +135,7 @@ class ContributionViewsTestCase(TestCase): ( models.Compound.objects.validated().count, models.Compound.objects.validated().count(), - "Validated Compounds count", + "Validated Compounds count should remains the same", ), ( models.CompoundAction.objects.count, @@ -159,6 +161,14 @@ class ContributionViewsTestCase(TestCase): "Pharmacokinetic tests count", ), ] + 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) @@ -166,7 +176,7 @@ class ContributionViewsTestCase(TestCase): ( models.Compound.objects.validated().count, models.Compound.objects.count(), - "Validated Compounds count", + "Validated Compounds count should have increased", ) ] contribution_to_be_validated = models.Contribution.objects.get(validated=False) @@ -175,7 +185,7 @@ class ContributionViewsTestCase(TestCase): 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): + def _process_contribution_wizard_without_sanity_check(self, entry_data, error_expected_in_step=None): """ The contribution add "wizard" returns a 200 @@ -199,13 +209,17 @@ class ContributionViewsTestCase(TestCase): } form_data["ippi_wizard-current_step"] = step["step-id"] response = self.client.post(step_url, form_data) - if response.status_code != 302 and step.get("step-id") != "done": + 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: @@ -370,12 +384,15 @@ class ContributionViewsTestCase(TestCase): data[ f"{idx}-compoundactivityresult_set-activity-results-{nidx}-activity_type" ] = compound_activity_result["activity_type"] - data[ - f"{idx}-compoundactivityresult_set-activity-results-{nidx}-activity" - ] = compound_activity_result["activity"] - data[ - f"{idx}-compoundactivityresult_set-activity-results-{nidx}-inhibition_percentage" - ] = compound_activity_result["inhibition_percentage"] + 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"] @@ -500,11 +517,11 @@ class ContributionViewsTestCase(TestCase): } yield item - def test_basic_entry(self): + def get_basic_entry(self): """ Basic entry test """ - entry_data = { + return { "source": "PM", "id_source": "15072770", "in_silico": True, @@ -553,15 +570,75 @@ class ContributionViewsTestCase(TestCase): { "compound_name": "toto", "activity_type": "pIC50", - "activity": 6.85, - "inhibition_percentage": "", + "activity_mol": 6.85, + "activity_unit": "1e-6", "modulation_type": "I", } ], } ], } - self._process_contribution_wizard(entry_data) + + def test_basic_entry(self): + self._process_contribution_wizard(self.get_basic_entry()) + + 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): """ @@ -618,8 +695,8 @@ class ContributionViewsTestCase(TestCase): { "compound_name": "toto", "activity_type": "pIC50", - "activity": 6.85, - "inhibition_percentage": "", + "activity_mol": 6.85, + "activity_unit": "1e-3", "modulation_type": "I", } ], @@ -730,8 +807,8 @@ class ContributionViewsTestCase(TestCase): { "compound_name": "toto", "activity_type": "pIC50", - "activity": 6.85, - "inhibition_percentage": "", + "activity_mol": 6.85, + "activity_unit": "1e-3", "modulation_type": "I", } ], @@ -810,8 +887,8 @@ class ContributionViewsTestCase(TestCase): { "compound_name": "16d", "activity_type": "pIC50", - "activity": 6.85, - "inhibition_percentage": "", + "activity_mol": 6.85, + "activity_unit": "1e-3", "modulation_type": "I", } ], @@ -879,8 +956,8 @@ class ContributionViewsTestCase(TestCase): { "compound_name": "FC", "activity_type": "pIC50", - "activity": "", - "inhibition_percentage": "", + "activity_mol": 0.25, + "activity_unit": "1e-6", "modulation_type": "S", } ],