Search code examples
django-modelsdjango-viewsdjango-forms

How to create an abstraction layer between Form fields and the Model when adding a new record?


In Django, when you want to code a form+view+HTML to create a new record for a model, online guides all steer you to the direct approach. That is, use a ModelForm and a CreateView to expose all the fields and let the user specify each value directly.

But what if -- for purposes of usability and data integrity -- you need an abstraction layer between the displayed form elements, and the record which is ultimately inserted in the model? Say for example,

  • my_model.field_1 needs to be a concatenated string built from user-selected records (dropdown box selections), where the entries in those two dropdowns are based on records in my_other_model1 and my_other_model2
  • my_model.field_2 and my_model.field_3 need to be obtained implicitly from the user-selected record from my_other_model1. Specifically, they are assigned to match the values of my_other_model1.field_7 and my_other_model1.field_9
  • my_model.field_4, an integer, needs to be the sum of the lengths of strings entered in two separate TextFields that don't directly tie to any model
  • my_model.field_5 and my_model.field_6 don't require any abstraction; the user can just enter their values directly in TextFields on the form

As a novice in Django I'm having a very hard time working out even a basic approach for this sort of use case. I've spent days fiddling with JavaScript event handling and storing calculated values in read-only form fields, but it seems like there should be a much cleaner approach.

(Note: the above bullet points are just generalized examples of the sort of functionality I need, not the actual application requirements I'm trying to code.)


Solution

  • Maybe this is not what you were hoping for, but my answer is: The abstraction layer "between user input and the database" are the form and the view. If the usage of default ModelForm and CreateView is too basic for you, then you can overwrite their behaviour to your liking via inheritance.

    # models.py
    class MyModel(models.Model):
        field1 = 
        field2 = 
        field3 = 
        field4 = 
        field5 = 
    
    class OtherModelOne(models.Model):
        field7 = 
        field9 = 
    
    class OtherModelTwo(models.Model):
        field1 = 
        field2 = 
        
        
        
    # forms.py
    class MyModelForm(forms.ModelForm):
        # task 1 & 2
        choice_of_other_models1 = forms.ModelChoiceField(queryset=OtherModelOne.objects.all())
        choice_of_other_models2 = forms.ModelChoiceField(queryset=OtherModelOne.objects.all())
        
        # task 3
        textfield1 = forms.CharField()
        textfield2 = forms.CharField()
        
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            
            # don't display field1, field2, field3 and field4 and disable userinput for it
            field1 = self.fields.get('field1')
            field1.disabled = True
            field1.widget = HiddenInput
            # [...]
        
        def clean(self):
            cleaned_data = super().clean()
            
            # task 2
            cleaned_data['field2'] = cleaned_data.get('choice_of_other_models1').field7
            cleaned_data['field3'] = cleaned_data.get('choice_of_other_models2').field9
            
            # task 3
            cleaned_data['field4'] = len(cleaned_data.get('textfield1')) + len(cleaned_data.get('textfield2'))
            return cleaned_data
    

    I know it might be a lot to take, so lets tackle this in words:

    You have your model and two other models. You want to create MyModel. For its creation, as defined in your tasks, you actually do not need to visualize field1 to field4 because they are dependend on other factors and get not assigned directly. Therefore you disable and hide them for userinput. You could do that already in the model with hidden=True but above you'll find how to do it in the forms __init__()method.

    It is a ModelForm, so field5 and field6 can assigned directly by the user as you know already to do. But since all the other fields are related on parameters external to MyModel you want to add more fields manually. These are here called choice_of_other_models1 and textfield1 and so on. Inside of the clean method you can assign and redefine and modify all of your data inside of the dictionary cleaned_data.

    Remember: If you need this functionality in multiple views it is better to put all that logic inside of the form. If on the other hand you need a more general creation of MyModel and this logic is only needed for this specific view, then of course you should put this logic to your view. This decision has to be done on reusuability.

    I understand that my code above is not a copy paste and it works solution, but I hope I could make my point clear: The abstraction layer you are talking about are the forms and the views. By inheritance it is reusuable and configurable to your needs.

    Try to tackle one of your examples. I'm happy to help further in case you then hit a roadblock with specific (potentially buggy) code.