vitalik / django-ninja

💨 Fast, Async-ready, Openapi, type hints based framework for building APIs
https://django-ninja.dev
MIT License
7.14k stars 426 forks source link

Getting objects by id of authenticated user #857

Closed kimdre closed 1 year ago

kimdre commented 1 year ago

I am using ninja with authentication and now want to retrieve objects of the authenticated user. I get an error that I'm not able to fix. The api wants me to pass a valid user_id, however I already to that (I confirmed it returns the correct value and this also works in other functions)

What am I missing here or what can I do to fix this?

Request URL: http://127.0.0.1:8000/api/accounts/contacts?limit=100&offset=0 422 Error: Unprocessable Entity Response body

{
  "detail": [
    {
      "loc": [
        "path",
        "user_id"
      ],
      "msg": "value is not a valid integer",
      "type": "type_error.integer"
    }
  ]
}

My api.py The first 2 functions work (one with id of authenticated user and one with passed id in the url). The third one does not work

# This works (Get user details by id of currently authenticated user)
@router.get("/", response=UserOut,
            tags=["User"],
            summary="Get curent user",
            description="Returns details of current user")
def get_current_user(request):
    return get_object_or_404(User, id=request.user.id)

# This also works (Get contacts of user by a passed user id)
@router.get("/{user_id}/contacts", response=list[ContactOut],
            tags=["Contact"],
            summary="Get all contacts of a user",
            description="Returns all contacts of a user")
@paginate
def get_account_contacts(request, user_id: int):
    return get_list_or_404(Contact, user=user_id)

# This does not work (Get contacts of user by id of currently authenticated user)
@router.get("/contacts", response=list[ContactOut],
            tags=["Contact"],
            summary="Get all contacts of current user",
            description="Returns all contacts of current user")
@paginate
def get_account_contacts_of_current_user(request):
    return get_list_or_404(Contact, user=request.user.id)

My schema.py:

class UserOut(ModelSchema):
    class Config:
        model = User
        model_exclude = ['password', 'user_permissions', 'groups',
                         'is_superuser', 'is_staff', 'first_name', 'last_name']

class EmailOut(ModelSchema):
    class Config:
        model = Email
        model_fields = "__all__"

class ContactOut(ModelSchema):
    emails: list[EmailOut] = []
    class Config:
        model = Contact
        model_fields = "__all__"

My models.py

class Contact(BaseModel):
    """
    Contacts that will be applied to projects for billing and notifications.
    A project can only have one contact applied, a contact can have only one address but multiple email addresses.
    """
    user = models.ForeignKey(
        User, on_delete=models.CASCADE, related_name="contacts", related_query_name="user")
    is_organization_choice = [
        (0, "Person"),
        (1, "Organisation")
    ]
    is_organization = models.BooleanField(
        default=False, choices=is_organization_choice)
    organization_name = models.CharField(max_length=150, null=True, blank=True)
    vat_number = models.CharField(null=True, blank=True, max_length=15)
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    street = models.CharField(max_length=75)
    street_addition = models.CharField(max_length=150, null=True, blank=True)
    postal_code = models.CharField(max_length=12)
    city = models.CharField(max_length=100)
    country = models.CharField(max_length=2, default="DE",
                               validators=[RegexValidator('^[A-Z_]*$', 'Only uppercase letters and underscores allowed.')])
    phone_regex = RegexValidator(
        regex=r'^\+?1?\d{9,15}$', message="Phone number must be entered in the format: '+999999999'. Up to 15 digits allowed.")
    phone_number = models.CharField(validators=[phone_regex], max_length=17)

    class Meta:
        indexes = [
            models.Index(fields=["first_name", "last_name"],
                         name="full_name_index")
        ]

    def full_name(self):
        return f"{self.first_name} {self.last_name}"

    def __str__(self):
        if self.is_organization and self.organization_name:
            return f"{self.user.email} - {self.organization_name}"
        else:
            return f"{self.user.email} - {self.full_name()}"

class Email(BaseModel):
    contact = models.ForeignKey(Contact, on_delete=models.CASCADE, related_name="emails", related_query_name="contact")
    email_address = models.EmailField(max_length=254, db_index=True, null=False, blank=False)
    is_contact_email = models.BooleanField(_("contact email"), default=True)
    is_billing_email = models.BooleanField(_("billing email"), default=False)
    is_subscribed_to_status_notifications = models.BooleanField(_("status notifications"), default=True)
    is_subscribed_to_newsletter = models.BooleanField(_("newsletter"), default=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["contact", "email_address"],
                name="contact_and_email_address_unqiue"
            ),
            models.UniqueConstraint(
                fields=["contact", "is_billing_email"],
                name="one_billing_email_per_contact",
                condition=models.Q(is_billing_email=True)
            )
        ]

    def __str__(self):
        return f"{self.email_address}"
OtherBarry commented 1 year ago

@kimdre I think this is due to the order and similarity of your urls. The /contacts endpoint is getting interpreted as /{user_id}/contacts, with an empty user_id. ( @vitalik I assume this is a problem with django's routing, rather than anything ninja is doing? )

This should be pretty easily fixed by moving the get_account_contacts_of_current_user endpoint above get_account_contacts, and/or by changing the url for get_account_contacts to "/{user_id:int}/contacts"

kimdre commented 1 year ago

Thank you 👍 Both your proposals did work for me.

rkulinski commented 9 months ago

I have similar problem. I don't understand why different endpoint is considered a different one... Why would something like that happen?