I make a page that lists all the existing vendors and modules that apply to each vendor. Here I need to change the status of modules (active or unactive), and if the module does not exist, but need to make it active then create it. It looks roughly like this.
Vendor1 module1/false module2/true module3/true .....
Vendor2 module1/false module2/true module3/true .....
.....
.....
models.py
class RfiParticipation(models.Model):
vendor = models.ForeignKey('Vendors', models.DO_NOTHING, related_name='to_vendor')
m = models.ForeignKey('Modules', models.DO_NOTHING, related_name='to_modules')
active = models.BooleanField(default=False)
user_id = models.IntegerField()
rfi = models.ForeignKey('Rfis', models.DO_NOTHING, related_name='to_rfi', blank=True, null=True)
timestamp = models.DateTimeField(auto_now=True)
To display it, I use ListCreateAPIView() class and nested serializer
serializer.py
class VendorModulesListManagementSerializer(serializers.ModelSerializer):
to_vendor = RfiParticipationSerializer(many=True)
class Meta:
model = Vendors
fields = ('vendorid', 'vendor_name', 'to_vendor',)
read_only_fields = ('vendorid', 'vendor_name', )
def create(self, validated_data):
validated_data = validated_data.pop('to_vendor')
for validated_data in validated_data:
module, created = RfiParticipation.objects.update_or_create(
rfi=validated_data.get('rfi', None),
vendor=validated_data.get('vendor', None),
m=validated_data.get('m', None),
defaults={'active': validated_data.get('active', False)})
return module
class RfiParticipationSerializer(serializers.ModelSerializer):
class Meta:
model = RfiParticipation
fields = ('pk', 'active', 'm', 'rfi', 'vendor', 'timestamp')
read_only_fields = ('timestamp', )
views.py
class AssociateModulesWithVendorView(generics.ListCreateAPIView):
"""
RFI: List of vendors with participated modules and modules status
"""
permission_classes = [permissions.AllowAny, ]
serializer_class = VendorModulesListManagementSerializer
queryset = Vendors.objects.all()
I have a question about using the create serializer method when sending a POST request.
Now the input format looks like this
{
"to_vendor": [
{
"active": false,
"m": 1,
"rfi": "20R1",
"vendor": 15
}]
}
I.e. the dictionary key for the current code implementation is the list of one dictionary. If I remove " [] " from dict value I got
{
"to_vendor": {
"non_field_errors": [
"Expected a list of items but got type \"dict\"."
]
}
}
And this is the reason why I need to add a for loop in the create method to iterate through the list with just one element. I already have any doubts that I'm doing the right thing. Maybe I chose the wrong implementation way?
But now question is why do I get a mistake?
AttributeError: Got AttributeError when attempting to get a value for field `to_vendor` on serializer `VendorModulesListManagementSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `RfiParticipation` instance.
Original exception text was: 'RfiParticipation' object has no attribute 'to_vendor'.
I would be very grateful for your help and advice!
upd
Get request format data:
[
{
"vendorid": 15,
"vendor_name": "Forest Gamp",
"to_vendor": [
{
"pk": 35,
"active": true,
"m": "Sourcing",
"rfi": "1",
"vendor": 15,
"timestamp": "2020-03-29T08:15:41.638427"
},
{
"pk": 39,
"active": false,
"m": "CLM",
"rfi": "20R1",
"vendor": 15,
"timestamp": "2020-03-29T09:09:03.431111"
}
]
},
{
"vendorid": 16,
"vendor_name": "Test21fd2",
"to_vendor": [
{
"pk": 41,
"active": false,
"m": "SA",
"rfi": "20R1",
"vendor": 16,
"timestamp": "2020-03-30T11:05:16.106412"
},
{
"pk": 40,
"active": false,
"m": "CLM",
"rfi": "20R1",
"vendor": 16,
"timestamp": "2020-03-30T10:40:52.799763"
}
]
}
]
It tries to access to_vendor on your model RfiParticipation and it complains that this property does not exist. related_name refers to the back relation on your Vendors model. So that you can do something like Vendors.to_vendor.all() and fetch all the RfiParticipation instances.
This happens when it tries to validate your input data, so even before it gets to the create function of your serializer.
If you are trying to create new RfiParticipation, why would you use a view that defines queryset = Vendors.objects.all()?
Instead define a view that takes care of creating RfiParticipation, since it seems that you already have a reference to Vendors. If I understand correctly, what you are trying to do is basically batch create RfiParticipation, so make a view that points to these.
I have gone through your implementation, and I suppose you might have used wrong generics class inheritance in views.py.
You should try to replace
class AssociateModulesWithVendorView(generics.ListCreateAPIView):
permission_classes = [permissions.AllowAny, ]
serializer_class = VendorModulesListManagementSerializer
queryset = Vendors.objects.all()
With
class AssociateModulesWithVendorView(generics.CreateAPIView, generics.ListAPIView):
permission_classes = [permissions.AllowAny, ]
serializer_class = VendorModulesListManagementSerializer
queryset = Vendors.objects.all()
You can check below links for reference:
CreateAPIView : https://www.django-rest-framework.org/api-guide/generic-views/#createmodelmixin
ListCreateAPIView : https://www.django-rest-framework.org/api-guide/generic-views/#listcreateapiview
Related
I am looking for the good architecture for my problem. I am using django rest framework for building an API. I receive a list of dict which contains an id and a list of values. The list of values need to be validated according to the id.
Example of my code:
class AttributesSerializer(serializers.Serializer):
id = serializers.PrimaryKeyRelatedField(queryset=Attribute.objects.all(), source="attribute", required=True)
values = serializers.ListField()
def validate(self, validated_data):
attribute = validated_data["attribute"]
values = validated_data["values"]
# This function returns the corresponding field according to attribute
values_child_field = get_values_field(attribute)
self.fields["values"].child = values_child_fields
new_values = self.fields["values"].run_child_validation(values)
set_value(validated_data, "values", new_values)
return validated_data
class BaseObjectApiInputSerializer(serializers.Serializer):
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all()
)
attributes = AttributesSerializer(many=True)
I want to parse json like this:
{
"categorty_id": 42, # Category pk of the baseobject. which defines some constraints about attributes available
"attributes": [
{"id": 124, "values": ["value"]},
{"id": 321, "values": [42]},
{
"id": 18,
"values": [
{
"location": {"type": "Point", "geometry": {...}},
"address": "an address",
}
],
},
]
}
Currently, this code does not work. DRF seems to try to revalidate all values entries for each iteration with each child field. I do not understand why... I guess I could make it work without using this fields["values"] for making the validation and just retrieve the field and use it directly, but i need this field for making the save later.
Do you think my architecture is ok? What is the good way for parsing this type of data with DRF?
EDIT:
Structure of models are complex but a version simplified following:
class Attribute(models.Model):
class DataType(models.TextChoices):
TEXT = "TEXT", _("datatype_text")
INTEGER = "INTEGER", _("datatype_integer")
DATETIME = "DATETIME", _("datatype_datetime")
BOOL = "BOOL", _("datatype_bool")
# Some examples, but there are about 30 items with
# type very complicated like RecurrenceRule (RFC2445)
# or GeoJSON type
label = models.CharField()
category = models.ForeignKey(Category)
attribute_type = models.CharField(choices=DataType.choices)
class AttributeValue(models.Model):
attribute = models.ForeignKey(Attribute)
# a model which represents an object with list of attributes
baseobject = models.ForeignKey(BaseObject)
value = models.TextField()
AttributeValue is like a through table for manytomany relation between BaseObject model and Attribute model.
My JSON represents the list of attribute/values attached to a baseobject.
In fact I don't understand why DRf doesn't allow delegating registration in the child serializers of the parent serializer. This would allow much greater flexibility in code architecture and separation of responsibilities.
EDIT 2 :
My urls.py
router = routers.DefaultRouter()
router.register("baseobjects", BaseObjectViewSet, basename="baseobjects")
I am using the default router and url for DRF viewset.
The view looks like:
class BaseObjectViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated]
authentication_classes = [TokenAuthentication]
def create(self, request, *args, **kwargs):
serializer = BaseObjectApiInputSerializer(
data=request.data
)
if not serializer.is_valid():
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
baseobject: BaseObject = serializer.save()
return Response(
{"results": [{"id": baseobject.pk}]}, status=HTTP_200_OK
)
I think you should use ListField with JSONField as child argument for values field.
validators = {
TinyurlShortener.DataType.TEXT: serializers.CharField(),
TinyurlShortener.DataType.INTEGER: serializers.IntegerField(),
TinyurlShortener.DataType.DATETIME: serializers.DateTimeField(),
TinyurlShortener.DataType.BOOL: serializers.BooleanField(),
}
class AttributesSerializer(serializers.Serializer):
id = serializers.PrimaryKeyRelatedField(queryset=Attribute.objects.all(), source="attribute", required=True)
values = serializers.ListField(
child=serializers.JSONField()
)
def validate(self, attrs):
attribute = attrs.get('id')
field = validators[attribute.attribute_type]
for v in attrs['values']:
field.run_validation(json.loads(v.replace("'", '"')))
return super().validate(attrs)
class BaseObjectApiInputSerializer(serializers.Serializer):
category_id = serializers.PrimaryKeyRelatedField(
queryset=Category.objects.all()
)
attributes = AttributesSerializer(many=True)
I'm working in Django 3.2 and graphene-django 2.15.
I'm still learning how to use Graphene by the way.
Note: I'm not allowed to share all the code so I rewrote it for the purpose of this question. Please notify me if you've found any error unrelated to the question.
I have an Team model which has a Many-to-Many relationship with the default Django Group model:
from django.db import models
from django.contrib.auth.models import Group
class Team(models.Model):
team_id = models.IntegerField(unique=True) # Custom ID not related to Django's pk
name = models.CharField(max_length=255)
groups = models.ManyToManyField(Group, blank=True)
Here is my schema:
import graphene
from django.contrib.auth.models import Group
from graphene_django import DjangoObjectType
from .models import Team
class TeamType(DjangoObjectType):
class Meta:
model = Team
class GroupType(DjangoObjectType):
class Meta:
model = Group
class GroupInput(graphene.InputObjectType):
id = graphene.ID(required=True)
class UpdateTeam(graphene.Mutation):
team = graphene.Field(TeamType)
class Arguments:
team_id = graphene.ID(required=True)
name = graphene.String(required=True)
groups_id = graphene.List(GroupInput, required=True)
def mutate(self, info, team_id, name, groups_id):
team = Team.objects.get(pk=team_id)
team.name = name
team.groups = Group.objects.filter(pk__in=groups_id)
team.save()
return UpdateTeam(team=team)
class TeamMutations(graphene.ObjectType):
update_team = UpdateTeam.Field()
class Mutation(TeamMutations, graphene.ObjectType):
pass
schema = graphene.schema(query=Query, mutation=Mutation)
When I perform this query:
mutation{
updateTeam(
teamId: 65961826547,
name: "My Team Name",
groupsId: [{id: 1}, {id: 2}]
) {
team {
team_id,
name,
groups {
name,
}
}
}
}
I get this error:
{
"errors": [
{
"message": "Field 'id' expected a number but got {'id': '1'}.",
"locations": [
{
"line": 2,
"column": 3
}
],
"path": [
"updateTeam"
]
}
],
"data": {
"updateTeam": null
}
}
I don't really understand how Many-to-may relationships are managed by Graphene.
Does someone has a solution and some explanations for me? Thanks a lot.
I found the solution.
At first, I thought there was something related to graphene, especially these InputObjectTypes, I didn't get correctly.
But the issue is actually very simple.
The GroupInput is expecting a single value, which it an ID.
class GroupInput(graphene.InputObjectType):
id = graphene.ID(required=True)
Because I put it in graphene.List(), I'm now expecting a list of ID's:
class UpdateTeam(graphene.Mutation):
...
class Arguments:
...
groups_id = graphene.List(GroupInput, required=True)
But in my API call, instead of giving an actual list, I gave a dict:
mutation{
updateTeam(
...
groupsId: [{id: 1}, {id: 2}]
) {
...
}
So this works:
mutation{
updateTeam(
...
groupsId: [1, 2]
) {
...
}
Note 1:
graphene.ID also accepts ids given as strings:
groupsId: ["1", "2"]
Note 2:
I actually removed my GroupInput and put the graphene.ID field directly in the graphene.List:
class UpdateTeam(graphene.Mutation):
...
class Arguments:
...
groups_id = graphene.List(graphene.ID, required=True)
Right now with the current Views functions I am getting the data given below:
{"item": "zxmnb",
"category": "zxc",
"price": "zxc",
"restaurant": 1}
Here is my views file:
class RestaurantMenuView(generics.RetrieveAPIView):
lookup_field = 'item'
serializer_class = MenuSerializer
def get_queryset(self):
return Menu.objects.all()
But the issue is I want the data to be in a format as:
{"restaurant": "name"
"item":"some item",
"category": "some category",
"price": "some price"
}
I want to mention that Restaurant is another model in my models class,Now I know that if I use restaurant I will only get the pk. But what I want is the JSON to be displayed like that.
You need to modify your MenuSerializer. Specifically, you need to change the restaurant field to be a CharField and also provide a source attribute. Something like the following:
class MenuSerializer(serializers.ModelSerializer):
restaurant = serializers.CharField(source='restaurant.name')
# ... other stuff in the serializer
Here, I am assuming that your Restaurant model has a name field.
You can read more about Serializer fields here: https://www.django-rest-framework.org/api-guide/fields/
Why don't you redefine the to_representation() function. Something like this:
class MenuSerializer(serializers.ModelSerializer):
def to_representation(self, obj):
restaurants = RestaurantSerializer(instance=obj,
context=self.context).data
data = []
for restaurant in restaurants:
data.append(
{
"restaurant": {
"item": obj.item,
"category": obj.category,
"price": obj.price,
"name": restaurant.name,
}
}
)
return data
Without looking at your models or why you want restaurant in there, I added the for loop in order to show you that you can pretty much access any of the data in your to_representation() and put it in any sort of format you want. I use this when I'm trying to render my JSON objects into XML in a specific way. Hope this helps.
Also check out the documentation:
https://www.django-rest-framework.org/api-guide/serializers/#overriding-serialization-and-deserialization-behavior
Another solution that you could consider is adding a Foreign Key on your Menu model back to the Restaurant, then you could define your serializer like this:
class RestaurantSerializer(serializers.ModelSerializer):
menu = MenuViewSerializer(read_only=True, many=True)
class Meta:
model = Restaurant
fields = [
"id",
"name",
"menu",
]
extra_kwargs = {
"menu": {"read_only": True},
}
Im trying to make a django-rest-framework serializer where a related model can be linked by list instead of by detail
class Device(models.Model):
device_id = models.CharField(max_length=100)
class Log(models.Model):
device = models.ForeignKey(Device, related_name='logs')
class DeviceSerializer(serializers.HyperlinkedModelSerializer):
logs = serializers.HyperlinkedRelatedField(many=True, view_name='log-detail')
class Meta:
model = Device
fields = ('logs', 'url', 'device_id')
If I navigate to 'api/devices' is will return a list of devices like so:
[
{
"device_id": "12345"
"url": "http://localhost/api/devices/12345"
"logs": [
"http://localhost/api/logs/1",
"http://localhost/api/logs/2"
"http://localhost/api/logs/3"
"http://localhost/api/logs/4"
]
}
]
There can be many many logs. I don't want to write custom logic for pagination of logs inside the Device model, that should be handled by a Logs view.
I would like to have a result like this:
[
{
"device_id": "12345"
"url": "http://localhost/api/devices/12345"
"logs": [
"http://localhost/api/logs?device_id=12345"
]
}
]
And a api consumer can just navigate to the provided link to get the device logs.
It seems that serializer relations don't handle this use case. Can someone point me in the most framework idiomatic way to achieve this.
There is one of the solutions:
Create a method (or property) in the Device model called, for example, get_logs_url, that returned url for device logs:
class Device(models.Model):
...
get_logs_url(self):
# First parameter is the url name, 'device-logs' for example
return reverse('device-logs', kwargs={'device_id': self.device_id})
Add field logs_url to DeviceSerializer
class DeviceSerializer(serializers.ModelSerializer):
...
logs_url = serializers.CharField(source='get_logs_url', read_only=True)
Then the result would be like this
[
{
"device_id": "12345"
"logs": "/api/logs?device_id=12345"
}
]
If you want absolute url in result then your serializer should look something like this
class DeviceSerializer(serializers.ModelSerializer):
logs_url = serializer.SerializerMethodField()
def get_logs_url(self, device):
return self.context['request'].build_absolute_uri(device.get_logs_url())
Then the result would be like this
[
{
"device_id": "12345"
"logs": "http://localhost/api/logs?device_id=12345"
}
]
I have a Django model that is like this:
class WindowsMacAddress(models.Model):
address = models.TextField(unique=True)
mapping = models.ForeignKey('imaging.WindowsMapping', related_name='macAddresses')
And two serializers, defined as:
class WindowsFlatMacAddressSerializer(serializers.Serializer):
address = serializers.Field()
class WindowsCompleteMappingSerializer(serializers.Serializer):
id = serializers.Field()
macAddresses = WindowsFlatMacAddressSerializer(many=True)
clientId = serializers.Field()
When accessing the serializer over a view, I get the following output:
[
{
"id": 1,
"macAddresses": [
{
"address": "aa:aa:aa:aa:aa:aa"
},
{
"address": "bb:bb:bb:bb:bb:bb"
}
],
"clientId": null
}
]
Almost good, except that I'd prefer to have:
[
{
"id": 1,
"macAddresses": [
"aa:aa:aa:aa:aa:aa",
"bb:bb:bb:bb:bb:bb"
],
"clientId": null
}
]
How can I achieve that ?
Create a custom serializer field and implement to_native so that it returns the list you want.
If you use the source="*" technique then something like this might work:
class CustomField(Field):
def to_native(self, obj):
return obj.macAddresses.all()
I hope that helps.
Update for djangorestframework>=3.9.1
According to documentation, now you need override either one or both of the to_representation() and to_internal_value() methods. Example
class CustomField(Field):
def to_representation(self, value)
return {'id': value.id, 'name': value.name}
Carlton's answer will work do the job just fine. There's also a couple of other approaches you could take.
You can also use SlugRelatedField, which represents the relationship, using a given field on the target.
So for example...
class WindowsCompleteMappingSerializer(serializers.Serializer):
id = serializers.Field()
macAddresses = serializers.SlugRelatedField(slug_field='address', many=True, read_only=True)
clientId = serializers.Field()
Alternatively, if the __str__ of the WindowsMacAddress simply displays the address, then you could simply use RelatedField, which is a basic read-only field that will give you a simple string representation of the relationship target.
# models.py
class WindowsMacAddress(models.Model):
address = models.TextField(unique=True)
mapping = models.ForeignKey('imaging.WindowsMapping', related_name='macAddresses')
def __str__(self):
return self.address
# serializers.py
class WindowsCompleteMappingSerializer(serializers.Serializer):
id = serializers.Field()
macAddresses = serializers.RelatedField(many=True)
clientId = serializers.Field()
Take a look through the documentation on serializer fields to get a better idea of the various ways you can represent relationships in your API.