Serializing MPTT Tree with Django REST Framework - api

I searched for a similar question without success.. So, i am working on a website in which i am using django-mptt to organize categories. My Model looks like:
class Category(MPTTModel):
parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, related_name='children')
name = models.CharField(max_length=90)
slug = models.SlugField(unique=True)
_full_slug_separator = '/'
#property
def url(self):
names = [category.name for category in self.get_ancestors(include_self=True)]
return self._full_slug_separator.join(names)
I defined the CategorySerializer as bellow:
class CategorySerializer(serializers.ModelSerializer):
children = serializers.SerializerMethodField()
class Meta:
model = Category
fields = ('name', 'url', 'children')
def get_children(self, obj):
return CategorySerializer(obj.get_children(), many=True).data
# views.py
class CategoryList(generics.ListAPIView):
queryset = Category.objects.root_nodes()
serializer_class = CategorySerializer
The question is how can i:
1. have the 'url' data included in leaf nodes only.
2. have the 'children' data included in non leaf nodes only.
Here is an example of the output I am looking for
[
{
"title":"root node",
"children":[
{
"title":"leaf node",
"url":"link"
},
{
"title":"non leaf node",
"children":[
{
"title":"leaf node",
"url":"link"
}
]
},
{
"title":"non leaf node",
"children":[
{
"title":"non leaf node",
"children":[
{
"title":"leaf node",
"url":"link"
}
]
}
}
]
},
{
"title":"root node",
"url":"link"
}
]
Also i want to know if there is a good way for generating the 'url' to reduce queries
And thanks for any help in advance.

I ran into the same problem and found this question. But there were no answers. So, I am posting how I managed to do it for anyone who may need it in the future. This is most likely not the best solution but it works.
My models.py:
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
class Category(MPTTModel):
name = models.CharField(max_length=100, null=False, blank=False, verbose_name=_("category name"), help_text=_("format: required, max_length=100"))
slug = models.SlugField(max_length=150, null=False, blank=False, editable=False, verbose_name=_("category url"), help_text=_("format: required, letters, numbers, underscore or hyphen"))
parent = TreeForeignKey("self", on_delete=models.SET_NULL, related_name="children", null=True, blank=True, verbose_name=_("parent category"), help_text=_("format: not required"))
class MPTTMeta:
order_insertion_by = ['name']
class Meta:
verbose_name = _('product category')
verbose_name_plural = _('product categories')
def save(self, *args, **kwargs):
self.slug = slugify(self.name)
super(Category, self).save(*args, **kwargs)
def __str__(self):
return self.name
My serializers.py:
from rest_framework import serializers
class SubCategorySerializer(serializers.ModelSerializer):
"""Serializer for lowest level category that has no children."""
parent = serializers.SerializerMethodField(source='get_parent')
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'parent', ]
def get_parent(self, obj):
if obj.parent:
return obj.parent.name
class CategorySerializer(serializers.ModelSerializer):
"""Serializer for category."""
parent = serializers.SerializerMethodField(source='get_parent')
children = serializers.SerializerMethodField(source='get_children')
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'parent', 'children', ]
def get_parent(self, obj):
if obj.parent:
return obj.parent.name
def get_children(self, obj):
if obj.children.exists():
children = [child for child in obj.children.all()]
children_with_children = [child for child in children if child.children.exists()]
children_without_children = [child for child in children if not child.children.exists()]
if children_with_children:
return CategorySerializer(children_with_children, many=True).data
if children_without_children:
return SubCategorySerializer(children_without_children, many=True).data
This way children is included in non-leaf nodes only.
Here is an example of how the data looks:
{
"count": 2,
"next": null,
"previous": null,
"results": [
{
"id": 4,
"name": "Clothes",
"slug": "clothes",
"parent": null,
"children": [
{
"id": 5,
"name": "Men's Clothes",
"slug": "mens-clothes",
"parent": "Clothes",
"children": [
{
"id": 7,
"name": "Men's Jeans",
"slug": "mens-jeans",
"parent": "Men's Clothes"
}
]
},
{
"id": 6,
"name": "Women's Clothes",
"slug": "womens-clothes",
"parent": "Clothes",
"children": [
{
"id": 8,
"name": "Women's Jeans",
"slug": "womens-jeans",
"parent": "Women's Clothes"
}
]
}
]
},
{
"id": 1,
"name": "Technology",
"slug": "technology",
"parent": null,
"children": [
{
"id": 3,
"name": "Laptop",
"slug": "laptop",
"parent": "Technology"
},
{
"id": 2,
"name": "Mobile Phone",
"slug": "mobile-phone",
"parent": "Technology"
}
]
}
]
}

Related

Django Rest Framework to receive specific product by field

I am currently making an API to return JSON for a product.
Currently using Django rest framework, I have successfully implemented the API to view all products via
path/api/products
to show a JSON of all products:
HTTP 200 OK
Allow: GET, POST, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
[
{
"url": "http://127.0.0.1:8000/api/products/1/",
"id": 1,
"brand": "Mars",
"name": "Barres",
"barcode": 5000159366168,
"category": "Snacks, Snacks sucrés, Cacao et dérivés, Confiseries, Barres, Confiseries chocolatées, Barres chocolatées, Barres chocolatées au caramel",
"allergens": "gluten,milk,soybeans",
"weight": 540.0,
"quantity": 1,
"footprint": 2.28655779803366e-06,
"perishable": false
},
{
"url": "http://127.0.0.1:8000/api/products/2/",
"id": 2,
"brand": "Twix",
"name": "Twix",
"barcode": 5000159366267,
"category": "Snacks, Snacks sucrés, Confiseries, Barres",
"allergens": "gluten,nuts",
"weight": 600.0,
"quantity": 1,
"footprint": 0.0,
"perishable": false
},
{
"url": "http://127.0.0.1:8000/api/products/3/",
"id": 3,
"brand": "Twix",
"name": "Twix salted caramel",
"barcode": 5000159528955,
"category": "Biscuits et gâteaux, Biscuit chocolat",
"allergens": "caramel, choclate, wheat",
"weight": 46.0,
"quantity": 1,
"footprint": 0.0,
"perishable": false
}
]
However, I would like to be able to receive the JSON object of only one product by barcode:
for example
path/api/products/5000159366168
or
path/api/products/?barcode=5000159366168
to return only the product that matches the barcode:
{
"url": "http://127.0.0.1:8000/api/products/1/",
"id": 1,
"brand": "Mars",
"name": "Barres",
"barcode": 5000159366168,
"category": "Snacks, Snacks sucrés, Cacao et dérivés, Confiseries, Barres, Confiseries chocolatées, Barres chocolatées, Barres chocolatées au caramel",
"allergens": "gluten,milk,soybeans",
"weight": 540.0,
"quantity": 1,
"footprint": 2.28655779803366e-06,
"perishable": false
}
Here is my code:
my views.py:
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
my serializers.py:
from app1.models import Product
from rest_framework import serializers
class ProductSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Product
fields = ["url","id", "brand", "name", "barcode","category","allergens", "weight", "quantity", "footprint","perishable"]
my urls.py:
router = routers.DefaultRouter()
router.register(r'products', views.ProductViewSet)
urlpatterns = [
...
path('api/', include(router.urls)),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]
You need to override get_queryset() method to filter your data with your barcode or any other filter you want.
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def get_queryset(self):
queryset = super().get_queryset()
barcode= self.request.query_params.get('barcode', None)
if barcode:
queryset = queryset.filter(barcode=barcode)
return queryset
Notice: How I added if condition for barcode that only if barcode variable have some data in it, filter it otherwise simply return the normal queryset.
With help from Asim Ejaz
Here is my working View.py class
class ProductViewSet(viewsets.ModelViewSet):
queryset = Product.objects.all()
serializer_class = ProductSerializer
def get_queryset(self):
super(ProductViewSet, self).get_queryset()
barcode = self.request.query_params.get('barcode', None)
print("Barcode = ", barcode)
print(self)
queryset = Product.objects.all()
if barcode:
queryset = Product.objects.filter(barcode=barcode)
return queryset
Notice: barcode = self.request
as request is not previously defined
If you too run into this error, ensure the request is typed in properly, in this case it should be, http://127.0.0.1:8000/api/products/?barcode=5000159366168

Getting testing error " Test data contained a dictionary value for key 'category'"

Writing test for a little API. Test for GET method working, but for create an error is being called. What could be the problem? i may guess the wrong data format is using.
class CoursesTest(APITestCase):
def setUp(self):
self.course_url = reverse('course-group')
User.objects.create(username='test111', password='123456')
def test_courses_post(self):
data = {
"name": "Blasssbla",
"description": "blabla",
"logo": "img",
"category": {
"name": "Baling",
"imgpath": "img"
},
"contacts": [
{
"status": 1
}
],
"branches": [
{
"latitude": "2131ssss2321",
"longitude": "12321321",
"address": "Osssssh"
}
]
}
self.response = self.client.post(self.course_url, data)
self.assertEqual(self.response.status_code, status.HTTP_201_CREATED)
Error:
AssertionError: Test data contained a dictionary value for key 'category', but multipart uploads do not support nested data. You may want to consider using format='json' in this test case.
If your test data is JSON, you should add format="json" to self.client.post.
class CoursesTest(APITestCase):
def setUp(self):
self.course_url = reverse('course-group')
User.objects.create(username='test111', password='123456')
def test_courses_post(self):
data = {
"name": "Blasssbla",
"description": "blabla",
"logo": "img",
"category": {
"name": "Baling",
"imgpath": "img"
},
"contacts": [
{
"status": 1
}
],
"branches": [
{
"latitude": "2131ssss2321",
"longitude": "12321321",
"address": "Osssssh"
}
]
}
self.response = self.client.post(self.course_url, data, format="json")
self.assertEqual(self.response.status_code, status.HTTP_201_CREATED)

How to returning only as string column in json response object from eloquent relationship using eager loading

I'm trying to figure out how to eager load data as a string instead of array object from a related table.
I have 3 models and here are the relations
Product.php
class Product extends Model
{
public function ProductTag()
{
return $this->hasMany(ProductTag::class);
}
public function Category()
{
return $this->belongsTo(Category::class);
}
}
ProductTag.php
class ProductTag extends Model
{
public function Product()
{
return $this->belongsTo(Product::class);
}
}
Category.php
class Category extends Model
{
public function Product()
{
return $this->hasMany(Product::class);
}
}
I've tried doing it like so:
public function tag(){
return $this->hasMany(ProductTag::class)
->selectRaw('GROUP_CONCAT(tag) as tag,id')
->groupBy('id');
}
public static function list(){
$result = Category::with(['Product'=>function($q){
$q->with(['tag'=>function($q1){
$q1->first();
}]);
}])->get();
}
Here is the reponse:
{
"data": {
"categories": [
{
"id": 1,
"category": "test 1",
"product": [
{
"id": 46,
"name": "test prod 1",
"tag": []
},
{...}
]
},
{
"id": 2,
"category": "test 2",
"product": [
{
"id": 45,
"name": "test prod 2",
"tag": [
{
"product_tag": "Test1, test12, test123"
}
]
},
{...}
]
},
{
"id": 3,
"category": "test 3",
"product": []
}
]
}
}
The Response is as expected except tag array so, instead of an array named "tag" can I get "product_tag" within the "product" array
"product": [
{
"id": 45,
"name": "test prod 2",
"tag": [
{
"product_tag": "Test1, test12, test123"
}
]
}
]
Here is what I want it to look like:
"product": [
{
"id": 45,
"name": "test prod 2",
"product_tag": "Test1, test12, test123"
}
]
Is it possible and any smart way of doing this in Laravel using Eloquent?
Simple :)
Btw, if you can - rename product_tag in response to tag_line or same - it's not right way to take same name for relation and mutator.
class Product extends Model
{
public function getTagLineAttribute()
{
//tag - is "name" field in ProductTag
return $this->ProductTag->pluck('tag')->implode(',');
}
public function ProductTag()
{
return $this->hasMany(ProductTag::class);
}
public function Category()
{
return $this->belongsTo(Category::class);
}
}
//eager loading with tags
Product::with('ProductTag')->find(..)->tagLine;

DRF: how to join many-to-many field into a string in json

models.py
class SalesPerson(models.Model):
name = models.CharField(max_length=50)
office = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True)
class Project(models.Model):
title = models.CharField(max_length=50)
leader = models.ForeignKey(SalesPerson, on_delete=models.SET_NULL, null=True,related_name='leader')
location = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True)
product = models.ManyToManyField(Product)
sales_person = models.ManyToManyField(SalesPerson)
serializers.py
class ProjectSerializer(serializers.ModelSerializer):
leader_name= serializers.ReadOnlyField(source='leader.name')
location_name= serializers.ReadOnlyField(source='location.name')
product = serializers.SlugRelatedField(read_only=True, slug_field='name', many=True)
sales_person = serializers.SlugRelatedField(read_only=True, slug_field='name', many=True)
class Meta:
model = Project
fields = ('id', 'title', 'leader_name', 'location_name', 'product', 'sales_person')
class SPSerializer(serializers.ModelSerializer):
projects = ProjectSerializer(many=True, read_only=True, source='project_set')
office_name= serializers.ReadOnlyField(source='office.city')
class Meta:
model = SalesPerson
fields = ('id', 'name', 'office_name', 'email', 'projects')
result:
{
"id": 2,
"name": "Angela",
"office_name": "NSW Sydney",
"email": "angela#angela.com",
"projects": [
{
"id": 1,
"title": "Mall Orchid",
"leader_name": "Boris",
"product": [
"Split wall mounted"
],
"sales_person": [
"Angela",
"Boris",
"David"
],
},
{
"id": 6,
"title": "Mall Petunia",
"leader_name": "Boris",
"product": [
"Split duct"
],
"sales_person": [
"Angela",
"Boris",
"David"
],
},
]
},
I am going to consume the json using react native
I know how to iterate over "projects"
However, I want to avoid iterating over "sales_person" to make rendering the array simpler
So I am sure I have to make the sales_person into a string but after googling for many hours today, I can't find an answer
I am hoping to do this in Django Rest Framework and not in Expo React Native, if possible
So in short, I want to make the result like this:
{
"id": 2,
"name": "Angela",
"office_name": "NSW Sydney",
"email": "angela#angela.com",
"projects": [
{
"id": 1,
"title": "Mall Orchid",
"leader_name": "Boris",
"product": [
"Split wall mounted"
],
"sales_person": "Angela", "Boris", "David",
},
{
"id": 6,
"title": "Mall Petunia",
"leader_name": "Boris",
"product": [
"Split duct"
],
"sales_person": "Angela", "Boris", "David",
},
]
},
Thank you so much for your help.
I've found the answer
I am writing it for my own future reference
class ProjectSerializer(serializers.ModelSerializer):
leader_name= serializers.ReadOnlyField(source='leader.name')
location_name= serializers.ReadOnlyField(source='location.city')
product = serializers.SlugRelatedField(read_only=True, slug_field='name', many=True)
sales_person = serializers.SerializerMethodField('get_sales_person')
class Meta:
model = Project
fields = ('id', 'title', 'leader_name', 'location_name', 'product', 'sales_person')
def get_sales_person(self, obj):
return ', '.join([sales_person.name for sales_person in obj.sales_person.all()])

Transform JSON response with lodash

I'm new in lodash (v3.10.1), and having a hard time understanding.
Hope someone can help.
I have an input something like this:
{
{"id":1,"name":"Matthew","company":{"id":1,"name":"abc","industry":{"id":5,"name":"Medical"}}},
{"id":2,"name":"Mark","company":{"id":1,"name":"abc","industry":{"id":5,"name":"Medical"}}},
{"id":3,"name":"Luke","company":{"id":1,"name":"abc","industry":{"id":5,"name":"Medical"}}},
{"id":4,"name":"John","company":{"id":1,"name":"abc","industry":{"id":5,"name":"Medical"}}},
{"id":5,"name":"Paul","company":{"id":1,"name":"abc","industry":{"id":5,"name":"Medical"}}}
];
I would like to output this or close to this:
{
"industries": [
{
"industry":{
"id":5,
"name":"Medical",
"companies": [
{
"company":{
"id":1,
"name":"abc",
"employees": [
{"id":1,"name":"Matthew"},
{"id":2,"name":"Mark"},
{"id":3,"name":"Luke"},
{"id":4,"name":"John"},
{"id":5,"name":"Paul"}
]
}
}
]
}
}
]
}
Here's something that gets you close to what you want. I structured the output to be an object instead of an array. You don't need the industries or industry properties in your example output. The output structure looks like this:
{
"industry name": {
"id": "id of industry",
"companies": [
{
"company name": "name of company",
"id": "id of company",
"employees": [
{
"id": "id of company",
"name": "name of employee"
}
]
}
]
}
}
I use the _.chain function to wrap the collection with a lodash wrapper object. This enables me to explicitly chain lodash functions.
From there, I use the _.groupBy function to group elements of the collection by their industry name. Since I'm chaining, I don't have to pass in the array again to the function. It's implicitly passed via the lodash wrapper. The second argument of the _.groupBy is the path to the value I want to group elements by. In this case, it's the path to the industry name: company.industry.name. _.groupBy returns an object with each employee grouped by their industry (industries are keys for this object).
I then do use _.transform to transform each industry object. _.transform is essentially _.reduce except that the results returned from the _.transform function is always an object.
The function passed to the _.transform function gets executed against each key/value pair in the object. In the function, I use _.groupBy again to group employees by company. Based off the results of _.groupBy, I map the values to the final structure I want for each employee object.
I then call the _.value function because I want to unwrap the output collection from the lodash wrapper object.
I hope this made sense. If it doesn't, I highly recommend reading Lo-Dash Essentials. After reading the book, I finally got why lodash is so useful.
"use strict";
var _ = require('lodash');
var emps = [
{ "id": 1, "name": "Matthew", "company": { "id": 1, "name": "abc", "industry": { "id": 5, "name": "Medical" } } },
{ "id": 2, "name": "Mark", "company": { "id": 1, "name": "abc", "industry": { "id": 5, "name": "Medical" } } },
{ "id": 3, "name": "Luke", "company": { "id": 1, "name": "abc", "industry": { "id": 5, "name": "Medical" } } },
{ "id": 4, "name": "John", "company": { "id": 1, "name": "abc", "industry": { "id": 5, "name": "Medical" } } },
{ "id": 5, "name": "Paul", "company": { "id": 1, "name": "abc", "industry": { "id": 5, "name": "Medical" } } }
];
var result = _.chain(emps)
.groupBy("company.industry.name")
.transform(function(result, employees, industry) {
result[industry] = {};
result[industry].id = _.get(employees[0], "company.industry.id");
result[ industry ][ 'companies' ] = _.map(_.groupBy(employees, "company.name"), function( employees, company ) {
return {
company: company,
id: _.get(employees[ 0 ], 'company.id'),
employees: _.map(employees, _.partialRight(_.pick, [ 'id', 'name' ]))
};
});
return result;
})
.value();
Results from your example are as follows:
{
"Medical": {
"id": 5,
"companies": [
{
"company": "abc",
"id": 1,
"employees": [
{
"id": 1,
"name": "Matthew"
},
{
"id": 2,
"name": "Mark"
},
{
"id": 3,
"name": "Luke"
},
{
"id": 4,
"name": "John"
},
{
"id": 5,
"name": "Paul"
}
]
}
]
}
}
If you ever wanted the exact same structure as in the questions, I solved it using the jsonata library:
(
/* lets flatten it out for ease of accessing the properties*/
$step1 := $ ~> | $ |
{
"employee_id": id,
"employee_name": name,
"company_id": company.id,
"company_name": company.name,
"industry_id": company.industry.id,
"industry_name": company.industry.name
},
["company", "id", "name"] |;
/* now the magic begins*/
$step2 := {
"industries":
[($step1{
"industry" & $string(industry_id): ${
"id": $distinct(industry_id)#$I,
"name": $distinct(industry_name),
"companies": [({
"company" & $string(company_id): {
"id": $distinct(company_id),
"name": $distinct(company_name),
"employees": [$.{
"id": $distinct(employee_id),
"name": $distinct(employee_name)
}]
}
} ~> $each(function($v){ {"company": $v} }))]
}
} ~> $each(function($v){ {"industry": $v} }))]
};
)
You can see it in action on the live demo site: https://try.jsonata.org/VvW4uTRz_