diff --git a/Cloud_dockerfile b/Cloud_dockerfile index c6c27e6f..06530eb5 100644 --- a/Cloud_dockerfile +++ b/Cloud_dockerfile @@ -8,7 +8,7 @@ ENV CALCUS_CLOUD True ADD ./cloud_requirements.txt /calcus/cloud_requirements.txt RUN pip install -r /calcus/cloud_requirements.txt -RUN apt update && apt install openbabel postgresql-client dos2unix nwchem libxm4 libgl1 -y +RUN apt update && apt install openbabel postgresql-client dos2unix nwchem libxm4 libgl1 libmagic1 -y COPY calcus /calcus/calcus COPY frontend /calcus/frontend diff --git a/Dockerfile b/Dockerfile index 54b5991b..9b0e6694 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,7 +20,7 @@ ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:"/binaries/orca" ENV PATH=$PATH:$XTB4STDAHOME/xtb/bin:$XTB4STDAHOME:$EBROOTORCA:$GAUSS_EXEDIR ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/binaries/orca:/usr/lib/openmpi/ -RUN apt update && apt install openbabel sshpass postgresql-client dos2unix python3-dev gfortran mpi-default-bin mpi-default-dev libxm4 libgl1 -y +RUN apt update && apt install openbabel sshpass postgresql-client dos2unix python3-dev gfortran mpi-default-bin mpi-default-dev libxm4 libgl1 libmagic1 -y RUN curl -LJO https://github.com/nwchemgit/nwchem/releases/download/v7.2.0-release/nwchem-data_7.2.0-2_all.debian_bullseye.deb RUN curl -LJO https://github.com/nwchemgit/nwchem/releases/download/v7.2.0-release/nwchem_7.2.0-2_amd64.debian_bullseye.deb RUN dpkg -i nwchem*7.2.0*.deb diff --git a/cloud_requirements.txt b/cloud_requirements.txt index ac4a6089..4302bc03 100644 --- a/cloud_requirements.txt +++ b/cloud_requirements.txt @@ -23,6 +23,7 @@ periodictable psutil==5.7.2 psycopg2 pysisyphus +python-magic rdkit requests scipy diff --git a/frontend/management/commands/init_static_obj.py b/frontend/management/commands/init_static_obj.py index 9f9fe60c..9b11a0c5 100755 --- a/frontend/management/commands/init_static_obj.py +++ b/frontend/management/commands/init_static_obj.py @@ -211,25 +211,24 @@ def handle(self, *args, **options): title=title, page_path="nmr_prediction_quick.html" ) - if not settings.IS_TEST: - if self.is_absent_property("mo"): - molden = get_showcase("mo_molden") - mo_diagram = get_showcase("mo_diagram") - prop = ShowcaseProperty.objects.create( - name="mo", molden=molden, mo_diagram=mo_diagram - ) + if self.is_absent_property("mo"): + molden = get_showcase("mo_molden") + mo_diagram = get_showcase("mo_diagram") + prop = ShowcaseProperty.objects.create( + name="mo", molden=molden, mo_diagram=mo_diagram + ) - if self.is_absent_ensemble("acetylsalicylic_acid"): - e = ShowcaseEnsemble.objects.create(label="acetylsalicylic_acid") - params = Parameters.objects.create(charge=0, multiplicity=1) - multixyz, E = parse_multixyz_from_file( - os.path.join(showcase_dir, "Acetylsalicylic_Acid.xyz") + if self.is_absent_ensemble("acetylsalicylic_acid"): + e = ShowcaseEnsemble.objects.create(label="acetylsalicylic_acid") + params = Parameters.objects.create(charge=0, multiplicity=1) + multixyz, E = parse_multixyz_from_file( + os.path.join(showcase_dir, "Acetylsalicylic_Acid.xyz") + ) + for ind, (xyz, ener) in enumerate(zip(multixyz, E)): + _xyz = format_xyz(xyz) + s = Structure.objects.create( + parent_ensemble=e, xyz_structure=_xyz, number=ind + 1 + ) + prop = ShowcaseProperty.objects.create( + energy=ener, parent_structure=s, parameters=params ) - for ind, (xyz, ener) in enumerate(zip(multixyz, E)): - _xyz = format_xyz(xyz) - s = Structure.objects.create( - parent_ensemble=e, xyz_structure=_xyz, number=ind + 1 - ) - prop = ShowcaseProperty.objects.create( - energy=ener, parent_structure=s, parameters=params - ) diff --git a/frontend/test_views.py b/frontend/test_views.py index 2a39a044..81d66a9a 100644 --- a/frontend/test_views.py +++ b/frontend/test_views.py @@ -625,8 +625,8 @@ def setUp(self): self.client.force_login(self.user) def test_single_file(self): - with open(os.path.join(tests_dir, "Cl.xyz")) as f: - xyz = bytes(f.read(), "UTF-8") + with open(os.path.join(tests_dir, "Cl.xyz"), "rb") as f: + xyz = f.read() f = SimpleUploadedFile("Cl.xyz", xyz, content_type="chemical/x-xyz") @@ -645,6 +645,48 @@ def test_single_file(self): self.assertEqual(Molecule.objects.count(), 1) self.assertEqual(Ensemble.objects.count(), 1) + def test_iso_8859_1(self): + with open(os.path.join(tests_dir, "ethanol_iso-8859-1.xyz"), "rb") as f: + xyz = f.read() + + f = SimpleUploadedFile("ethanol.xyz", xyz, content_type="chemical/x-xyz") + + params = basic_params.copy() + params["structure"] = "" + params["calc_charge"] = "0" + params["file_structure"] = [f] + params["calc_combine_files"] = ("",) + params["calc_parse_filenames"] = "" + + response = self.client.post("/submit_calculation/", data=params, follow=True) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, "Error while submitting your calculation") + + self.assertEqual(CalculationOrder.objects.count(), 1) + self.assertEqual(Molecule.objects.count(), 1) + self.assertEqual(Ensemble.objects.count(), 1) + + def test_utf_16(self): + with open(os.path.join(tests_dir, "ethanol_utf-16.xyz"), "rb") as f: + xyz = f.read() + + f = SimpleUploadedFile("ethanol.xyz", xyz, content_type="chemical/x-xyz") + + params = basic_params.copy() + params["structure"] = "" + params["calc_charge"] = "0" + params["file_structure"] = [f] + params["calc_combine_files"] = ("",) + params["calc_parse_filenames"] = "" + + response = self.client.post("/submit_calculation/", data=params, follow=True) + self.assertEqual(response.status_code, 200) + self.assertNotContains(response, "Error while submitting your calculation") + + self.assertEqual(CalculationOrder.objects.count(), 1) + self.assertEqual(Molecule.objects.count(), 1) + self.assertEqual(Ensemble.objects.count(), 1) + def test_parse_charge_no_filename(self): with open(os.path.join(tests_dir, "Cl.xyz")) as f: xyz = bytes(f.read(), "UTF-8") @@ -2008,3 +2050,69 @@ def test_refill_busted(self): self.assertEqual( self.user.allocated_seconds, settings.FREE_DEFAULT_COMP_SECONDS + 100 ) + + +class LaunchPageTests(TestCase): + def setUp(self): + call_command("init_static_obj") + self.email = "Tester@test.com" + self.password = "test1234" + + self.user = User.objects.create_superuser( + email=self.email, + password=self.password, + ) + self.proj = Project.objects.create(author=self.user) + self.group = ResearchGroup.objects.create(name="Test group", PI=self.user) + self.client = Client() + self.client.force_login(self.user) + + def test_basic(self): + response = self.client.get("/launch/") + self.assertEqual(response.status_code, 200) + + def test_ensemble_does_not_exist(self): + response = self.client.post("/launch/", data={"ensemble": 2}) + self.assertEqual(response.status_code, 302) + + def test_ensemble_no_structure(self): + mol = Molecule.objects.create(project=self.proj) + e = Ensemble.objects.create(parent_molecule=mol) + response = self.client.post("/launch/", data={"ensemble": e.id}) + self.assertEqual(response.status_code, 200) + + def test_ensemble_no_prop(self): + mol = Molecule.objects.create(project=self.proj) + e = Ensemble.objects.create(parent_molecule=mol) + struct = Structure.objects.create(parent_ensemble=e, number=1) + response = self.client.post("/launch/", data={"ensemble": e.id}) + self.assertEqual(response.status_code, 200) + + def test_ensemble_prop(self): + mol = Molecule.objects.create(project=self.proj) + e = Ensemble.objects.create(parent_molecule=mol) + struct = Structure.objects.create(parent_ensemble=e, number=1) + prop = Property.objects.create(parent_structure=struct) + response = self.client.post("/launch/", data={"ensemble": e.id}) + self.assertEqual(response.status_code, 200) + + def test_ensemble_structure_with_prop(self): + mol = Molecule.objects.create(project=self.proj) + e = Ensemble.objects.create(parent_molecule=mol) + struct = Structure.objects.create(parent_ensemble=e, number=1) + prop = Property.objects.create(parent_structure=struct) + + response = self.client.post( + "/launch/", data={"ensemble": e.id, "structures": [1]} + ) + self.assertEqual(response.status_code, 200) + + def test_ensemble_structure_without_prop(self): + mol = Molecule.objects.create(project=self.proj) + e = Ensemble.objects.create(parent_molecule=mol) + struct = Structure.objects.create(parent_ensemble=e, number=1) + + response = self.client.post( + "/launch/", data={"ensemble": e.id, "structures": [1]} + ) + self.assertEqual(response.status_code, 200) diff --git a/frontend/tests/ethanol_iso-8859-1.xyz b/frontend/tests/ethanol_iso-8859-1.xyz new file mode 100644 index 00000000..f30f1361 --- /dev/null +++ b/frontend/tests/ethanol_iso-8859-1.xyz @@ -0,0 +1,11 @@ +9 +Title +C -1.31970 -0.64380 0.00000 +H -0.96310 -1.65260 0.00000 +H -0.96310 -0.13940 -0.87370 +H -2.38970 -0.64380 0.00000 +C -0.80640 0.08220 1.25740 +H -1.16150 1.09160 1.25640 +H -1.16470 -0.42110 2.13110 +O 0.62360 0.07990 1.25870 +H 0.94410 0.53240 2.04240 diff --git a/frontend/tests/ethanol_utf-16.xyz b/frontend/tests/ethanol_utf-16.xyz new file mode 100644 index 00000000..65842bd9 Binary files /dev/null and b/frontend/tests/ethanol_utf-16.xyz differ diff --git a/frontend/views.py b/frontend/views.py index 60044d42..d9a36f89 100644 --- a/frontend/views.py +++ b/frontend/views.py @@ -34,6 +34,7 @@ import base64, gzip import copy import itertools +import magic from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric import rsa @@ -1620,7 +1621,13 @@ def set_project_default(request): def handle_file_upload(ff, is_local, verify=False): - in_file = clean(ff.read().decode("utf-8")) + blob = ff.read() + + m = magic.open(magic.MAGIC_MIME_ENCODING) + m.load() + encoding = m.buffer(blob) + + in_file = clean(blob.decode(encoding)) fname = clean(ff.name) filename = ".".join(fname.split(".")[:-1]) ext = fname.split(".")[-1] @@ -4549,9 +4556,11 @@ def launch(request): params["interface"] = "simple" if "ensemble" in request.POST.keys(): + e_id = clean(request.POST["ensemble"]) try: - e = Ensemble.objects.get(pk=clean(request.POST["ensemble"])) + e = Ensemble.objects.get(pk=e_id) except Ensemble.DoesNotExist: + logger.info(f"Ensemble with id {e_id} requested, but not found") return redirect("/") if not can_view_ensemble(e, request.user): @@ -4585,16 +4594,20 @@ def launch(request): if s_num not in avail_nums: return HttpResponse(status=404) - # TODO: catch possible edge cases - init_params = struct.properties.all()[0].parameters + if struct.properties.count() > 0: + init_params = struct.properties.first().parameters + if init_params is not None: + params["init_params_id"] = init_params.id params["structures"] = s_str params["structure"] = struct - params["init_params_id"] = init_params.id else: - init_params = e.structure_set.all()[0].properties.all()[0].parameters - - params["init_params_id"] = init_params.id + if e.structure_set.count() > 0: + example_struct = e.structure_set.first() + if example_struct.properties.count() > 0: + init_params = example_struct.properties.first().parameters + if init_params is not None: + params["init_params_id"] = init_params.id elif "calc_id" in request.POST.keys(): calc_id = clean(request.POST["calc_id"]) diff --git a/requirements.txt b/requirements.txt index 457d2c5e..541916c3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,6 +24,7 @@ pre-commit psutil==5.7.2 psycopg2 pysisyphus +python-magic rdkit requests scipy