Django field not passing through serializer - python

Using the Django REST Framework 2.2, I have a Person model as follows in models.py::
class Person(models.Model):
id = models.CharField(max_length = 20, primary_key = True, blank = True)
name = Models.CharField(max_length = 1024, blank = True)
values = {}
#staticmethod
def create_person(personData):
person = Person(
name = personData.get("name", "Unknown"),
values = personData.get("values", {}),
)
return person
All data is stored in a Firestore database for saving and retrieving data via the REST API. Before new entries are made into the database, a serializer is used to validate incoming POST data.
The route /person takes POST request data and runs it by the PersonCreateSerializer in
views.py:
def create_person(request):
"""
Route: /person
Method: POST
"""
try:
print(request.data)
# Above print outputs:
# <QueryDict: {'name': ['John Doe'], 'values': ['{ "height": 180 }']}>
serializer = PersonCreateSerializer(data = request.data)
serializer.is_valid(raise_exception = True)
person = Person.create_person(request.data)
...
except APIException as exception:
return JsonResponse(exception.APIError, status = exception.status)
serializers.py:
class PersonCreateSerializer(CreateModelSerializer):
class Meta:
model = Person
fields = "__all__"
def validate(self, data):
print(data)
# Above print outputs:
# OrderedDict([('name', 'John Doe')])
# Notice missing 'values' field.
if not data.get("values"): # Ensure we have a values field within the data.
raise APIException("ERROR_MISSING_FIELD", "Missing required field 'values'.", 400)
return data
The problem is however any value provided for the values dictionary is discarded when the serializer validate() function receives it.
POST Payload:
My question is why is the dictionary received from the POST request not received by the serializer so it can be parsed? What is the correcy way to create dictionary fields in Django?
Sent to Serializer:
<QueryDict: {'name': ['John Doe'], 'values': ['{ "height": 180 }']}>
Received by Serializer:
OrderedDict([('name', 'John Doe')])
The problem with JSONField and HStoreField
I have looked at alternatives mentioned such as HStoreField and JSONField however this data is being stored in a Firestore database and the key-value association needs to be retained rather than it being stored as a plain JSON string.
Because the data is being stored in Firestore, the structure of the dictionary array needs to be retained as a map in the database, this allows it to be indexed and queried with Firestore queries.
If we use JSONField, this simply converts the value to a string and removes this functionality.

I believe it is because values is not a field. It is just a class variable.
Serializer filters the data by fields you have pointed to. And you pointed to __all__ which means all fields in model.
You can try:
fields = ['name', 'values']
And if it didn't work, make a function and pass it "as a field":
# models.py
class Person(models.Model):
id = models.CharField(max_length = 20, primary_key = True, blank = True)
name = Models.CharField(max_length = 1024, blank = True)
values = {}
def get_values(self):
return self.values
# serializers.py
class PersonCreateSerializer(CreateModelSerializer):
class Meta:
model = Person
fields = ['name', 'get_values']

The solution I found was to make use of the django-dictionaryfield module, this provides a Dictionary field type that can be used for converting to and from all array types, such as dictionaries and lists.
Without a field declared in the model.py, the serializer ignores it since it isn't considered part of the model itself, therefore using a custom DictionaryField model allows it to be stored as a Django model field.
Django DictionaryField Setup
Install the module into your project:
$ pip install django-dictionaryfield
Add dictionaryfield into your INSTALLED_APPS in the Django configuration file:
INSTALLED_APPS = (
...
"dictionaryfield",
)
Model Class
Use the DictionaryField for fields that should be arrays.
from django.db import models
from dictionaryfield import DictionaryField
class Person(models.Model):
id = models.CharField(max_length = 20, primary_key = True, blank = True)
name = Models.CharField(max_length = 1024, blank = True)
values = DictionaryField(default = {})
#staticmethod
def create_person(personData):
person = Person(
name = personData.get("name", "Unknown"),
values = personData.get("values", {}),
)
return person

Related

Django Rest Framework, creating a one-to-many field relationship between users and another model

I am trying to create a simple model which holds a number as the primary key (week number) and then a list of users. Thus the model should be something like this,
{
id: 10,
users: [
user1,
user2,
...
]
}
I am pretty sure I should do this with a one-to-many field. Thus I created the following model,
class Schema(models.Model):
week = models.PositiveIntegerField(primary_key=True,
unique=True,
validators=[MinValueValidator(1), MaxValueValidator(53)],
)
users = models.ForeignKey(MyUser, related_name="users", null=True, blank=True, on_delete=models.SET_NULL)
class Meta:
ordering = ('week',)
What I want to happen is that if you do a POST request with an id and a list of users, then it simply creates the model. However if the id already exists, then it should simply clear the users, and add the newly given users instead. This is where I am stuck, I have tried the following (keeping comments in the code),
class SchemaSerializer(serializers.ModelSerializer):
# users = serializers.PrimaryKeyRelatedField(many = True, queryset = MyUser.objects.all())
# user_set = UserSerializer(many = True)
class Meta:
model = Schema
fields = ('week', 'users')
# def create(self, validated_data):
# # users_data = validated_data.pop('users')
# schema = Schema.objects.create(**validated_data)
# # answer, created = Schema.objects.update_or_create(
# # week=validated_data.get('week', 1),
# # defaults={'users', validated_data.get('users', [])}
# # )
# return schema
def update(self, instance, validated_data):
users_data = validated_data.pop('users')
instance.users.clear()
instance.save()
for user in users_data:
instance.users.add(user)
instance.save()
return instance
Another problem I am running into is that, I don't know how the request expects the data, e.g, from Postman, I would think that it would only need the users id and of course the models id (week), however this is one of the things I have tried,
{
"week": 32,
"users": [{
"id": 1,
"first_name": "Test",
"last_name": "test",
"email": "test#test.dk"
}]
}
ForeignKey won't work for you. If one Schema object should have many relations with User model then you have to go with ManyToMany relationship(django docs). So in yours model:
class Schema(models.Model):
week = models.PositiveIntegerField(primary_key=True,
unique=True,
validators=[MinValueValidator(1), MaxValueValidator(53)],
)
users = models.ManyToManyField(MyUser, related_name="users")
class Meta:
ordering = ('week',)
for the part of update i think you need to pass user object not id to add(), so i would try:
def update(self, instance, validated_data):
users_data = validated_data.pop('users')
users = User.object.filter(id__in=user_data)
instance.users.clear()
instance.users.add(*users)
instance.save()
return instance
for api view i recomend to read this thread on stack: REST ManyToMany

How to create 2 objects from separate models with a single serializer and also retrieve them from the database with a single serializer in Django RF?

I have 3 models: Maker, Item and MakerItem that creates the relation between the items and their makers:
class Maker(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
class Item(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
name = models.CharField(max_length=100)
class MakerItem(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
item_id = models.ForeignKey(Item, on_delete=models.CASCADE)
maker_id = models.ForeignKey(Maker, on_delete=models.CASCADE)
the items can have a random amount of makers.
I want to create both the Item and the MakerItem objects at the same time with a single set of data,
for example if a Maker with id = "abcd" already exists, and I go to /item and send a POST request with the following data:
{
"name": "item1",
"makers": [
{
"maker_id": "abcd"
}
]
}
I want the serializer to create the Item object and the MakerItem object.
I have achieved this, with the following setup:
views.py
class ItemListCreate(ListCreateAPIView):
queryset = Item.objects.all()
serializer_class = ItemSerializer
serializers.py
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True)
class Meta:
model = Item
fields = ['id', 'name', 'makers']
def create(self, validated_data):
maker_item_data = validated_data.pop('makers')
item_instance = Item.objects.create(**validated_data)
for each in maker_item_data:
MakerItem.objects.create(
item_id=check_instance,
maker_id=each['maker_id']
)
return item_instance
but when Django tries to return the created object, it always gives me the error:
AttributeError at /item/
Got AttributeError when attempting to get a value for field `makers` on serializer `ItemSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `Item` instance.
Original exception text was: 'Item' object has no attribute 'makers'.
What am I doing wrong?
Thanks
EDIT: To clarify, the objects get created and populate the database correctly, but when the browsable API that DRF provides tries to display the created object, it gives me the error above.
Change:
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True)
To:
class ItemSerializer(serializers.ModelSerializer):
class MakerItemSerializer(serializers.ModelSerializer):
class Meta:
model = MakerItem
exclude = ['id', 'item_id']
makers = MakerItemSerializer(many=True, source="makeritem_set")
Hope this works!
For clarity, you're attempting to serialise the reverse relationship between MakerItem and Item for this serialiser.
This means that the attribute on your object is automatically set by Django as fieldname_set but you can override this behaviour by setting the related_name kwarg on the field and then makemigrations and migrate it.
In your case you would need to do:
maker_id = models.ForeignKey(Maker, on_delete=models.CASCADE, related_name="maker_items")
And then update the field in the Meta to match the new field name, this way you don't have to manually specify source. Because actually the attribute "makers" is misleading, due to the fact its actually the MakerItem, not the Maker itself.
See https://docs.djangoproject.com/en/3.2/ref/models/relations/ for further details about this behaviour.

Django filter returns queryset with ID instead of username

Hi guys so I have a search function but when using the .objects.filter() method I get a queryset that shows an ID instead of the username.
This is the view:
def search_expense(request):
if request.method == 'POST':
search_str = json.loads(request.body).get('searchText')
expenses = Expense.objects.filter(
amount__istartswith=search_str) | Expense.objects.filter(
date__icontains=search_str) | Expense.objects.filter(
description__icontains=search_str) | Expense.objects.filter(
category__icontains=search_str)
data = expenses.values()
return JsonResponse(list(data), safe=False)
<QuerySet [{'id': 16, 'amount': 2.33, 'date': datetime.date(2020, 10, 2), 'description': 'Something', 'owner_id': 1, 'category': 'Food'}]>
So instead of the 'owner_id': 1 I need it to be 'owner': username
The model (the User model is Django's standard model):
class Expense(models.Model):
amount = models.FloatField()
date = models.DateField(default=now)
description = models.TextField()
owner = models.ForeignKey(to=User, on_delete=models.CASCADE)
category = models.CharField(max_length=255)
def __str__(self):
return self.category
class Meta:
ordering: ['-date']
You can add new fields to .values().
from django.db.models import F
fields = tuple(x.name for x in Expense._meta.get_fields())
data = expenses.values(*fields, owner_name=F("owner__value"))
You will also have to specify all the fields you want manually, or if you want all the fields then you can just use .get_fields() like I have done.
You have to name the new field something other than "owner", or you will get a ValueError:
ValueError: The annotation 'owner' conflicts with a field on the model.
Showing the ID of ForeignKey fields is the intended behaviour of .values() according to the documentation:
If you have a field called foo that is a ForeignKey, the default values() call will return a dictionary key called foo_id, since this is the name of the hidden model attribute that stores the actual value (the foo attribute refers to the related model).

Django Rest Framework: POST object with id

Is it possible to POST a new object while also specifying its id instead of auto incrementing? This is a one time import and the database id sequence would be corrected afterward.
class TestModelSerializer(serializers.ModelSerializer):
class Meta:
model = TestModel
fields = ('id', 'name')
class TestModelViewSet(viewsets.ModelViewSet):
queryset = TestModel.objects.all()
serializer_class = TestModelSerializer
import requests
def test_post(endpoint):
data = {
"id": 30,
"name": "Test",
}
r = requests.post(endpoint, data=data)
print(r.status_code, r.reason)
test_post('http://localhost:8000/api/test_model/30/')
>>> 405 Method Not Allowed
test_post('http://localhost:8000/api/test_model/')
>>> 201 Created
201 Created creates a new object but with the next id in sequence instead of the desired id.
I've also tried r = requests.put('http://localhost:8000/api/test_model/30/', data=data) but get 404 Not Found
This could work but i'm not sure, I believe django-rest is reading from your django model that id is an auto-increment field, and hence doesn't create with the id provided. But it could work if you specify it explicitly as an integer field.
class TestModelSerializer(serializers.ModelSerializer):
id = serializers.IntegerField()
class Meta:
model = TestModel
fields = ('id', 'name')

Django Tastypie - Filtering ToManyField resource with URL parameter

I am working on implementing an API for my Django (v1.5) application using Tastypie. I would like to be able to filter/limit the related resources I get when the parent resource.
Here are my (simplified) models:
# myapp/models.py
class User(models.Model):
number = models.IntegerField()
device_id = models.CharField(verbose_name="Device ID", max_length=255)
timezone = models.CharField(max_length=255, blank=True)
def data(self, limit=0):
result = Data.objects.filter(patient_id = self.id).order_by('-datetime').values('datetime', 'value')
if limit != 0:
result = result[:limit]
return result
class Data(models.Model):
user = models.ForeignKey(User)
datetime = models.DateTimeField()
value = models.IntegerField()
My resources:
# api/resources.py
class DataResource(ModelResource):
class Meta:
queryset = Data.objects.all()
resource_name = 'cgm'
fields = ['value', 'datetime']
serializer = Serializer(formats=['json', 'xml'])
filtering = {
'datetime': ('gte', 'lte'),
}
include_resource_uri = False
def dehydrate(self, bundle):
bundle.data['timestamp'] = calendar.timegm(bundle.data['datetime'].utctimetuple())
return bundle
class UserResource(ModelResource):
data = fields.ToManyField(DataResource, attribute=lambda bundle: Data.objects.filter(patient_id=bundle.obj.id), full=True, related_name='data', null=True)
class Meta:
queryset = User.objects.all().order_by('number')
resource_name = 'user'
fields = ['number', 'timezone', 'device_id'],
serializer = Serializer(formats=['json', 'xml'])
filtering = {
'data': ALL_WITH_RELATIONS,
}
I would like to be able to filter the Data resources by 'datetime' inside the User resource using an URL parameter, e.g.:
127.0.0.1:8000/api/v1/user/1/?format=json&datetime__gte=2013-11-14%2012:00:00
or
127.0.0.1:8000/api/v1/user/1/?format=json&data__datetime__gte=2013-11-14%2012:00:00
to get the User's number, timezone, device id and Data list filtered with the given datetime.
I don't want to have to query the Data resources separately to filter them, I want the whole thing bundled within the User resource.
Is there a way to implement a filter applied to the nested resource using the framework?
Thanks for your time, I'll appreciate any suggestion!
You can extend your attribute argument you've passed to the data field with a full-scale function and reuse the DataResource:
def filter_data_items(bundle):
res = DataResource()
new_bundle = Bundle(request=bundle.request)
objs = res.obj_get_list(new_bundle)
return objs.filter(parent_id=bundle.obj.pk)
res.obj_get_list handles building and applying filters as defined per your DataResource. You just need to filter it futher on parent_id.
Reference.

Categories