Skip to content

Commit

Permalink
Merge pull request #156 from ImperialCollegeLondon/chem-calc
Browse files Browse the repository at this point in the history
Calculation of formula mass
  • Loading branch information
dandavies99 authored Sep 1, 2022
2 parents d5fbf74 + 05516dc commit 6de1abc
Show file tree
Hide file tree
Showing 5 changed files with 100 additions and 6 deletions.
18 changes: 16 additions & 2 deletions dfndb/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -34,8 +41,15 @@ def __init__(self, *args, **kwargs):
Div(HTML("<h1> Compound </h1>")),
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).
The mass is calculated automatically from the formula unless
specified otherwise.
<a href="https://www.sciencegateway.org/tools/fwcal.htm"
target="_blank"> Click here for an external molecular weight
calculator &#10697;</a>.
"""
),
css_class="container py-2",
),
Expand Down
10 changes: 9 additions & 1 deletion dfndb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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(
Expand All @@ -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",
Expand Down
63 changes: 63 additions & 0 deletions dfndb/views.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,5 @@ django-filter
tablib
openpyxl
django-storages[azure]
django-override-storage
django-override-storage
molmass
12 changes: 10 additions & 2 deletions tests/dfndb/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@

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"
)
self.model.clean() # to calculate the mass from the formula

def test_compound_creation(self):
for k, v in self.expected.items():
Expand All @@ -26,6 +27,13 @@ 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):
invalid_comp = baker.make_recipe("tests.dfndb.compound", formula="H2So4")
invalid_comp.clean()


class TestComponent(TestCase):
def setUp(self):
Expand Down

0 comments on commit 6de1abc

Please sign in to comment.