Handling data for both JSON and FormData in Angular

Discussão em 'Angular' iniciado por Stefan Makivic, Janeiro 14, 2025.

  1. So im kinda new to angular and django, but im trying to create a web app kinda similar to Amazon, where the user is able to log in and browse different listings from other user, create his own listings and do basic CRUD operations on his own listings. When creating a listing the user needs to add a title, description, price, location choose a category and subcategory, add values to the attributes, and he can attach images as files. In the database both categories and subcategories have their own "attributes" whose values also need to be inputed by the user. For an example a category is lets say "Pet", that will have a attribute lets say "Age", and for a subcategory "Dog" there will be an attribute "Breed". I found a tutorial online for adding images, and basically im sending them as files, and saving them in a folder on backend. When I try to add a listing, everything works fine except that the attributes are never added to the database. I added some console logs on front to see if im getting them and sending as json and I do, so I think that the problem is on backend so I tried to add some prints on backend to try and see whats the problem, and I think its because of how formData handles JSON

    I tried googling, Gemini, ChatGpt, Phind, nothing helped so here I am Here is my ts file

    @Component({
    standalone: true,
    selector: 'app-add-listing',
    templateUrl: './add-listing.component.html',
    styleUrls: ['./add-listing.component.scss'],
    imports: [
    CommonModule,
    MatFormField,
    MatLabel,
    MatInputModule,
    MatSelect,
    MatOption,
    ReactiveFormsModule,
    TranslateModule
    ]
    })
    export class AddListingComponent implements OnInit {
    listingForm: FormGroup;
    categories: any[] = [];
    subcategories: string[] = [];
    selectedAttributes: string[] = [];
    images: File[] = [];
    imagePreviews: string[] = [];

    constructor(
    private fb: FormBuilder,
    private authService: AuthService,
    @Inject(ListingService) private listingService: ListingService,
    private toastr: ToastrService,
    private router: Router
    ) {
    this.listingForm = this.fb.group({
    user_id: [this.authService.getUserId()],
    title: ['', [Validators.required]],
    description: ['', [Validators.required]],
    price: ['', [Validators.required]],
    location: [''],
    category: ['', Validators.required],
    subcategory: ['', Validators.required],
    attributes: this.fb.array([]),
    });
    }

    ngOnInit(): void {
    this.listingService.getCategories().subscribe({
    next: (categories: any[]) => {
    this.categories = categories.map((category) => ({
    name: category.category.category_name,
    attributes: category.attributes.map((attr: any) => attr.attribute_name),
    subcategories: category.subcategories.map((subcategory: any) => ({
    name: subcategory.subcategory_name,
    attributes: subcategory.attributes.map((attr: any) => attr.attribute_name),
    })),
    }));
    },
    error: (error) => {
    console.error('Error fetching categories:', error);
    },
    });
    }

    get attributes(): FormArray {
    return this.listingForm.get('attributes') as FormArray;
    }

    onCategoryChange(event: any): void {
    const category = this.categories.find((cat) => cat.name === event.value);
    this.subcategories = category ? category.subcategories.map((sub: any) => sub.name) : [];
    this.selectedAttributes = category ? category.attributes : [];
    this.updateAttributesFormArray(this.selectedAttributes);
    }

    onSubCategoryChange(event: any): void {
    const selectedCategory = this.categories.find((cat) =>
    cat.subcategories.some((sub: any) => sub.name === event.value)
    );
    const selectedSubcategory = selectedCategory?.subcategories.find((sub: any) => sub.name === event.value);
    const parentAttributes = selectedCategory?.attributes || [];
    const subcategoryAttributes = selectedSubcategory?.attributes || [];
    this.selectedAttributes = [...new Set([...parentAttributes, ...subcategoryAttributes])];
    this.updateAttributesFormArray(this.selectedAttributes);
    }

    private updateAttributesFormArray(attributes: string[]): void {
    this.attributes.clear();
    attributes.forEach((attr) => {
    this.attributes.push(
    this.fb.group({
    attribute_name: [attr, Validators.required],
    value: ['', Validators.required],
    })
    );
    });
    }

    onImageChange(event: Event): void {
    const files = (event.target as HTMLInputElement).files;
    if (files && this.images.length + files.length <= 5) {
    Array.from(files).forEach((file) => {
    this.images.push(file);
    const reader = new FileReader();
    reader.onload = () => this.imagePreviews.push(reader.result as string);
    reader.readAsDataURL(file);
    });
    } else {
    this.toastr.error('You can upload a maximum of 5 images.');
    }
    }

    removeImage(index: number): void {
    this.images.splice(index, 1);
    this.imagePreviews.splice(index, 1);
    }

    onSubmit(): void {
    if (this.listingForm.valid) {
    const formData = new FormData();
    formData.append('data', JSON.stringify(this.listingForm.value));
    this.images.forEach((image) => {
    formData.append('images', image);
    });

    this.listingService.createListing(formData).subscribe({
    next: () => {
    this.toastr.success('Listing created successfully');
    this.router.navigate(['/user', this.authService.getUserId()]);
    },
    error: (error) => {
    console.error('Error creating listing:', error);
    },
    });
    }
    }
    }



    and here is the backend code for creating a listing


    from rest_framework.views import APIView
    from rest_framework.response import Response
    from rest_framework import status
    from rest_framework.permissions import AllowAny
    from drf_yasg.utils import swagger_auto_schema
    from ..serializers.listing_serializers import ListingCreateSerializer
    from merkatapp.models import User
    from rest_framework.parsers import MultiPartParser, JSONParser
    from django.http import QueryDict
    import json

    class ListingCreateAPI(APIView):
    permission_classes = [AllowAny]
    parser_classes = [MultiPartParser, JSONParser] # Allow JSON and multipart/form-data

    @swagger_auto_schema(
    request_body=ListingCreateSerializer,
    responses={
    201: "Listing created successfully.",
    400: "Bad Request",
    },
    )
    def post(self, request, *args, **kwargs):
    print("-------------------------------------------\n")
    print("Request data:", request.data)
    print("-------------------------------------------\n")

    # Create a mutable copy of request.data
    data_copy = request.data.dict() if isinstance(request.data, QueryDict) else request.data

    print("-------------------------------------------\n")
    print("Request data copy:", data_copy)
    print("-------------------------------------------\n")

    # Extract and parse the JSON payload from the 'data' field
    if 'data' in data_copy:
    try:
    parsed_data = json.loads(data_copy['data'])
    data_copy = parsed_data # Merge parsed JSON data into data_copy
    print("Parsed and merged data:", data_copy)
    except json.JSONDecodeError as e:
    return Response({"error": f"Invalid JSON in 'data': {str(e)}"}, status=status.HTTP_400_BAD_REQUEST)
    print("-------------------------------------------\n")
    print("Request data after parsing:", data_copy)
    print("-------------------------------------------\n")


    # Reconstruct the request data as a QueryDict
    mutable_data = QueryDict('', mutable=True)
    mutable_data.update(data_copy)

    print("-------------------------------------------\n")
    print("Request data images:", request.FILES.getlist('images'))
    print("-------------------------------------------\n")

    mutable_data.setlist('images', request.FILES.getlist('images')) # Add images as a list

    print("-------------------------------------------\n")
    print("Mutable data:", mutable_data)
    print("-------------------------------------------\n")

    serializer = ListingCreateSerializer(data=mutable_data)

    print("-------------------------------------------\n")
    print("Serializer data:", serializer)
    print("-------------------------------------------\n")


    if serializer.is_valid():
    user_id = serializer.validated_data.pop('user_id', None)
    try:
    user = User.objects.get(user_id=user_id)
    except User.DoesNotExist:
    return Response({"error": f"User with ID {user_id} does not exist."}, status=status.HTTP_400_BAD_REQUEST)

    serializer.save(user=user)
    return Response({"message": "Listing created successfully."}, status=status.HTTP_201_CREATED)
    return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)




    as you can see I added some prints for debugging, and when trying to create a listing here what those prints printed

    Request data: <QueryDict: {'data': ['{"user_id":10,"title":"Testing","description":"Creating a listing","price":100,"location":"Sweden","category":"Video igre","subcategory":"Kompjuter","attributes":[{"attribute_name":"Platforma (PC, konzola, mobilni)","value":"PC"},{"attribute_name":"Žanr","value":"Action"}]}'], 'images': [<InMemoryUploadedFile: download (2).jpg (image/jpeg)>, <InMemoryUploadedFile: download (1).jpg (image/jpeg)>]}>
    -------------------------------------------

    -------------------------------------------

    Request data copy: {'data': '{"user_id":10,"title":"Testing","description":"Creating a listing","price":100,"location":"Sweden","category":"Video igre","subcategory":"Kompjuter","attributes":[{"attribute_name":"Platforma (PC, konzola, mobilni)","value":"PC"},{"attribute_name":"Žanr","value":"Action"}]}', 'images': <InMemoryUploadedFile: download (1).jpg (image/jpeg)>}
    -------------------------------------------

    Parsed and merged data: {'user_id': 10, 'title': 'Testing', 'description': 'Creating a listing', 'price': 100, 'location': 'Sweden', 'category': 'Video igre', 'subcategory': 'Kompjuter', 'attributes': [{'attribute_name': 'Platforma (PC, konzola, mobilni)', 'value': 'PC'}, {'attribute_name': 'Žanr', 'value': 'Action'}]}
    -------------------------------------------

    Request data after parsing: {'user_id': 10, 'title': 'Testing', 'description': 'Creating a listing', 'price': 100, 'location': 'Sweden', 'category': 'Video igre', 'subcategory': 'Kompjuter', 'attributes': [{'attribute_name': 'Platforma (PC, konzola, mobilni)', 'value': 'PC'}, {'attribute_name': 'Žanr', 'value': 'Action'}]}
    -------------------------------------------

    -------------------------------------------

    Request data images: [<InMemoryUploadedFile: download (2).jpg (image/jpeg)>, <InMemoryUploadedFile: download (1).jpg (image/jpeg)>]
    -------------------------------------------

    -------------------------------------------

    Mutable data: <QueryDict: {'user_id': [10], 'title': ['Testing'], 'description': ['Creating a listing'], 'price': [100], 'location': ['Sweden'], 'category': ['Video igre'], 'subcategory': ['Kompjuter'], 'attributes': [[{'attribute_name': 'Platforma (PC, konzola, mobilni)', 'value': 'PC'}, {'attribute_name': 'Žanr', 'value': 'Action'}]], 'images': [<InMemoryUploadedFile: download (2).jpg (image/jpeg)>, <InMemoryUploadedFile: download (1).jpg (image/jpeg)>]}>
    -------------------------------------------

    -------------------------------------------

    Serializer data: ListingCreateSerializer(data=<QueryDict: {'user_id': [10], 'title': ['Testing'], 'description': ['Creating a listing'], 'price': [100], 'location': ['Sweden'], 'category': ['Video igre'], 'subcategory': ['Kompjuter'], 'attributes': [[{'attribute_name': 'Platforma (PC, konzola, mobilni)', 'value': 'PC'}, {'attribute_name': 'Žanr', 'value': 'Action'}]], 'images': [<InMemoryUploadedFile: download (2).jpg (image/jpeg)>, <InMemoryUploadedFile: download (1).jpg (image/jpeg)>]}>):
    user_id = IntegerField()
    category = CharField()
    subcategory = CharField(allow_null=True, required=False)
    title = CharField(max_length=1024)
    description = CharField(style={'base_template': 'textarea.html'})
    price = DecimalField(decimal_places=0, max_digits=8)
    location = CharField(allow_blank=True, allow_null=True, max_length=1024, required=False)
    attributes = ListingAttributeInputSerializer(many=True, required=False):
    attribute_name = CharField()
    value = CharField()
    images = ListField(allow_empty=True, child=ImageField(), required=False, write_only=True)


    i also tried to add a listing trough swagger and it worked there, here is how the request body looks like on swagger

    {
    user_id* integer
    title: User id
    category* string
    title: Category
    minLength: 1
    subcategory string
    title: Subcategory
    minLength: 1
    x-nullable: true
    title* string
    title: Title
    maxLength: 1024
    minLength: 1
    description* string
    title: Description
    minLength: 1
    price* string($decimal)
    title: Price
    location string
    title: Location
    maxLength: 1024
    x-nullable: true
    attributes [ListingAttributeInput{
    attribute_name* string
    title: Attribute name
    minLength: 1
    value* string
    title: Value
    minLength: 1

    }]
    images [string($uri)
    readOnly: true]

    }


    any tips, tricks, hints would be very welcome, thanks in advance

    Continue reading...

Compartilhe esta Página