From 7f36804ccdebea9126f6f1ccedd78353c6b40540 Mon Sep 17 00:00:00 2001 From: Daniel Davies Date: Thu, 1 Sep 2022 10:10:10 +0100 Subject: [PATCH 1/6] Enable automatic mol mass calculation --- dfndb/forms.py | 13 ++++++++-- dfndb/models.py | 10 +++++++- dfndb/views.py | 63 ++++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 3 ++- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/dfndb/forms.py b/dfndb/forms.py index 7eff9ed6..ea6ee39d 100644 --- a/dfndb/forms.py +++ b/dfndb/forms.py @@ -25,6 +25,13 @@ class NewCompoundForm(DataCreateForm): class Meta: model = Compound fields = ["name", "formula", "mass"] + help_texts = { + "formula": "Chemical formula. This will be used to calculate the mass.", + "mass": ( + "Only fill in if the formula should not be used to automatically " + " calculate the mass. Otherwise, leave as 0." + ), + } def __init__(self, *args, **kwargs): super(NewCompoundForm, self).__init__(*args, **kwargs) @@ -34,8 +41,10 @@ def __init__(self, *args, **kwargs): Div(HTML("

Compound

")), Div( HTML( - """Note: compounds should be specified in their discharged state, - if relevant, and are always public (visible to all users).""" + """ + Compounds should be specified in their discharged state + (if relevant) and are always public (visible to all users). + """ ), css_class="container py-2", ), diff --git a/dfndb/models.py b/dfndb/models.py index 65885cd5..9736dd23 100644 --- a/dfndb/models.py +++ b/dfndb/models.py @@ -4,6 +4,7 @@ from django.db.models import Sum from django.urls import reverse from django_better_admin_arrayfield.models.fields import ArrayField +from molmass import Formula import common.models as cm @@ -13,7 +14,7 @@ class Compound(models.Model): name = models.CharField( max_length=100, - help_text="Full name for the element or compound", + help_text="Full name for the element or compound.", ) formula = models.CharField(max_length=20, help_text="Chemical formula") mass = models.FloatField( @@ -25,6 +26,13 @@ class Compound(models.Model): def __str__(self): return "%s (%s)" % (self.name, self.formula) + def clean(self): + """ + Use the formula to calculate the mass if not specified. + """ + if self.mass == 0: + self.mass = Formula(self.formula).mass + class Meta: unique_together = ( "name", diff --git a/dfndb/views.py b/dfndb/views.py index ddfd521f..41dea83c 100644 --- a/dfndb/views.py +++ b/dfndb/views.py @@ -1,9 +1,12 @@ +from django.contrib import messages +from django.shortcuts import redirect, render from django.urls import reverse_lazy from django.views.generic import DetailView, ListView from django_filters.views import FilterView from django_tables2 import SingleTableMixin from django_tables2.export.views import ExportMixin from guardian.mixins import PermissionListMixin, PermissionRequiredMixin +from molmass import FormulaError from rest_framework.generics import ListCreateAPIView from common.views import ( @@ -31,6 +34,36 @@ class NewCompoundView(PermissionRequiredMixin, NewDataView): success_message = "New compound created successfully." failure_message = "Could not save new compound. Invalid information." + def post(self, request, *args, **kwargs): + form = self.form_class(request.POST, request.FILES) + try: + if form.is_valid(): + obj = form.save(commit=False) + # Do other stuff before saving here + obj.user_owner = request.user + if form.is_public(): + obj.status = "public" + else: + obj.status = "private" + obj.save() + messages.success(request, self.success_message) + # Redirect to object detail view or stay on form if "add another" + if "another" in request.POST: + return redirect(request.path_info) + else: + return ( + redirect(self.success_url) + if self.success_url + else redirect(obj) + ) + messages.error(request, self.failure_message) + except FormulaError as e: + messages.error( + request, + f"{e} - Formula is not valid. Please check your formula or enter the mass manually.", + ) + return render(request, self.template_name, {"form": form}) + class NewComponentView(PermissionRequiredMixin, NewDataViewInline): permission_required = "dfndb.add_component" @@ -98,6 +131,36 @@ class UpdateCompoundView(PermissionRequiredMixin, UpdateDataView): inline_key = "composition" formset = CompositionPartFormSet + def post(self, request, *args, **kwargs): + self.object = self.get_object() + form_class = self.get_form_class() + form = self.get_form(form_class) + try: + if form.is_valid(): + # Do other stuff before saving here + if form.is_public(): + self.object.status = "public" + else: + self.object.status = "private" + self.object.save() + messages.success(request, self.success_message) + # Redirect to object detail view or stay on form if "add another" + if "another" in request.POST: + return redirect(request.path_info) + else: + return ( + redirect(self.success_url) + if self.success_url + else redirect(self.object) + ) + messages.error(request, self.failure_message) + except FormulaError as e: + messages.error( + request, + f"{e} - Formula is not valid. Please check your formula or enter the mass manually.", + ) + return render(request, self.template_name, {"form": form}) + class DataListView(ListView): """View of available data. diff --git a/requirements.txt b/requirements.txt index c54445cf..a618d771 100644 --- a/requirements.txt +++ b/requirements.txt @@ -25,4 +25,5 @@ django-filter tablib openpyxl django-storages[azure] -django-override-storage \ No newline at end of file +django-override-storage +molmass \ No newline at end of file From 62654fccad137fc2261f01c4f6b09daef09cbee7 Mon Sep 17 00:00:00 2001 From: Daniel Davies Date: Thu, 1 Sep 2022 10:20:03 +0100 Subject: [PATCH 2/6] add link to form --- dfndb/forms.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dfndb/forms.py b/dfndb/forms.py index ea6ee39d..23aba48f 100644 --- a/dfndb/forms.py +++ b/dfndb/forms.py @@ -44,6 +44,11 @@ def __init__(self, *args, **kwargs): """ Compounds should be specified in their discharged state (if relevant) and are always public (visible to all users). + The mass is calculated automatically from the formula unless + specified otherwise. + Click here for an external molecular weight + calculator ⧉. """ ), css_class="container py-2", From aa3054e309d1c59692992b1e7c9d3c1adaedfd0e Mon Sep 17 00:00:00 2001 From: Daniel Davies Date: Thu, 1 Sep 2022 10:36:25 +0100 Subject: [PATCH 3/6] clean when saving --- dfndb/models.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dfndb/models.py b/dfndb/models.py index 9736dd23..941f36fd 100644 --- a/dfndb/models.py +++ b/dfndb/models.py @@ -33,6 +33,10 @@ def clean(self): if self.mass == 0: self.mass = Formula(self.formula).mass + def save(self, *args, **kwargs): + self.clean() + return super(Compound, self).save(*args, **kwargs) + class Meta: unique_together = ( "name", From ba22b98ed6421597b4c0ba3ce95acdc84833e3d3 Mon Sep 17 00:00:00 2001 From: Daniel Davies Date: Thu, 1 Sep 2022 10:36:33 +0100 Subject: [PATCH 4/6] Add tests --- tests/dfndb/test_models.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/dfndb/test_models.py b/tests/dfndb/test_models.py index efafd1ac..2e77dbb2 100644 --- a/tests/dfndb/test_models.py +++ b/tests/dfndb/test_models.py @@ -5,9 +5,9 @@ class TestCompound(TestCase): def setUp(self): - self.expected = dict(name="Carbon Dioxide", formula="CO2", mass=44.01) + self.expected = dict(name="Carbon Dioxide", formula="CO2", mass=44.00955) self.model = baker.make_recipe( - "tests.dfndb.compound", name="Carbon Dioxide", formula="CO2", mass=44.01 + "tests.dfndb.compound", name="Carbon Dioxide", formula="CO2" ) def test_compound_creation(self): @@ -26,6 +26,12 @@ def test_unique_together(self): "tests.dfndb.compound", name="Carbon Dioxide", formula="CO2", mass=44.01 ) + def test_invalid_formula(self): + from molmass import FormulaError + + with self.assertRaises(FormulaError): + baker.make_recipe("tests.dfndb.compound", formula="H2So4") + class TestComponent(TestCase): def setUp(self): From 6f5ddb566cde3bae795a6a9470e7af193ca289e4 Mon Sep 17 00:00:00 2001 From: Daniel Davies Date: Thu, 1 Sep 2022 11:18:46 +0100 Subject: [PATCH 5/6] don't clean when saving --- dfndb/models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/dfndb/models.py b/dfndb/models.py index 941f36fd..9736dd23 100644 --- a/dfndb/models.py +++ b/dfndb/models.py @@ -33,10 +33,6 @@ def clean(self): if self.mass == 0: self.mass = Formula(self.formula).mass - def save(self, *args, **kwargs): - self.clean() - return super(Compound, self).save(*args, **kwargs) - class Meta: unique_together = ( "name", From 05516dc8367e95a260c7efc92d594d24a934fec0 Mon Sep 17 00:00:00 2001 From: Daniel Davies Date: Thu, 1 Sep 2022 11:20:57 +0100 Subject: [PATCH 6/6] update tests --- tests/dfndb/test_models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/dfndb/test_models.py b/tests/dfndb/test_models.py index 2e77dbb2..7a457850 100644 --- a/tests/dfndb/test_models.py +++ b/tests/dfndb/test_models.py @@ -9,6 +9,7 @@ def setUp(self): self.model = baker.make_recipe( "tests.dfndb.compound", name="Carbon Dioxide", formula="CO2" ) + self.model.clean() # to calculate the mass from the formula def test_compound_creation(self): for k, v in self.expected.items(): @@ -30,7 +31,8 @@ def test_invalid_formula(self): from molmass import FormulaError with self.assertRaises(FormulaError): - baker.make_recipe("tests.dfndb.compound", formula="H2So4") + invalid_comp = baker.make_recipe("tests.dfndb.compound", formula="H2So4") + invalid_comp.clean() class TestComponent(TestCase):