So far, my adventures with Django have been pretty exciting. I’ve created models and forms based on those. I’ve overridden default ModelForm methods to inject extra logic. I’ve learned a lot about Django, and by proxy, Python. Today, I came across a feature (or lack-of, in my opinion) that I didn’t fully enjoy.
For most Many-to-many relationships, all the intermediary table needs to contain are model1_id and model2_id fields. Django supports this, by default, creating a ManyToMany field on a model will generate this intermediary table for you. By doing this, you are able to assign models to the relationship via model1.m2m_field.add(model2). This also registers the relationship in any ModelForm subclass that uses model1 so that when you call model1.save(), all related intermediary rows are inserted as well.
This works great most of the time. But what if you need extra information to be contained within that intermediary table, and therefore, in the intermediary model?
Django allows you to specify an intermediary model when describing ManyToMany relationships. An example of the way I’ve used it is:
class Module(Model):
# ...
class Ammo(Model):
# ...
class Fitting(Model):
# ...
fitted_modules = ManyToMany(Module, through = 'FittedModule')
class FittedModule(Model):
fitting = ForeignKey(Fitting)
module = ForeignKey(Module)
count = IntegerField(default = 1)
ammo = ForeignKey(Ammo, null = True, blank = True)
As you can see, a Fitting has fitted Modules. I need extra information related to the FittedModule: how many modules and what ammo, if any, is loaded. Modules are static information, while Fittings are created by users.
This works perfectly fine. I can create a Fitting, save it, create a FittedModule and assign a Module and the new Fitting and save it. I can do this for multiple FittedModules, and I am able to then access all related Modules via the ManyToMany field on the Fitting model.
Now, normally you would use a FormSet of some type to create a Fitting and FittedModules at the same time. What if you don’t want to or can’t use a FormSet? In my example, I need to create a Fitting, complete with FittedModules, from just a single text field. Basically, the text field contains one Fitted Module per line. My FittingForm:
class FittingForm(models.ModelForm):
# ...
fitting_data = forms.CharField(label = 'Paste your fitting below', max_length = 8096, widget = forms.Textarea)
class Meta:
model = Fitting
exclude = ('fitted_modules')
Essentially, the fitting_data field is a proxy for the fitted_modules field on the model. Somehow, I need some logic somewhere to make this conversion. In my mind, this should go somewhere in the Form, rather than in the view or somewhere else. To summarize, I’ve overridden the _post_clean() method (naughty, I know) because I need this to occur during the first round of validation – not after validation is complete as in the clean() method.
In the _post_clean() method, I access the instance property (which is an instance of Fitting) and set the properties directly. This is what normally happens when _post_clean() is called, so it only feels right. The problem starts when I try to save Fitted Modules.
class FittingForm(models.ModelForm):
# ...
def _post_clean(self):
super(models.ModelForm, self)._post_clean()
fitting = self.instance
# some logic to get a Module from a line in fitting_data
fitted_module = Fitted_Module()
fitted_module.module = module
fitted_module.fitting = fitting
Note the last line. The problem here is that fitting is not yet saved and therefore, fitting_id is not set on the new Fitted_Module model object. If you attempt to call fitted_module.save() you’ll throw an exception from the db layer stating that the fitted_module cannot be saved because fitting_id cannot be null. Even if you call fitted_module.save() after the fitting is saved, you’ll get this error. fitting_id is not a reference to the pk property of the fitting, rather, fitting_id is set at the time of setting the fitted_module.fitting property. At least, it seems to work that way.
So, what I’ve done is created a new list within the Form instance to keep track of intermediary models created while the form was cleaning. When save() is called on the form, the Fitting instance is saved, unless commit = False, it will iterate through the list of intermediary models, re-setting each of the foreign key property, and then saving the model. If commit = False, then, similar to the save_m2m() method that is created, a save_related() method is created and attached to the form instance.
class MyForm(models.ModelForm):
def __init__(self, *args, **kwargs):
self.save_after_self = []
super(models.ModelForm, self).__init__(*args, **kwargs)
def save(self, commit = True):
super(models.ModelForm, self).save(commit)
def save_related():
for model in self.save_after_self:
for field in model._meta.fields:
if isinstance(field, ForeignKey):
if isinstance(self.instance, field.rel.to):
setattr(model, field.name, self.instance)
model.save()
if commit:
save_related()
else:
self.save_related = save_related
return self.instance
Sorry about the excessive indentation. Hopefully you’ll also name your class more appropriately, but you can see that I iterate through the save_after_self property, and then look at each model’s field metadata. If the field is a ForeignKey field and set to the instance of the form, I re-set that property.
As a disclaimer, I’m still very new to Django and Python in general. I’ve probably missed something completely obvious or have broken some cardinal rule. Hopefully the Python gods don’t smite me for this.



good content
This is way more helpful than anyhting else I’ve looked at.
Action requeirs knowledge, and now I can act!