How to include Hyperlinks for nested resources in drf-nested-routers to apply HATEOAS principle? - python-3.x

Context
I have an API in Django REST framework with the following nested resources
/wizard-api/industries/
/wizard-api/industries/<pk>/
/wizard-api/industries/<industry_pk>/sub-industries/
/wizard-api/industries/<industry_pk>/sub-industries/<pk>/
/wizard-api/industries/<industry_pk>/sub-industries/<sub_industry_pk>/details/
/wizard-api/industries/<industry_pk>/sub-industries/<sub_industry_pk>/details/<pk>/
# basenames:
wizard-api:industries-list
wizard-api:industries-detail
wizard-api:sub-industries-list
wizard-api:sub-industries-detail
wizard-api:details-list
wizard-api:details-detail
Here my URLs config using drf-nested-routers:
# Nested Routes
first_level = routers.SimpleRouter()
first_level.register(r'industries', views.IndustryViewSet, basename='industries')
second_level = routers.NestedSimpleRouter(first_level, r'industries', lookup='industry')
second_level.register(r'sub-industries', views.SubIndustryViewSet, basename='sub-industries')
third_level = routers.NestedSimpleRouter(second_level, r'sub-industries', lookup='sub_industry')
third_level.register(r'details', views.SubIndustryDetailsViewSet, basename='abc')
ERD
I want to apply the HATEOAS principle
# endpoint: /wizard-api/industries/1/
# response:
{
"id": 1,
"name": "food and beverage",
"subindustries": "http://127.0.0.1:8000/wizard-api/industries/1/sub-industries/"
}
I made it for the first level using the HyperlinkedIdentityField in the first Serializer
class IndustryModelSerializer(serializers.ModelSerializer):
subindustries = serializers.HyperlinkedIdentityField(
view_name='wizard-api:sub-industries-list',
lookup_url_kwarg='industry_pk'
)
class Meta:
model = Industry
exclude = ['created', 'modified', 'active']
Problem
The problem appears when I try to apply the same logic in the subsequent levels, i.e: generating the url from the sub-industries level to the details level:
/wizard-api/industries/<industry_pk>/sub-industries/<sub_industry_pk>/details/
I tried with the details field in the serializer of the second level (sub-industries):
class SubIndustryModelSerializer(serializers.ModelSerializer):
details = serializers.HyperlinkedIdentityField(
view_name='wizard-api:details-list',
lookup_url_kwarg='industry_pk'
)
class Meta:
model = SubIndustry
exclude = ['created', 'modified', 'active']
Expected
The expected response is:
# endpoint: /wizard-api/industries/1/sub-industries/
# response:
[
{
"id": 1,
"name": "beverage industries",
"details": "http://127.0.0.1:8000/wizard-api/industries/1/sub-industries/1/details/"
},
{
"id": 2,
"name": "food production",
"details": "http://127.0.0.1:8000/wizard-api/industries/1/sub-industries/2/details/"
}
]
Error
But I got the following error:
Could not resolve URL for hyperlinked relationship using view name
"wizard-api:details-list". You may have failed to include the related
model in your API, or incorrectly configured the lookup_field
attribute on this field.

I made some tests and found the solution.
First, the best way to implement the nested routes in your case will be with these HyperlinkedModelSerializer, NestedHyperlinkedModelSerializer.
So the first level should look like this:
class IndustryModelSerializer(HyperlinkedModelSerializer):
subindustries = serializers.HyperlinkedIdentityField(
view_name='wizard-api:sub-industries-list',
lookup_url_kwarg='industry_pk'
)
class Meta:
model = Industry
fields = ['subindustries', 'foo'...]
The main problem in the second level can be fixed like this:
class SubIndustryModelSerializer(NestedHyperlinkedModelSerializer):
parent_lookup_kwargs = {
'industry_pk': 'industry_id'
}
class Meta:
model = SubIndustry
fields = ['foo', 'url', ...]
extra_kwargs = {
'url': {
'view_name': 'wizard-api:details-list',
'lookup_url_kwarg': 'sub_industry_pk'
}
}
Based on your ERD, you must include a parent_kwargs for the proper configuration of nested routes, change the exclude for fields, and add the url parameter.
Hope it works!

Related

Print the output in my desired format using nested serializers?

I am trying to send messages using django-rest framework.I have created serializers for both user and message.I am using django default user model for storing user's details.
I am pasting my model and serializers here:
class MessageModel(models.Model):
message = models.CharField(max_length=100)
created_at = models.DateTimeField(default=now)
updated_at = models.DateTimeField(default=now)
user = models.ForeignKey(User,on_delete=models.CASCADE,related_name='user')
class UserSerializer(ModelSerializer):
class Meta:
model = get_user_model()
fields = ['id','username','email']
class MessageSerializer(ModelSerializer):
created_by = UserSerializer(read_only=True)
class Meta:
model = MessageModel
fields = ['id','message', 'created_at', 'updated_at','user','created_by']
View :
class MessageViewSet(viewsets.ModelViewSet):
authentication_classes = [TokenAuthentication]
permission_classes = [IsAuthenticated]
queryset = MessageModel.objects.all()
serializer_class = MessageSerializer
After creating message,I am getting response like below:
{
"id": 6,
"message": "Lorem ipsum",
"created_at": "2022-11-07T09:21:19.492219Z",
"updated_at": "2022-11-07T09:21:19.492237Z",
"user": 2
}
but my output should looks like in below format,everytime when a new message is created :
{
"id": 102,
"message": "Lorem ipsum",
"created_at": "created time in UTC",
"updated_at": "last updated time in UTC",
"created_by": {
"id": 3,
"username": "testuser",
"email": "test#mail.com",
}
}
In case of any error, return the error message.
The name of the field is user, not created_by, so:
class MessageSerializer(ModelSerializer):
user = UserSerializer(read_only=True)
class Meta:
model = MessageModel
fields = ['id', 'message', 'created_at', 'updated_at', 'user']
or you can specify user as source:
class MessageSerializer(ModelSerializer):
created_by = UserSerializer(read_only=True, source='user')
class Meta:
model = MessageModel
fields = ['id', 'message', 'created_at', 'updated_at', 'created_by']
Note: Models normally have no …Model suffix. Therefore it might be better to rename MessageModel to Message.
Note: It is normally better to make use of the settings.AUTH_USER_MODEL [Django-doc] to refer to the user model, than to use the User model [Django-doc] directly. For more information you can see the referencing the User model section of the documentation.
Note: The related_name=… parameter [Django-doc]
is the name of the relation in reverse, so from the User model to the MessageModel
model in this case. Therefore it (often) makes not much sense to name it the
same as the forward relation. You thus might want to consider renaming the user relation to messages.

How to change Pydantic default __root__ example

So I have this class:
class Table(BaseModel):
__root__: Dict[int, Test]
and I'm using the __root__ since it is a dynamic value but when I go to /redoc (I'm using FastAPI) the example values it defaults to is property1, property2 as in the image below but I wanted the default example to be id_1, id_2 for example. How can I achieve this?
I tried changing the alias and title using the Field method of the library but didn't seem to work like I expected.
Have you tried doing it with the schema customization ?
class QcastTable(BaseModel):
__root__: Dict[int, Test]
class Config:
schema_extra = {
"examples": [
{
"id_1": {
"title": "yourTitle",
"description": "yourDescription",
},
"id_2": {
"title": "yourTitle2",
"description": "yourDescription2",
}
}
]
}

How to resolve multiple querysets in graphene.Union?

I want to create a graphene.Union type of multiple existing types. I am able to resolve it but it is not in my required format.
Schema
from graphene_django import DjangoObjectType
class ageType(DjangoObjectType):
class Meta:
model = age
class ethnicityType(DjangoObjectType):
class Meta:
model = ethnicity
class combinedType(graphene.Union):
class Meta:
types = (ageType, ethnicityType)
class Query(graphene.ObjectType):
defaultPicker = graphene.List(combinedType)
def resolve_defaultPicker(self, info):
items = []
age_q = age.objects.all()
items.extend(age_q)
ethnicity_q = ethnicity.objects.all()
items.extend(ethnicity_q)
return items
The query I am using in the graphql admin:
{
defaultPicker{
... on ageType{
id
age
}
... on ethnicityType{
id
ethnicity
}
}
}
I want to get a output like this:
{
"data": {
"defaultPicker": [
'ageType': [{
"id": "2",
"ethnicity": "American",
"ethnicityFr": "Test"
}],
'ethnicityType': [{
"id": "1",
"familyPlans": "3 kids",
"familyPlansFr": "3 enfants"
}],
]
}
}
I tried many things but couldn't find a way to resolve it.

SQLAlchemy / jsonpatch - how to make patch paths case-insensitive?

I've been trying to find some documentation for jsonpatch==1.16 on how to make PATCH paths case-insensitive. The problem is that:
PATCH /users/123
[
{"op": "add", "path": "/firstname", "value": "Spammer"}
]
Seems to mandate that the DB (MySQL / MariaDB) column is also exactly firstname and not for example Firstname or FirstName. When I change the path in the JSON to /FirstName, which is what the DB column is, then the patch works just fine. But I'm not sure if you are supposed to use CamelCase in the JSON in this case? It seems a bit non-standard.
How can I make jsonpatch at least case-insensitive? Or alternatively, is there some way to insert some mapping in the middle, for example like this:
def users_mapping(self, path):
select = {
"/firstname": "FirstName",
"/lastname": "last_name", # Just an example
}
return select.get(path, None)
Using Python 3.5, SQLAlchemy 1.1.13 and Flask-SQLAlchemy 2.2
Well, the answer is: yes, you can add mapping. Here's my implementation with some annotations:
The endpoint handler (eg. PATCH /news/123):
def patch(self, news_id):
"""Change an existing News item partially using an instruction-based JSON,
as defined by: https://tools.ietf.org/html/rfc6902
"""
news_item = News.query.get_or_404(news_id)
self.patch_item(news_item, request.get_json())
db.session.commit()
# asdict() comes from dictalchemy method make_class_dictable(news)
return make_response(jsonify(news_item.asdict()), 200)
The method it calls:
# news = the db.Model for News, from SQLAlchemy
# patchdata = the JSON from the request, like this:
# [{"op": "add", "path": "/title", "value": "Example"}]
def patch_item(self, news, patchdata, **kwargs):
# Map the values to DB column names
mapped_patchdata = []
for p in patchdata:
# Replace eg. /title with /Title
p = self.patch_mapping(p)
mapped_patchdata.append(p)
# This follows the normal JsonPatch procedure
data = news.asdict(exclude_pk=True, **kwargs)
# The only difference is that I pass the mapped version of the list
patch = JsonPatch(mapped_patchdata)
data = patch.apply(data)
news.fromdict(data)
And the mapping implementation:
def patch_mapping(self, patch):
"""This is used to map a patch "path" or "from" to a custom value.
Useful for when the patch path/from is not the same as the DB column name.
Eg.
PATCH /news/123
[{ "op": "move", "from": "/title", "path": "/author" }]
If the News column is "Title", having "/title" would fail to patch
because the case does not match. So the mapping converts this:
{ "op": "move", "from": "/title", "path": "/author" }
To this:
{ "op": "move", "from": "/Title", "path": "/Author" }
"""
# You can define arbitrary column names here.
# As long as the DB column is identical, the patch will work just fine.
mapping = {
"/title": "/Title",
"/contents": "/Contents",
"/author": "/Author"
}
mutable = deepcopy(patch)
for prop in patch:
if prop == "path" or prop == "from":
mutable[prop] = mapping.get(patch[prop], None)
return mutable

get this JSON representation of your neo4j objects

I want to get data from thisarray of json object :
[
{
"outgoing_relationships": "http://myserver:7474/db/data/node/4/relationships/out",
"data": {
"family": "3",
"batch": "/var/www/utils/batches/d32740d8-b4ad-49c7-8ec8-0d54fcb7d239.resync",
"name": "rahul",
"command": "add",
"type": "document"
},
"traverse": "http://myserver:7474/db/data/node/4/traverse/{returnType}",
"all_typed_relationships": "http://myserver:7474/db/data/node/4/relationships/all/{-list|&|types}",
"property": "http://myserver:7474/db/data/node/4/properties/{key}",
"self": "http://myserver:7474/db/data/node/4",
"properties": "http://myserver:7474/db/data/node/4/properties",
"outgoing_typed_relationships": "http://myserver:7474/db/data/node/4/relationships/out/{-list|&|types}",
"incoming_relationships": "http://myserver:7474/db/data/node/4/relationships/in",
"extensions": {},
"create_relationship": "http://myserver:7474/db/data/node/4/relationships",
"paged_traverse": "http://myserver:7474/db/data/node/4/paged/traverse/{returnType}{?pageSize,leaseTime}",
"all_relationships": "http://myserver:7474/db/data/node/4/relationships/all",
"incoming_typed_relationships": "http://myserver:7474/db/data/node/4/relationships/in/{-list|&|types}"
}
]
what i tried is :
def messages=[];
for ( i in families) {
messages?.add(i);
}
how i can get familes.data.name in message array .
Here is what i tried :
def messages=[];
for ( i in families) {
def map = new groovy.json.JsonSlurper().parseText(i);
def msg=map*.data.name;
messages?.add(i);
}
return messages;
and get this error :
javax.script.ScriptException: groovy.lang.MissingMethodException: No signature of method: groovy.json.JsonSlurper.parseText() is applicable for argument types: (com.tinkerpop.blueprints.pgm.impls.neo4j.Neo4jVertex) values: [v[4]]\nPossible solutions: parseText(java.lang.String), parse(java.io.Reader)
Or use Groovy's native JSON parsing:
def families = new groovy.json.JsonSlurper().parseText( jsonAsString )
def messages = families*.data.name
Since you edited the question to give us the information we needed, you can try:
def messages=[];
families.each { i ->
def map = new groovy.json.JsonSlurper().parseText( i.toString() )
messages.addAll( map*.data.name )
}
messages
Though it should be said that the toString() method in com.tinkerpop.blueprints.pgm.impls.neo4j.Neo4jVertex makes no guarantees to be valid JSON... You should probably be using the getProperty( name ) function of Neo4jVertex rather than relying on a side-effect of toString()
What are you doing to generate the first bit of text (which you state is JSON and make no mention of how it's created)
Use JSON-lib.
GJson.enhanceClasses()
def families = json_string as JSONArray
def messages = families.collect {it.data.name}
If you are using Groovy 1.8, you don't need JSON-lib anymore as a JsonSlurper is included in the GDK.
import groovy.json.JsonSlurper
def families = new JsonSlurper().parseText(json_string)
def messages = families.collect { it.data.name }

Resources