I am creating a Django application which is multi-tenanted. The custom middleware I use attaches a tenant
object to the request.
My issue is when type checking, my views are not aware of extra attribute on the HttpRequest
class.
I have tried creating a TenantHttpClass
which extends HttpRequest
and adds the tenant attribute.
Edit: Forgot to say I am using mypy to type check.
How do I make my views aware of this. My code is below:
middleware/main.py
:
from typing import Type
from django.db import connection
from django.http import Http404
from django.utils.deprecation import MiddlewareMixin
from apps.tenants.custom_request import TenantHttpRequest as HttpRequest
from apps.tenants.models import Domain, Tenant
from apps.tenants.utils import get_public_schema_name, get_tenant_domain_model, remove_www
from vastdesk import settings
class TenantMainMiddleware(MiddlewareMixin):
TENANT_NOT_FOUND_EXCEPTION: Type[Http404] = Http404
"""
This middleware should be placed at the very top of the middleware stack.
Selects the proper database schema using the request host. Can fail in
various ways which is better than corrupting or revealing data.
"""
@staticmethod
def hostname_from_request(request: HttpRequest) -> str:
"""Extracts hostname from request. Used for custom requests filtering.
By default removes the request's port and common prefixes.
"""
return remove_www(request.get_host().split(":")[0])
def get_tenant(self, domain_model: Domain, hostname: str) -> Tenant:
domain = domain_model.objects.select_related("tenant").get(domain=hostname)
return domain.tenant
def process_request(self, request: HttpRequest) -> None:
# Connection needs first to be at the public schema, as this is where
# the tenant metadata is stored.
connection.set_schema_to_public()
hostname = self.hostname_from_request(request)
domain_model = get_tenant_domain_model()
try:
tenant = self.get_tenant(domain_model, hostname)
except domain_model.DoesNotExist:
self.no_tenant_found(request, hostname)
return
tenant.domain_url = hostname
request.tenant = tenant
connection.set_tenant(request.tenant)
self.setup_url_routing(request)
def no_tenant_found(self, request: HttpRequest, hostname: str) -> None:
"""What should happen if no tenant is found.
This makes it easier if you want to override the default behavior"""
if (
hasattr(settings, "SHOW_PUBLIC_IF_NO_TENANT_FOUND")
and settings.SHOW_PUBLIC_IF_NO_TENANT_FOUND
):
self.setup_url_routing(request=request, force_public=True)
else:
raise self.TENANT_NOT_FOUND_EXCEPTION('No tenant for hostname "%s"' % hostname)
@staticmethod
def setup_url_routing(request: HttpRequest, force_public: bool = False) -> None:
"""
Sets the correct url conf based on the tenant
:param request:
:param force_public
"""
# Do we have a public-specific urlconf?
if hasattr(settings, "PUBLIC_SCHEMA_URLCONF") and (
force_public or request.tenant.schema_name == get_public_schema_name()
):
request.urlconf = settings.PUBLIC_SCHEMA_URLCONF
custom_request.py
:
from typing import Union, TYPE_CHECKING
from django.http import HttpRequest
if TYPE_CHECKING:
from apps.tenants.models import Tenant
class TenantHttpRequest(HttpRequest):
tenant: Union["Tenant", None]
views.py
:
from typing import Any, Dict
from django.views.generic import TemplateView
from apps.tenants.models import Tenant as Realm
from apps_tenants.ticket_system.models import Ticket
class StaffDashboardView(TemplateView):
template_name = "dashboard/dash-staff/dash.html"
def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
context = super(StaffDashboardView, self).get_context_data(**kwargs)
context["logo_url"] = Realm.objects.get(
schema_name=self.request.tenant.schema_name
).logo_url
context["profile_image_url"] = ""
context["tickets"] = Ticket.objects.all()
return context
class CustomerDashboardView(TemplateView):
template_name = "dashboard/dash-customer/dash.html"
def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
context = super(CustomerDashboardView, self).get_context_data(**kwargs)
context["logo_url"] = Realm.objects.get(
schema_name=self.request.tenant.schema_name
).logo_url
context["profile_image_url"] = ""
context["tickets"] = Ticket.objects.all()
return context
CodePudding user response:
Turns out this was actually extremely easy to fix and just need this adding to my views.py
:
from typing import Any, Dict
from django.views.generic import TemplateView
from apps.tenants.models import Tenant as Realm
from apps_tenants.ticket_system.models import Ticket
class StaffDashboardView(TemplateView):
template_name = "dashboard/dash-staff/dash.html"
request: TenantHttpRequest
def get_context_data(self, **kwargs: Dict[str, Any]) -> Dict[str, Any]:
context = super(StaffDashboardView, self).get_context_data(**kwargs)
context["logo_url"] = Realm.objects.get(
schema_name=self.request.tenant.schema_name
).logo_url
context["profile_image_url"] = ""
context["tickets"] = Ticket.objects.all()
return context