eadwinCode / django-ninja-extra

Django Ninja Extra - Class-Based Utility and more for Django Ninja(Fast Django REST framework)
https://eadwincode.github.io/django-ninja-extra/
MIT License
387 stars 32 forks source link

Error defining POST body schema for class-based controller #197

Open tmorgan497 opened 1 day ago

tmorgan497 commented 1 day ago

Hello. I'm having an issue defining a controller for a POST endpoint containing a body with a ninja schema. It seems like ninja_extra is importing the ProductCreateSchema after it creates the api_controller causing it to not be able to import ProductCreateSchema. I also attempted importing my schema before importing ninja_extra, but no luck.  

# schemas.py
"""Schemas for the shop app."""

from __future__ import annotations

from ninja import Schema
from pydantic import BaseModel

class ProductCreateSchema(Schema):
    """Schema for creating products."""

    name: str
    description: str = ""
    price: float = 0.0
# api.py
from shop.schemas import ProductCreateSchema
from ninja import Router
from django.http import HttpRequest  # noqa: TCH002
from ninja_extra import ControllerBase, http_post, permissions, api_controller
from ninja_jwt.authentication import JWTAuth

router = Router(tags=["Shop"])

class IsCmsAdmin(permissions.BasePermission):
    """Custom permission for cms_admin role."""

    def has_permission(self, request: HttpRequest, controller: ControllerBase) -> bool:  # noqa: ARG002
        """Check if user has cms_admin role."""
        user = request.auth
        return user and user.groups.filter(name="cms_admin").exists()

@api_controller
class ProductController:
    """Product API controller."""

    @http_post("/product", auth=JWTAuth(), permissions=[IsCmsAdmin])
    def create_product(
        self,
        request: HttpRequest,  # noqa: ARG002
        payload: ProductCreateSchema,
    ) -> dict:
        """Create product API endpoint."""
        product = Product.objects.create(
            name=payload.name,
            description=payload.description,
            price=payload.price,
        )
        return {
            "id": product.id,
            "name": product.name,
            "description": product.description,
            "price": product.price,
        }
File "/code/backend/urls.py", line 8, in <module>
  from backend.api import api
File "/code/backend/api.py", line 12, in <module>
  from shop.api import router as shop_router
File "/code/shop/api.py", line 33, in <module>
  @api_controller
   ^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ninja_extra/controllers/base.py", line 558, in api_controller
  return APIController(
         ^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ninja_extra/controllers/base.py", line 405, in __call__
  self._add_operation_from_route_function(v)
File "/usr/local/lib/python3.12/site-packages/ninja_extra/controllers/base.py", line 470, in _add_operation_from_route_function
  route_function.operation = self.add_api_operation(
                             ^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ninja_extra/controllers/base.py", line 505, in add_api_operation
  operation = path_view.add_operation(
              ^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ninja_extra/operation.py", line 361, in add_operation
  operation = operation_class(
              ^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ninja_extra/operation.py", line 66, in __init__
  super().__init__(path, methods, view_func, **kwargs)
File "/usr/local/lib/python3.12/site-packages/ninja/operation.py", line 82, in __init__
  self.signature = ViewSignature(self.path, self.view_func)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ninja/signature/details.py", line 48, in __init__
  self.signature = get_typed_signature(self.view_func)
                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ninja/signature/utils.py", line 43, in get_typed_signature
  annotation=get_typed_annotation(param, globalns),
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ninja/signature/utils.py", line 54, in get_typed_annotation
  annotation = make_forwardref(annotation, globalns)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ninja/signature/utils.py", line 60, in make_forwardref
  return evaluate_forwardref(forward_ref, globalns, globalns)
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/site-packages/ninja/signature/utils.py", line 20, in evaluate_forwardref
  return cast(Any, type_)._evaluate(globalns, localns, recursive_guard=set())
         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.12/typing.py", line 924, in _evaluate
  eval(self.__forward_code__, globalns, localns),
  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<string>", line 1, in <module>
backend  | NameError: name 'ProductCreateSchema' is not defined
eadwinCode commented 14 hours ago

@tmorgan497 I will look into this. But at the moment can you share your python version, ninja-extra version and your pydantic version

eadwinCode commented 13 hours ago

Also, the error message does not show where you registered the controller

eadwinCode commented 13 hours ago

I copied your code to a new Django project and it worked perfectly

Screenshot 2024-11-01 at 5 33 22 AM Screenshot 2024-11-01 at 5 35 10 AM
tmorgan497 commented 5 hours ago

@tmorgan497 I will look into this. But at the moment can you share your python version, ninja-extra version and your pydantic version

My versions are:

I purged my docker volumes and reinstalled and now I'm not getting the import error. I also switched to using the router that I already have in this module. However, I now have an issue where I have to pass self as a parameter to the endpoint. Is there a way to suppress self as a query parameter for the endpoint? If I remove self from the args, then I get a linting error (which I can suppress, but I didn't know if this is the proper way to go about this).

image image

"""API endpoints for the shop app."""

# from __future__ import annotations

import logging

from shop.schemas import BaseQueryParams, ProductResponse, ProductReadSchema, ProductCreateSchema  # isort: skip

from ninja import Query, Router
from django.http import HttpRequest
from ninja_extra import ControllerBase, permissions, api_controller
from django.db.models import Q
from django.core.paginator import Paginator
from ninja_jwt.authentication import JWTAuth
from django.contrib.postgres.search import TrigramSimilarity

from api.api import GenericResponse
from shop.models import Vendor, Product, ProductImage

router = Router(tags=["Shop"])

logger = logging.getLogger(__name__)

class IsCmsAdmin(permissions.BasePermission):
    """Custom permission for cms_admin role."""

    def has_permission(self, request: HttpRequest) -> bool:
        """Check if user has cms_admin role."""
        user = request.auth
        return user and user.groups.filter(name="cms_admin").exists()

@api_controller("shop_class/", auth=[JWTAuth()], permissions=[IsCmsAdmin])
class ProductController(ControllerBase):
    """Product API controller."""

    @router.post("/product")
    def create_product(
        request: HttpRequest,
        payload: ProductCreateSchema,
    ) -> dict:
        """Create product API endpoint."""
        product = Product.objects.create(
            name=payload.name,
            description=payload.description,
            price=payload.price,
        )
        return {
            "id": product.id,
            "name": product.name,
            "description": product.description,
            "price": product.price,
        }