sei-ec-remote / project-4-issues

Open an issue to receive help on project 4
0 stars 0 forks source link

Need Help Dealing with Patch/Post Routes for nested/join tables #121

Closed alysarnau closed 2 years ago

alysarnau commented 2 years ago

What stack are you using?

(ex: MERN(mongoose + react), DR(django + react), PEN, etc.)

DR

What's the problem you're trying to solve?

I'm trying to update 'amount' on a table with 4 lines 1) Material_id (foreign key) 2) Resource_id (foreign key) 3) Amount 4) Primary Key

But I'm not sending the request properly due to the serializer and I'm trying to figure it out!

Post any code you think might be relevant (one fenced block per file)

PATCH route:

class ShowInventoryView(generics.RetrieveUpdateDestroyAPIView):
    permission_classes=(IsAuthenticated,)
    serializer_class = InventorySerializer
        def partial_update(self, request, pk, fk):
        """Update Request"""
        # Locate Character for auth
        # get_object_or_404 returns a object representation of our Character
        character = get_object_or_404(Character, pk=pk)
        # Check the character's owner against the user making this request
        if request.user != character.owner:
            raise PermissionDenied('Unauthorized, you do not own this character')
            # If pass...    
        # set inventory character_id (ref with pk)
        request.data['inventory']['character_id'] = pk
        # set inventory material_id (ref with fk)
        request.data['inventory']['material_id'] = fk
        # Serialize/create inventory for validation
        # Validate updates with serializer
        inventory = InventorySerializer(data=request.data['inventory'])
        if inventory.is_valid():
            # Save & send a 204 no content
            inventory.save()
            return Response(status=status.HTTP_204_NO_CONTENT)
        # If the data is not valid, return a response with the errors
        return Response(inventory.errors, status=status.HTTP_400_BAD_REQUEST)

If you see an error message, post it here. If you don't, what unexpected behavior are you seeing?

When I use postman to patch to with this body:

{
    "inventory": {
        "material_id": 2,
        "character_id": 2,
        "amount": 500
    }
}

I get this:

{
    "material": [
        "This field is required."
    ],
    "character": [
        "This field is required."
    ]
}

What is your best guess as to the source of the problem?

I'm not referencing material or character in a way the serializer can use

What things have you already tried to solve the problem?

In InventorySerializer, I set these fields to read only to help deal with nested data issue:

class InventorySerializer(serializers.ModelSerializer):
    material = MaterialSerializer(source='material_id')
    character = CharacterSerializer(source='character_id')
    class Meta:
        model = Inventory
        fields = ('material', 'character', 'amount', 'id')
        # this sets the 'nested' fields above as read only
        extra_kwargs = {
            'material': {
                'read_only': True
            },
            'character': {
                'read_only': True
            }
        }

Didn't work!

Paste a link to your repository here https://github.com/alysvolatile/my-junimo-api/tree/crudInventory

kestler01 commented 2 years ago

Can you share your models so we can check out how the relationship is set up ?

alysarnau commented 2 years ago

Material:

from django.db import models
from django.core.validators import MaxValueValidator, MinValueValidator

# Create your models here.
class Material(models.Model):
    material_name = models.CharField(max_length=50)
    material_description = models.CharField(max_length=500)
    material_image = models.CharField(max_length=150)
    sale_price = models.IntegerField(
        validators=[
            MaxValueValidator(2147483647),
            MinValueValidator(1),
        ]
    )
    link_to_wiki = models.CharField(max_length=150)
    def __str__(self):
        return self.material_name

Character

from django.db import models
from django.contrib.auth import get_user_model
from django.core.validators import MaxValueValidator, MinValueValidator

# Create your models here.
class Character(models.Model):
    # define fields
    # https://docs.djangoproject.com/en/3.0/ref/models/fields/
    owner = models.ForeignKey(
        get_user_model(),
        on_delete=models.CASCADE
    )
    # we will use these constants for the pets later on
    CAT = 'Cat'
    DOG = 'Dog'
    # this defines the pet choices later on
    PET_TYPE_CHOICES = [
        (CAT, 'Cat'),
        (DOG, 'Dog'),
    ]
    # we will use these constants for the love interests! yes I will be adding in Krobus
    ALEX = 'AL'
    ELLIOT = 'EL'
    HARVEY = 'HA'
    SAM = 'SA'
    SEBASTIAN = 'SE'
    SHANE = 'SH'
    ABIGAIL = 'AB'
    EMILY = 'EM'
    HALEY = 'HL'
    LEAH = 'LE'
    MARU = 'MA'
    PENNY = 'PE'
    KROBUS = 'KR'
    # this defines the love interest choices later on
    LOVE_INTEREST_CHOICES = [
        (ALEX, 'Alex'),
        (ELLIOT, 'Elliot'),
        (HARVEY, 'Harvey'),
        (SAM, 'Sam'),
        (SEBASTIAN, 'Sebastian'),
        (SHANE, 'Shane'),
        (ABIGAIL, 'Abigail'),
        (EMILY, 'Emily'),
        (HALEY, 'Haley'),
        (LEAH, 'Leah'),
        (MARU, 'Maru'),
        (PENNY, 'Penny'),
        (KROBUS, 'Krobus'),
    ]
    # and these are for the pet urls
    CAT1 = 'C1'
    CAT2 = 'C2'
    CAT3 = 'C3'
    DOG1 = 'D1'
    DOG2 = 'D2'
    DOG3 = 'D3'
    # this defines the pet URL choices later on
    PET_URL_CHOICES = [
        (CAT1, 'Cat 1'),
        (CAT2, 'Cat 2'),
        (CAT3, 'Cat 2'),
        (DOG1, 'Dog 1'),
        (DOG2, 'Dog 2'),
        (DOG3, 'Dog 3'),
    ]
    # and now for the models
    name = models.CharField(max_length=12)
    Platform = models.CharField(max_length=30)
    farm_name = models.CharField(max_length=12)
    # this lets us define the choices given the character
    pet_type = models.CharField(
        max_length=3,
        choices=PET_TYPE_CHOICES,
    )
    pet_name = models.CharField(max_length=12)
    pet_image = models.CharField(
        max_length=2,
        choices=PET_URL_CHOICES,
    )
    love_interest = models.CharField(
        max_length=2,
        choices=LOVE_INTEREST_CHOICES,
    )
    horse_name = models.CharField(max_length=12)
    total_g = models.IntegerField(
        default=1,
        validators=[
            MaxValueValidator(2147483647),
            MinValueValidator(0),
        ]
    )
    year = models.IntegerField(
        default=1,
        validators=[
            MaxValueValidator(2147483647),
            MinValueValidator(1),
        ]
    )
    def __str__(self):
        return self.name

Inventory

from django.db import models

from JunimoDatabaseApp.models.character import Character
from JunimoDatabaseApp.models.material import Material

# you will need to bring over the user for authentication
# do we want to create an amount of 0 for all resource IDs?

# Create your models here.
class Inventory(models.Model):
    character_id = models.ForeignKey(Character, on_delete=models.CASCADE)
    material_id = models.ForeignKey(Material, on_delete=models.CASCADE)
    amount = models.IntegerField()
    def __str__(self):
        return ("{}'s {}".format(self.character_id, self.material_id) )

# if update amount, all that is needed to send is character id, resource id, new amount
alysarnau commented 2 years ago

For further context, the show route works fine and this is how it delivers the json:

{
    "inventory": {
        "material": {
            "id": 2,
            "material_name": "Bone Fragment",
            "material_description": "A small piece of bone.",
            "material_image": "Bone_Fragment.png",
            "sale_price": 12,
            "link_to_wiki": "https://stardewvalleywiki.com/Bone_Fragment"
        },
        "character": {
            "id": 2,
            "name": "Alys",
            "Platform": "Switch",
            "farm_name": "Fern",
            "pet_type": "Dog",
            "pet_name": "Sunny",
            "pet_image": "D2",
            "love_interest": "PE",
            "horse_name": "Trelle",
            "total_g": 999999,
            "year": 4,
            "owner": 1
        },
        "amount": 7,
        "id": 2
    }
}

I'm running into an issue with the nested values :/

alysarnau commented 2 years ago

Hm, good news, getting a new error! Bad news, getting a new error. Here's what I get for the patch route:

image

Here is my updated serializer:

# for serializer SPECIFICALLY for PATCH/POST routes
class UpdateInventorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Inventory
        fields = ('__all__')

And my updated views:


# this will affect ONE inventory entry for ONE character
class ShowInventoryView(generics.RetrieveUpdateDestroyAPIView):
    permission_classes=(IsAuthenticated,)
    def get(self, request, pk, fk):
        """Index request"""
        # Filter the characters by owner, so you can only see your owned characters
        character = get_object_or_404(Character, pk=fk)
        # Only do request if they own the character whose inventory it is
        if request.user != character.owner:
            raise PermissionDenied('Unauthorized, you do not own this character')
        # Filter the inventories by character, so you can only see your owned inventories
        inventory = get_object_or_404(Inventory, pk=pk, character_id=fk)
        # Run the data through the serializer
        data = InventorySerializer(inventory).data
        return Response({ 'inventory': data })

    # THESE ARE TRICKY DUE TO NESTED DATA
    # create an entry in inventory
    def post(self, request, pk, fk):
        # this would work for patch better
        """Create request"""
        # Filter the characters by owner, so you can only see your owned characters
        character = get_object_or_404(Character, pk=fk)
        # Only do request if they own the character whose inventory it is
        if request.user != character.owner:
            raise PermissionDenied('Unauthorized, you do not own this character')
        # Only do request if they own the character whose inventory it is
        if request.user != character.owner:
            raise PermissionDenied('Unauthorized, you do not own this character')
            # If pass...
        # print the request data
        print(request.data)
        # Serialize/create inventory
        inventory = UpdateInventorySerializer(data=request.data)
        # If the inventory data is valid according to our serializer...
        if inventory.is_valid():
            # Save the created inventory & send a response
            inventory.save()
            return Response({ 'inventory': inventory.data }, status=status.HTTP_201_CREATED)
        # If the data is not valid, return a response with the errors
        return Response(inventory.errors, status=status.HTTP_400_BAD_REQUEST)

    def patch(self, request, pk, fk):
        """Update Request"""
        # Locate Character for auth
        character = get_object_or_404(Character, pk=fk)
        # Check the character's owner against the user making this request
        if request.user != character.owner:
            raise PermissionDenied('Unauthorized, you do not own this character')
            # If pass...    
        inventory = get_object_or_404(Inventory, pk=pk)
        # Serialize/create inventory for validation
        # Validate updates with serializer
        updated_inventory = UpdateInventorySerializer(inventory, data=request.data)
        if updated_inventory.is_valid():
            # Save & send a 204 no content
            updated_inventory.save()
            return Response(status=status.HTTP_204_NO_CONTENT)
        # If the data is not valid, return a response with the errors
        return Response(updated_inventory.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk, fk):
        """Delete request"""
        # Locate mango to delete
        character = get_object_or_404(Character, pk=pk)
        # Check the characters's owner against the user making this request
        if request.user != character.owner:
            raise PermissionDenied('Unauthorized, you do not own this character')
        # Only delete if the user owns the character
        # set inventory to delete
        inventory = get_object_or_404(Inventory, pk=fk)
        inventory.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
alysarnau commented 2 years ago

FIXED IT!!!!!

For the post and patch requests, I just had to unnest the body. For example:

instead of this in Postman:

{
    "inventory": {
        "material_id": 2,
        "character_id": 2,
        "amount": 500
    }
}

I had to send this in Postman

{
    "material_id": 2,
    "character_id": 2,
    "amount": 500
}