Reputation: 1948
Disclaimer: I can wipe out the database anytime. So while answering this, please don't care about migrations and stuff.
Imagine me having a model with multiple values:
class Compound(models.Model):
color = models.CharField(max_length=20, blank=True, default="")
brand = models.CharField(max_length=200, blank=True, default="")
temperature = models.FloatField(null=True, blank=True)
melting_temp = models.FloatField(null=True, blank=True)
# more (~20) especially numeric values as model fields
Now I want to add a comment to be stored for every value of that model. For example I want to add a comment "measured in winter" to the temperature
model field.
What is the best approach to do that?
My brainstorming came up with:
temperature_comment = ...
but that sounds not very DRYValue
for every value and connect them to Compound
via OneToOneField
s. But how do I then create a Form for Compound
? Because I want to create a Compound
utilizing one form. I do not want to create every Value
on its own. Also it is not as easy as before, to access and play around with the values inside the Compound
model.I guess this is a fairly abstract question for a usecase that comes up quite often. I do not know why I did not find resources on how to accomplish that.
Upvotes: 2
Views: 272
Reputation: 8222
I would go with a JSONfield.
It's quite easy to dynamically create a form with a Charfield matching every field in a model. The keys to this are (a) the documented Model._meta interface (don't be perturbed by that underscore) and (b) the three-argument form of the Python type
built-in.
I'm writing this off the top of my head, but let's add a JsonField comments
to our object(s) and then construct a form with a Charfield for every field in Compound
except comments
:
def construct_form_class( obj):
# obj can be an instance or a ModelClass.
# we probably don't want a comment for the primary key or the comments field
exclude = ( 'id', 'comments')
form_fields = {}
for field in Compound._meta.get_fields() :
name = field.name
if name in exclude:
continue
charfield = forms.Charfield(
max_length = 120,
# anything else? Maybe look up suitable initial based on field name?
# otherwise blank=True or initial='something'
)
form_fields[ name] = charfield
# construct the form class
MyForm = type( 'MyForm', (forms.Form, ), form_fields )
return MyForm
Now a view for updating the comments on a particular instance compound
. The class includes SingleObjectMixin, which provides everything we need to fetch that instance via get_object
and the usual declarations. ( Classy CBVs comes in handy, as ever)
def UpdateCommentsView( SingleObjectMixin, FormView)
template_name = 'app:update_comments.html'
model = Compound
...
def setup( self, request, *args, **kwargs):
super().setup( request, *args, **kwargs)
self.object = self.get_object()
def get_form_class(self):
return construct_form_class( self.object) # above
def get_initial(self)
return self.object.comments.copy()
def form_valid( self, form):
# form.cleaned_data that has passed validation is an
# appropriate value {fieldname: value, ...} to store
# in a JsonField.
self.object.comments = form.cleaned_data
self.object.save()
Think this is everything essential. No code change is needed if you add a field to the model with a subsequent migration. The dynamically generated form will automatically have a new field with the same name as the model field.
The template can be as simple as presenting {{form.as_p}}
to the user.
Upvotes: 1
Reputation: 21
Depends on the access pattern of the model.
you could have a model1 for values, model2 for comments, one2one relation.
If you access one model more than the other you don't have to load and resolve text or varchar each time.
Upvotes: 0
Reputation: 137
One can also use django many-to-many field relationships to handle it, as specified in the documentation Django many-to-many. Since in winter there can be many temperature measurements with different timestamps, and vice-versa
from django.db import models
class Compound(models.Model):
color = models.CharField(max_length=20, blank=True, default="")
brand = models.CharField(max_length=200, blank=True, default="")
temperature = models.FloatField(null=True, blank=True)
melting_temp = models.FloatField(null=True, blank=True)
# more (~20) especially numeric values as model fields
class Meta:
ordering = ['temperature']
def __str__(self):
return self.tempertaure
class Comment(models.Model):
datetime = models.CharField(max_length=100)
comment = models.ManyToManyField(Compound)
class Meta:
ordering = ['datetime']
def __str__(self):
return self.headline
One can use the python datetime time stamps as strings to save it along with the comments for every melting_temp
Upvotes: 0
Reputation: 1877
I think you're right by going with the 3rd approach. If every value needs to have a comment, than that value is kind of a complex field. The only issue is that it's going to make rendering kind of hard to do, but not too complex.
Also, I would go even further and use a one-to-many field between Compound
and Value
. One Compound
has many values. Each of them has a name, value to be stored and a comment.
When dealing with a form for this, you can dynamically create a form in the __init__
method.
The issue might be that you lose the intended validation in your form, but maybe you can think of how to improve this. You could create different subtypes of Value
, e.g. NumericValue
and so on.
# models.py
class Compound(models.Model):
def save(self, **kwargs)
creating = self.pk is None
super().save(**kwargs)
if creating:
# Initialize all values
self.values.create(name='temperature')
...
class Value(models.Model):
compound = models.ForeignKey(Compound, on_delete=models.CASCADE, related_name='values')
# field name e.g. melting_temp
name = models.CharField(max_length=255)
# verbose name e.g. Melting Temperature
label = models.CharField(max_length=255, blank=True, null=True)
value = models.CharField(max_length=255, blank=True, null=True)
note = models.TextField(blank=True, null=True)
# forms.py
class CompoundForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Create a field for each value
for value in self.instance.values.all():
# In this field, you enter the value
self.fields[value.name] = forms.CharField(label=value.label)
# In this field, you enter the comment (note)
self.fields[f'{value.name}_note'] = forms.CharField(label='Note')
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
# Save values
# You can speed this up using Value.objects.bulk_update
for value in instance.values.all():
value.value = self.cleaned_data.get(value.name)
value.note = self.cleaned_data.get(f'{value.name}_note')
value.save()
Upvotes: 1
Reputation: 43108
The Pythonic way is to use a metaclass:
class CommentModelMeta(models.base.ModelBase):
def __new__(mcs, name, bases, attrs, **kwargs):
for attr_name, attr in list(attrs.items()):
if isinstance(attr, models.Field):
attrs[f'{attr_name}_comment'] = models.CharField(max_length=20, blank=True, default="")
return super().__new__(mcs, name, bases, attrs, **kwargs)
class Compound(models.Model, metaclass=CommentModelMeta):
color = models.CharField(max_length=20, blank=True, default="")
...
If you need code completion, you can use type hints:
class Compound(models.Model, metaclass=CommentModelMeta):
color = models.CharField(max_length=20, blank=True, default="")
brand = models.CharField(max_length=200, blank=True, default="")
temperature = models.FloatField(null=True, blank=True)
melting_temp = models.FloatField(null=True, blank=True)
# more (~20) especially numeric values as model fields
color_comment: models.CharField
temperature_comment: models.CharField
Upvotes: 2
Reputation: 477608
You can make an abstract base model:
class CommentBase(models.Model):
comment = models.CharField(
max_length=128, blank=True, default=None, null=True
)
# …
class Meta:
abstract = True
Then you can mix that model in your models:
class Compound(CommentBase, models.Model):
# …
pass
class Other(CommentBase, models.Model):
# ..
pass
# …
It will automatically add the field(s) defined in CommentBase
to the models that inherit from CommentBase
.
Upvotes: 0