Pavarine
Pavarine

Reputation: 726

Django model form is_valid() in specific database

I have two different database connections defined in my settings.py ('default' and 'banco1').

Then in some view I receive a POST containing some data that I want to validate against a ModelForm that I create:

 product_form = ProductForm(request.POST)
 if product_form.is_valid():

The product model:

class Product(models.Model):

    name = models.CharField(max_length=125, blank=False, null=False)
    description = models.CharField(max_length=255, blank=True)
    abbreviation = models.CharField(max_length=15, blank=True)
    category = models.ForeignKey('inventory.Category',
        on_delete=models.SET_NULL, null=True, blank=True)
    metric = models.CharField(max_length=15, blank=True, null=True)

The Form:

class ProductForm(ModelForm):
    class Meta:
        model = Product
        fields = ['name', 'description', 'abbreviation', 'category', 'metric']

The view:

 def create(request):
        if request.method != 'POST':
            return JsonResponse('Operação inválida', safe=False)

        product_form = ProductForm(request.POST)
        if product_form.is_valid():
            f = product_form.save(commit = False)
            f.save(using=request.session['database'])
            return HttpResponse("Novo product salvo com sucesso")
        else:
            return HttpResponse("Impossível salvar product. Verifique os campos")

The Product model is suposed to be saved always in the 'banco1' database, so the FK validations should be queried from the 'banco1' database, but whenever I try to validade with product_form.is_valid() Django tries to query the 'default' database and throws a validation error as the FK constraint can't be satisfied in the 'default' database.

How can I set a model form to make his validations agains a manually selected database?

EDIT

Was suggested to me to utilize a DB ROUTER schema to being able to select the proper database config, which works fine. My problem is that this selection should be made based in some criteria that not just the model. How can I pass some variables from the view to the db_for_read method in my db router class?

EDIT2

New ProductForm Class

class ProductForm(ModelForm):
    class Meta:
        model = Product
        fields = ['name', 'description', 'abbreviation', 'category', 'metric']

    def __init__(self,database):
        category = forms.ModelChoiceField(queryset=Category.objects.using(database).all())

trying to instantiate the form inside my view like this:

product_form = ProductForm(request.POST, request.session['database'])

Which gives me an error:

init() takes 2 positional arguments but 3 were given

I know that something is not valid with my manner to override the __init__ method but i'm to noobish in python to realize that.

EDIT 3

Last errors:

Traceback:

File "C:\Users\Pavarine\Documents\Visual Studio 2015\Projects\gims\gims\env\lib\site-packages\django\core\handlers\base.py" in get_response 149. response = self.process_exception_by_middleware(e, request)

File "C:\Users\Pavarine\Documents\Visual Studio 2015\Projects\gims\gims\env\lib\site-packages\django\core\handlers\base.py" in get_response 147. response = wrapped_callback(request, *callback_args, **callback_kwargs)

File "C:\Users\Pavarine\Documents\Visual Studio 2015\Projects\gims\gims\gims\apps\inventory\views.py" in create 247. if product_form.is_valid():

File "C:\Users\Pavarine\Documents\Visual Studio 2015\Projects\gims\gims\env\lib\site-packages\django\forms\forms.py" in is_valid 161. return self.is_bound and not self.errors

File "C:\Users\Pavarine\Documents\Visual Studio 2015\Projects\gims\gims\env\lib\site-packages\django\forms\forms.py" in errors 153. self.full_clean()

File "C:\Users\Pavarine\Documents\Visual Studio 2015\Projects\gims\gims\env\lib\site-packages\django\forms\forms.py" in full_clean 362. self._clean_fields()

File "C:\Users\Pavarine\Documents\Visual Studio 2015\Projects\gims\gims\env\lib\site-packages\django\forms\forms.py" in _clean_fields 374. value = field.widget.value_from_datadict(self.data, self.files, self.add_prefix(name))

File "C:\Users\Pavarine\Documents\Visual Studio 2015\Projects\gims\gims\env\lib\site-packages\django\forms\widgets.py" in value_from_datadict 231. return data.get(name)

Exception Type: AttributeError at /inventory/product/create Exception Value: 'tuple' object has no attribute 'get'

Upvotes: 3

Views: 1561

Answers (1)

Nikita
Nikita

Reputation: 6341

To enable the router just add it to DATABASE_ROUTERS in your setting.py, see detailes here: https://docs.djangoproject.com/en/1.9/topics/db/multi-db/#using-routers

Every router method gets hints dictionary, which should contain instance key representing the model instance being used. Depending on what information you want to get from the view, you might be good with the information from instance attributes. See here: https://docs.djangoproject.com/en/1.9/topics/db/multi-db/#topics-db-multi-db-hints.

I.e.:

def db_for_read(self, model, **hints):
    model_instance = hints.get('instance')   
    if model_instance is not None:
        pass
        #perform actions with model_instance attributes
    return None

Other then this I don't think there's other straight way to pass information from the active view to the router, as routers are supposed to work with models and make decisions based on models being used.

DB routers are used for automatic DB selection in models layer, but model methods allow to select the DB manually. See examples here: https://docs.djangoproject.com/en/1.9/topics/db/multi-db/#manually-selecting-a-database

So the other solution is to redefine methods of your form class, i.e. save() (see https://docs.djangoproject.com/en/1.9/topics/forms/modelforms/#the-save-method), and save the form data manually specifying the DB to use with using argument. I.e.:

class ProductForm(ModelForm):
    class Meta:
        ...

    def save(self, commit=True):
        product_to_save=super(ProductForm, self).save(commit=False)
        #the above creates product_to_save instance, but doesn't saves it to DB
        product_to_save.save(using=='banco1')
        return product_to_save

In the comments I suggested to subclass the field that needs to be validated against the other DB, but there's probably even easier way... Since it's FK field it's probably the instance of ModelChoiceField which accepts the queryset argument in constructor, so you could provide it, i.e.:

class ProductForm(ModelForm):
   category_field = forms.ModelMultipleChoiceField(queryset=Category.objects.using('banco1'))
    ...

See: https://docs.djangoproject.com/en/1.9/ref/forms/fields/#fields-which-handle-relationships

Answer to EDIT2

The way you redefined the constructor your database argument gets request.POST and request.session['database'] is not mapped to anything, that's why you get the error.

You should account for other arguments and also call superclass constructor or you'll break the MRO, so something like this should do the job:

class ProductForm(ModelForm):
    ...

    def __init__(self, *args, db_to_use='default', **kwargs):
        super(ProductForm, self).__init__(*args, **kwargs)
        #category = forms.ModelChoiceField(queryset=Category.objects.using(db_to_use).all())
        #the below line suggested as improvement by Alasdair and confirmed as working by Pavarine, so I updated the answer here
        self.fields['category'].queryset = Category.objects.using(database).all()

and then as usual, but use named argument:

product_form = ProductForm(request.POST, db_to_use=request.session['database'])

Upvotes: 4

Related Questions