Search code examples
pythondjangomulti-database

Django how to validate a form in CreateView when using foreignkeys and using multiple databases


Hello This is my first question so please forgive the formatting:

Current lab setup

1 project:

library

1 app

catalog

2 databases

library_admin_db (admin-mysql)

catalog_db (mysql1

I'm trying to add a database object on "catalog_db" database using a "CreateView" Class based view

I already set up the Databases connection:

DATABASES = {
# Must use Default
'default': {
    'ENGINE': 'django.db.backends.mysql',
    'NAME': 'library_admin_db',
    'USER': 'root',
    'PASSWORD': 'password',
    'HOST': '192.168.164.128',
    'PORT': '8002'
},
'catalog': {
    'ENGINE': 'django.db.backends.mysql',
    'NAME': 'catalog_db',
    'USER': 'root',
    'PASSWORD': 'password',
    'HOST': '192.168.164.128',
    'PORT': '8000',
}

}

I set up the DATABASE_ROUTERS:

DATABASE_ROUTERS = [
    BASE_DIR / 'routers.db_routers.LibraryRouter'# the "MyApp1Router is the class inside the db_routers file"
]

Here is the Routers class:

    class LibraryRouter:
    
    route_app_labels = {'catalog'}

    def db_for_read(self, model, **hints):
        if model._meta.app_label in self.route_app_labels:
            return 'catalog_db'
        return None

    def db_for_write(self, model, **hints):
        if model._meta.app_label in self.route_app_labels:
            return 'catalog_db'
        return None
    
    def allow_relation(self, obj1, obj2, **hints):
        if (
            obj1._meta.app_label in self.route_app_labels or
            obj2._meta.app_label in self.route_app_labels
        ):
           return True
        return None

    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if app_label in self.route_app_labels:
            return db == 'catalog_db'
        return None

here is my model with the foreign keys:

from django.db import models
from django.urls import reverse
import uuid

# Create your models here.
class Genre(models.Model):
    name = models.CharField(max_length=150)

    def __str__(self):
        return self.name

class Book(models.Model):
    
    title = models.CharField(max_length=200)
    author = models.ForeignKey('Author', on_delete=models.SET_NULL, null=True)
    summary = models.TextField(max_length=600)
    isbn = models.CharField('ISBN', max_length=13, unique=True)
    genre = models.ManyToManyField(Genre)
    language = models.ForeignKey('language', on_delete=models.SET_NULL, null=True)


    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        return reverse('book_detail', kwargs={"pk":self.pk})

class Language(models.Model):
    name = models.CharField(max_length=200)
    def __str__(self):
        return self.name

class Author(models.Model):
    
    first_name = models.CharField(max_length=200)
    last_name = models.CharField(max_length=200)
    date_of_birth = models.DateField(null=True,blank=True)

    class Meta:
        ordering = ['last_name','first_name']
    
    def get_absolute_url(self):
        return reverse('author_detail', kwargs={"pk":self.pk})
    
    def __str__(self):
        return f"{self.last_name} {self.first_name}"

class BookInstance(models.Model):
    id = models.UUIDField(primary_key=True, default=uuid.uuid4)
    book = models.ForeignKey('Book', on_delete=models.RESTRICT, null=True)
    imprint = models.CharField(max_length=200)
    due_back = models.DateField(null=True, blank=True)

    LOAN_STATUS = (
        ('m', "Maintenance"),
        ('o', 'On Loan'),
        ('a', 'Available'),
        ('r', 'Reserved')
    )
    status = models.CharField(max_length=1, choices=LOAN_STATUS, blank=True, default='m')

    class Meta:
        ordering = ['due_back']

    def __str__(self):
        return f'{self.id} ({self.book.title})'

Here is the View:

from django.shortcuts import render
from catalog.models import Book, BookInstance, Author, Genre, Language
from django.views.generic import CreateView
from django.urls import reverse_lazy


# Create your views here.

class BookCreateView(CreateView):
    model = Book
    fields = ['title', 'author', 'summary', 'isbn', 'genre', 'language']
    # queryset = Book.objects.using('catalog') # Must use this if using a secondary database! if not using secondary database then this is automated!
    success_url = reverse_lazy('catalog:home')

    

    def form_valid(self, form):
        temp = form.save(commit=False)
        temp.save(using='catalog')
        print('Hello')
        return super().form_valid(form)

    def form_invalid(self, form):
        print('form_invalid') 
        return super().form_invalid(form)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context['form'].fields['author'].queryset = Author.objects.using('catalog')
        context['form'].fields['language'].queryset = Language.objects.using('catalog').all()
        context['form'].fields['genre'].queryset = Genre.objects.using('catalog').all()
        return context

Here is the Jinja template with the form:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <h1>Teacher Form</h1>
    <form method="POST">
        {% csrf_token %}
        {{ form.as_p }}
        <input type="submit" value="Submit">
    </form>
</body>
</html>

The Issue comes here on the validation:

[enter image description here][1]

[1]: https://i.sstatic.net/oKD1n.png <- Image of issue

I'm not sure how to validate the information for the create form to the "create_db" I'm almost certain it's checking the "admin_db" but none of the records are stored in the "admin_db" only the "create_db".

I've been searching for hours with no luck as it seems everyone uses a single database and as such there isn't much documentation on support for multiple databases.


Solution

  • The problem is that you are specifying the DATABASE_ROUTERS incorrectly. The DATABASE_ROUTERS setting is supposed to be a list of import strings or the database router instance itself, whereas you are passing a list of pathlib.Path objects:

    DATABASE_ROUTERS = [
        BASE_DIR / 'routers.db_routers.LibraryRouter'# the "MyApp1Router is the class inside the db_routers file"
    ]
    

    You coincidentally don't get any error for this because of a few reasons, firstly Django's implementation [GitHub] is just assuming that you are passing a router instance to it:

    for r in self._routers:
        if isinstance(r, str):
            router = import_string(r)()
        else:
            router = r
    

    Next when it comes to actually using the instance it just assumes that the router passed doesn't implement the specific method as seen from the code [GitHub]:

    for router in self.routers:
        try:
            method = getattr(router, action)
        except AttributeError:
            # If the router doesn't have a method, skip to the next one.
            pass
    

    So in simpler words your router isn't actually being used and you should update your settings to pass a proper import string:

    DATABASE_ROUTERS = [
        'routers.db_routers.LibraryRouter' # the "MyApp1Router is the class inside the db_routers file"
    ]