Handling data for both JSON and FormData in Angular
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