I am pretty new to DRF/Django and want to create an endpoint that returns nested json from multiple models in the format:
{
"site": {
"uuid": "99cba2b8-ddb0-11eb-bd58-237a8c3c3fe6",
"domain_name": "hello.org"
},
"status": "live",
"configuration": {
"secrets": [
{
"name": "SEGMENT_KEY", # Configuration.name
"value": [...] # Configuration.value
},
{
"name": "CONFIG_KEY",
"value": [...]
},
"admin_settings": {
'tier'='trail',
'subscription_ends'='some date',
'features'=[]
}
Here are the models:
class Site(models.Model):
uuid = models.UUIDField(
default=uuid.uuid4,
editable=False,
unique=True)
domain_name = models.CharField(max_length=255, unique=True)
created = models.DateTimeField(editable=False, auto_now_add=True)
modified = models.DateTimeField(editable=False, auto_now=True)
class AdminConfiguration(models.Model):
TRIAL = 'trial'
PRO = 'pro'
TIERS = [
(TRIAL, 'Trial'),
(PRO, 'Professional'),
]
site = models.OneToOneField(
Site,
null=False,
blank=False,
on_delete=models.CASCADE)
tier = models.CharField(
max_length=255,
choices=TIERS,
default=TRIAL)
subscription_ends = models.DateTimeField(
default=set_default_expiration)
features = models.JSONField(default=list)
class Configuration(models.Model):
CSS = 'css'
SECRET = 'secret'
TYPES = [
(CSS, 'css'),
(SECRET, 'secret')
]
LIVE = 'live'
DRAFT = 'draft'
STATUSES = [
(LIVE, 'Live'),
(DRAFT, 'Draft'),
]
site = models.ForeignKey(Site, on_delete=models.CASCADE)
name = models.CharField(max_length=255, blank=False)
type = models.CharField(
max_length=255,
choices=TYPES)
value = models.JSONField(
null=True)
status = models.CharField(
max_length=20,
choices=STATUSES)
Logic behind serializer/viewset to achieve mentioned json:
retrieves lookup_field: uuid
filters query param: Configuration.status (either live or draft
filters AdminConfiguration on site id (something like AdminConfiguration.objects.get(Site.objects.get(uuid))
filters Configuration on type = secret
Here are my serializers:
class SiteSerializer(serializers.ModelSerializer):
class Meta:
model = Site
fields = [
'uuid',
'domain_name'
]
class AdminSerializer(serializers.ModelSerializer):
class Meta:
model = AdminConfiguration
fields = [
'tier',
'subscription_ends',
'features'
]
class ConfigurationSubSerializer(serializers.ModelSerializer):
class Meta:
model = Configuration
fields = [
'name',
'value',
]
class SecretsConfigSerializer(serializers.ModelSerializer):
site = SiteSerializer()
admin_settings = AdminSerializer()
status = serializers.CharField()
configuration = ConfigurationSubSerializer(many=True, source='get_secret_config')
class Meta:
model = Configuration
fields = [
'site',
'admin_settings',
'status'
'configuration'
]
def get_secret_config(self, uuid):
site = Site.objects.get(uuid=self.context['uuid'])
if self.context['status'] == 'live' or self.context['status'] == 'draft':
return Configuration.objects.filter(
site=site,
status=self.context['status'],
type='secret'
)
Viewset:
class SecretsViewSet(viewsets.ReadOnlyModelViewSet):
model = Site
lookup_field = 'uuid'
serializer_class = SecertsConfigSerializer
filter_backends = (DjangoFilterBackend,)
filterset_fields = ['status'] #query params
def get_serializer_context(self):
return {
'status': self.request.GET['status'],
'uuid': self.request.GET['uuid']
}
def get_serializer(self, *args, **kwargs):
kwargs['context'] = self.get_serializer_context()
return CombinedConfigSerializer(*args, **kwargs)
What am I missing to achieve the desired output?
output from django shell:
from site_config.models import Site, AdminConfiguration, Configuration
from site_config.serializers import SecretsConfigSerializer
site = Site.objects.get(id=2)
s = SecretsConfigSerializer(site)
s.data
### OUTPUT ###
AttributeError: Got AttributeError when attempting to get a value for field `site` on serializer `SecretsConfigSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Site` instance.
Original exception text was: 'Site' object has no attribute 'site'.
Why you don't try something more general and build your response separating the serializers like this (maybe you can use the same serializers in somewhere else):
def get(self, request, *args, **kwargs):
resp = {
'site': None,
'status': None,
'configuration': None,
'admin_settings': None,
}
sites = models.Site.objects.all()
resp['site'] = serializers.SitesSerializer(sites, many=True).data
admin_settings = models.AdminConfiguration.objects.all()
resp['admin_settings'] = serializers.AdminConfigurationSerializer(admin_settings, many=True).data
# and so
return Response(resp, status=status.HTTP_200_OK)
** You can try like this. This will also help to findout errors**
def get(self, request, *args, **kwargs):
resp = {
"site": None,
"status": None,
"configuration": None,
"admin_settings": None
}
sites = models.Site.objects.all()
resp['site'] = serializers.SitesSerializer(sites, many=True).data
if resp['site'].is_valid():
admin_settings = models.AdminConfiguration.objects.all()
resp['admin_settings'] = serializers.AdminConfigurationSerializer(admin_settings, many=True).data
if resp['admin_settings'].is_valid():
return Response(resp, status=status.HTTP_200_OK)
return Response(resp['admin_settings'].errors, status=status.HTTP_404_NOT_FOUND)
return Response(resp['site'].errors, status=status.HTTP_404_NOT_FOUND)
Related
I've a model:
class ListingPrice(Timestamps):
price = models.ForeignKey("Price", on_delete=models.CASCADE)
location = models.ForeignKey("location", on_delete=models.CASCADE)
class Meta:
unique_together = ["price", "location"]
class Price(Timestamps):
package = models.ForeignKey("products.Package", on_delete=models.CASCADE)
locations = models.ManyToManyField("location", through="ListingPrice")
price = models.DecimalField(max_digits=11, decimal_places=3)
with a serializer:
class LocationSerializer(serializers.ModelSerializer):
name = LocalizedField()
class Meta:
model = location
fields = ['id', 'name']
class PriceSerializer(serializers.ModelSerializer):
locations = LocationSerializer(many=True, read_only=True)
class Meta:
model = Price
fields = ['package', 'locations', 'price']
def create(self, validated_data):
print("validated_data, validated_data)
and viewset:
class PriceViewSet(ModelViewSet):
queryset = Price.objects.all()
serializer_class = PriceSerializer
ordering = ['id']
permissions = {
"GET": ["view_minimum_listing_price", ],
"POST": ["add_minimum_listing_price", ],
'PUT': ['update_minimum_listing_price', ],
'DELETE': ['delete_minimum_listing_price', ],
}
In testing I'mm using the following:
data = {
"price": 12,
"package": self.package.id,
"is_enabled": False,
"location": self.location
}
response = self.client.post(path=self.url, data=data, format='json')
locations doesn't appear in validated_data?
How to get it to assign locations to the instance with post requests?
I also tried to send it with as ids list, but non works. I only field price, package, is_enabled in validated}_data, but location doesn't appear!
read_only=True means the field will be neglected in request body and will only appear in response body
locations = LocationSerializer(many=True, read_only=True)
so, remove it and you can access locations in validated_data
some context here... I have a Room object that has a list of pictures. Right now I'm stuck at the POST method (create a new room with pictures). But I keep getting the validation error "this field is required".
model
class Room(models.Model):
CONDO = 'condo'
HOUSE = 'house'
OTHER = 'other'
ROOM_TYPES = [(CONDO, 'condo'), (HOUSE, 'house'), (OTHER, 'other')]
name = models.CharField(max_length=50)
price = models.IntegerField()
type = models.CharField(max_length=5, choices=ROOM_TYPES)
location = models.CharField(max_length=100)
info = models.TextField(max_length=500, blank=True)
owner = models.ForeignKey(CustomUser, on_delete=models.CASCADE, related_name='listings', null=True)
start_date = models.DateField()
end_date = models.DateField()
is_available = models.BooleanField(default=True)
include_utility = models.BooleanField(default=False)
allow_pet = models.BooleanField(default=False)
include_parking = models.BooleanField(default=False)
include_furniture = models.BooleanField(default=False)
class RoomPicture(models.Model):
room = models.ForeignKey(Room, on_delete=models.CASCADE, related_name='images', null=True)
image = models.ImageField(upload_to='images/', blank=True, null=True)
class Meta:
unique_together = ['room', 'image']
serializer
class RoomPictureSerializer(serializers.ModelSerializer):
class Meta:
model = RoomPicture
fields =['id','image']
class RoomSerializer(serializers.ModelSerializer):
# images = serializers.ImageField(upload_to='images/', blank=True, null=True)
images = RoomPictureSerializer(many=True)
class Meta:
model = Room
fields = ['id', 'owner', 'name', 'price', 'type', 'location', 'info',
'start_date', 'end_date', 'is_available' , 'include_utility' ,'allow_pet',
'include_parking', 'include_furniture', 'images']
def create(self, validated_data):
images_data = validated_data.pop('images')
room = Room.objects.create(**validated_data)
for image_data in images_data:
RoomPicture.objects.create(room=room, image=image_data["image"])
return room
views
def convert(image):
dict = {}
# dict['id'] = 0
dict['image'] = image
return dict
class RoomViewSet(viewsets.ModelViewSet):
# permission_classes = [
# permissions.IsAuthenticated,
# ]
parser_classes = (MultiPartParser, FormParser)
serializer_class = RoomSerializer
queryset = Room.objects.all()
def create(self, request):
# converts querydict to original dict
dict_data = dict((request.data).lists())
dict_data['images'] = list(map(convert, dict_data['images']))
print(request.data)
query_dict = QueryDict('', mutable=True)
query_dict.update(MultiValueDict(dict_data))
print(query_dict)
serializer = RoomSerializer(data=query_dict)
if serializer.is_valid():
serializer.save(owner=request.user)
return Response(serializer.data)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
What I don't understand is that in my view, I explicitly passed in query_dict which has an 'images' key. But for some reason the validator does not recognize it.
so the request.data looks like this
<QueryDict: { 'name': [ 'The Building'], 'price': [ '900'], 'type': [ 'condo'], 'location': [ '100 Trump Rd'], 'info': [ '1 bedroom + 1 bath / share kitchen & living room'], 'start_date': [ '2021-09-01'], 'end_date': [ '2021-12-31'], 'is_available': [
'true'], 'include_utility': [ 'true'], 'include_parking': [ 'true'], 'include_furniture': [ 'true'], 'allow_pet': [ 'true'], 'images': [<InMemoryUploadedFile: Phillip-Square-Blair-House-Conference-Room.jpg (image/jpeg)>,
<InMemoryUploadedFile: Phillip-Square-Blair-House-Gym.jpg (image/jpeg)>]}>
and the tuned query_dict looks like this
<QueryDict: {'name': ['The Building'], 'price': ['900'], 'type': ['condo'], 'location': ['100 Trump Rd'], 'info': ['1 bedroom + 1 bath / share kitchen & living room'], 'start_date': ['2021-09-01'], 'end_date': ['2021-12-31'], 'is_available': ['true'], 'include_utility': ['true'], 'include_parking': ['true'], 'include_furniture': ['true'], 'allow_pet': ['true'], 'images': [{'image': <InMemoryUploadedFile: Phillip-Square-Blair-House-Conference-Room.jpg (image/jpeg)>}, {'image': <InMemoryUploadedFile: Phillip-Square-Blair-House-Gym.jpg (image/jpeg)>}]}>
whether I pass in serializer = RoomSerializer(data=request.data) or serializer = RoomSerializer(data=query_dict) I always get the same error...
Can somebody help me? Thanks in advance!
The default ModelSerializer implementation does not allow nested serializer data so serializer.is_valid() will return false hence the error.
Even using the drf_writable_nested module doesn't work for it as well.
Based on what you're trying to do, there are 3 options that I can advise:
Write your serializer explicitly with serializers.Serializer though, that will be a lot of code but, it'll satisfy the nesting.
Create separate views and serializers for the RoomPicture model. This way, on your client-side, you'll initially create the Room object then upload the images afterwards.
If you have a specific amount of images for the Room object, you can add image fields to the Room model e.g
class Room(models.Model):
...
image1 = models.ImageField(upload_to='images/', blank=True, null=True)
image2 = models.ImageField(upload_to='images/', blank=True, null=True)
image3 = models.ImageField(upload_to='images/', blank=True, null=True)
...
#property
def images(self):
images_list = []
image_fields = [self.image1, self.image2, self.image3]
for image_count, image_field in enumerate(image_fields, start=1):
if image_field:
image_dict = {'path': image_field.url, 'count': image_count}
images_list.append(image_dict)
return images_list
If the images can be more than 5, you might want to use option 2.
You'll have to add images to your RoomSerializer as a ReadOnlyField(), it'll serialize the data as a list of objects just like what you'll get from using FK.
You'll be able to create and update your Room data with the images altogether.
To delete a particular image, just set the field as null. That's why I added count to the image_dict data to help know which image to update from the client-side.
You should also install django-cleanup to help delete the previous image after an update has been made to any of the image fields.
I have two models, "Blog_model" and "File_model" where "blog_id" of "Blog_model" is the foreign key for "File_Model". The concept is to save multiple files for a single blog. Here is the model structure for reference.
class Blog_model(models.Model):
type = models.CharField(max_length = 50, default = "FOOD")
count = models.PositiveIntegerField(default = 0)
title = models.CharField(max_length = 500, unique = True)
substance = models.CharField(max_length = 5000, default = "")
thumbnail = models.ImageField(upload_to = get_media_file_name, default = "")
text = models.TextField()
create_time = models.DateTimeField(auto_now_add = True)
update_time = models.DateTimeField(auto_now = True)
class File_model(models.Model):
blog_id = models.ForeignKey(Blog_model, on_delete = models.CASCADE)
file_name = models.FileField(upload_to = get_media_file_name)
upload_time = models.DateTimeField(auto_now_add = True)
def __str__(self):
return str(self.file_name)
Now, I want to create a new blog using a single API that will have details of blogs, as well as file names. I am imagining the API structure something like -
{
"type": "FOOD",
"title": "Some Blog",
"Substance": "Some blog about food",
"text": "This is some blog about food",
"thumbnail": <InMemoryUploadedFile: Capture.PNG (image/png)>
"files": [<InMemoryUploadedFile: food1.jpg (image/jpeg)>, <InMemoryUploadedFile: food2.jpg (image/jpeg)>, <InMemoryUploadedFile: food3.jpg (image/jpeg)>]
}
Please suggest how to achieve the goal.
You may suggest a correct API structure also if above mentioned seems to be wrong.
Any suggestion is appreciated.
This is the serializer and view I am using for this purpose.
-----------------------------------
serializers.py
-----------------------------------
class File_modelCreateSerializer(serializers.ModelSerializer):
# upload_time = serializers.DateTimeField(format = date_time_format)
class Meta:
model = File_model
fields = ["file_name"]
class Blog_modelCreateSerializer(serializers.ModelSerializer):
files = File_modelCreateSerializer(many = True, required = False)
class Meta:
model = Blog_model
fields = ["type", "title", "substance", "thumbnail", "text", "files"]
def create(self, validated_data):
# files = validated_data.pop("files") # Getting no key named "files" in validated_data
new_blog = Blog_model.objects.create(**validated_data)
# for f in files:
# File_model.objects.create(blog_id = new_blog, **f)
return new_blog
-----------------------------------
views.py
-----------------------------------
# class Blog_modelCreateView(generics.CreateAPIView):
# serializer_class = Blog_modelCreateSerializer
class Blog_modelCreateView(APIView):
parser_classes = (MultiPartParser, FormParser)
def post(self, request, *args, **kwargs):
blog_serializer = Blog_modelCreateSerializer(data = request.data)
if blog_serializer.is_valid():
blog_serializer.save()
return Response(blog_serializer.data)
else:
return Response(blog_serializer.errors)
Actually, View and Serializer are linked to a model.
But, you can use #action decorator.
See Django REST Framework: Routing for extra actions
If you want to link File serializer to Blog, try this.
class BlogViewSet(ModelViewSet):
def get_serializer(self):
if self.action == 'files':
return FileSerializer
...
#action(url_path='files')
def file(self):
qs = File.objects.all()
...
Just like Youtube, I want to see what categories my post(outfit) is in.
My approach is...
Get whole list of categories belong to user
Get whole list of categories belong to user and the post
compare them and return JSON
While building a Seriailizer, I feel like I'm totally stock. So frustraing... :(
Please feel free to change my approach if you have any better idea.
Expected JSON outputis below
{
...
"categories": [
{
"id": 1,
"name": "asd"
"added": True <- since the post is added to 'asd'
},
{
"id": 2,
"name": "workout"
"added": True
},
...
{
"id": 5,
"name": "bgm"
"added": False <- since the post is not added to 'bgm'
},
]
}
Here is views.py
class OutfitDetailView(generics.RetrieveAPIView):
queryset = Outfit.objects.all()
serializer_class = OutfitDetailSerializer
lookup_field = 'pk'
Here is serializers.py (this is a problem)
class OutfitDetailSerializer(serializers.ModelSerializer):
...
def get_categories(self, obj):
# Right Here! Get whole categories associated with user
all_categories = Category.objects.filter(owner=self.context['request'].user)
# And the categories associated with user AND the post
categories = obj.categories.filter(owner=self.context['request'].user)
return CategorySerializer(categories, many=True).data
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = (
'id',
'name',
'added' <- here I want to add 'added' field but I don't know how.
)
In case you need a model
class Outfit(models.Model):
...
user = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True)
content = models.CharField(max_length=30)
...
class Category(models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, blank=True)
outfits = models.ManyToManyField(Outfit, related_name="categories", blank=True)
main_img = models.ImageField(
upload_to=upload_location_category,
null=True,
blank=True)
...
if i understand you correct, you can try:
class OutfitDetailSerializer(serializers.ModelSerializer):
def get_categories(self, obj):
user = self.context['request'].user
categories = Category.objects.filter(owner=user)
added = categories.extra(select={'added': '1'}).filter(outfits__pk=obj.pk)
added = list(added.values('added', 'name', 'id'))
added_f = categories.extra(select={'added': '0'}).exclude(outfits__pk=obj.pk)
added_f = list(added_f.values('added', 'name', 'id'))
categories = added + added_f
return CategorySerializer(categories, many=True).data
Note in the values you need add all fields you need for the CategorySerializer
class CategorySerializer(serializers.ModelSerializer):
added = serializers.BooleanField()
class Meta:
model = Category
fields = (
'id',
'name',
'added'
)
I have django app that handle restAPIs with following JSON response, also i have ember app with django-ember-adapter. The problem is i can not get related items in the sub route(http://localhost:8000/api/projects/4/tasks) of API. I can get projects by http://localhost:8000/api/projects/4. Am i have to include task in project's django model in API?, I have tried using links in serializer but seems it's not working.
How do i get project's tasks successfully?
json response on http://localhost:8000/api/projects/4 i can get this by this.store.findRecord('project', params.project_id):
{
"id": 4,
"title": "test1 project",
"description": "some project",
"owner": "test1"
}
json response on http://localhost:8000/api/projects/4/tasks:
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 5,
"title": "4th",
"description": "4tg description"
},
{
"id": 6,
"title": "",
"description": ""
}
]
}
django models:
class Project(models.Model):
"""Canara project """
title = models.CharField(max_length=50, blank=True, default='')
created = models.DateTimeField(auto_now_add=True)
description = models.CharField(max_length=255, blank=True)
active = models.BooleanField(default=True)
owner = models.ForeignKey(User, related_name='projects', null=True)
def __unicode__(self):
return self.title
class Task(models.Model):
title = models.CharField(max_length=55, blank=True)
description = models.CharField(max_length=255, blank=True)
created = models.DateTimeField(auto_now_add=True)
due_date = models.DateField(blank=True, null=True)
assign = models.ForeignKey(User, null=True)
project = models.ForeignKey(Project, null=True)
serializer:
class ProjectViewSet(ModelViewSet):
"""
API endpoint for project
"""
authentication_classes = [TokenAuthentication, SessionAuthentication]
permission_classes = (IsAuthenticated, IsOwner)
serializer_class = ProjectSerializer
def get_queryset(self):
"""
Get user's project
"""
return Project.objects.filter(members__member=self.request.user)
def perform_create(self, serializer):
"""
Set user as an owner when create project
"""
serializer.save(owner=self.request.user)
class TaskViewSet(ModelViewSet):
"""
API for task
"""
authentication_classes = [TokenAuthentication, SessionAuthentication]
permission_classes = [IsAuthenticated,]
serializer_class = TaskSerializer
queryset = Task.objects.all()
ember app's project model
export default Model.extend({
title: attr(),
description: attr(),
});
ember app's task model
export default Model.extend({
title: attr(),
description: attr(),
due_date: attr('date'),
assign: belongsTo('user'),
project: belongsTo('project')
});
Update
I have tried embedded record mixin but no luck
in the serializer:
class ProjectSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Project
fields = ('id', 'title', 'description', 'owner', 'tasks')
def create(self, validated_data):
"""
Create project using given validated data
"""
return Project.objects.create(**validated_data)
def update(self, instance, validated_data):
"""
Update project title description
"""
instance.title = validated_data.get('title', instance.title)
instance.description = validated_data.get(
'description', instance.description)
instance.save()
class TaskSerializer(serializers.ModelSerializer):
project = serializers.PrimaryKeyRelatedField(queryset=Project.objects.all())
class Meta:
model = Task
fields = ('id', 'title', 'description', 'due_date', 'assign', 'project')
def create(self, validated_data):
return Task.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.title = validated_data.get('title', instance.title)
instance.description = validated_data.get('description', instance.description)
instance.due_date = validated_data.get('due_date', instance.due_date)
instance.assign = validated_data.get('assign', instance.assign)
i've tried links in this way:
import DRFSerializer from './drf';
export default DRFSerializer.extend({
normalizeFindAllResponse(store, primaryModelClass, payload, id, requestType) {
console.log(payload)
payload.links: {
tasks: tasks
};
});
return this._super(...arguments);
}
});