Serializers in django rest framework with dynamic fields - django

I am trying to build a small api with django rest framework but I don't want to map directly the tables with calls (as in the examples).
I have the following database schema:
In models.py:
class ProductType(models.Model):
name = models.CharField(max_length=255, blank=False, null=False, unique=True)
class Product(models.Model):
#staticmethod
def get_accepted_fields(self):
return {'color': 'pink', 'size': 34, 'speed': 0, 'another_prop': ''}
name = models.CharField(max_length=255, blank=False, null=False, unique=True)
class ProductConfig(models.Model):
product_type = models.ForeignKey(ProductType)
product = models.ForeignKey(Product)
# a json field with all kind of fields: eg: {"price": 123, "color": "red"}
value = models.TextField(blank=True)
As you can see, every product can have multiple configurations and the value field is a json with different parameters. The json will be one level only. The configuration will have a flag if is active or not (so, the 1 product will have only 1 active configuration)
So, the data will look for example like this:
store_producttype
=================
1 type1
2 type2
store_product
=============
id name
1 car
store_productconfig
===================
id product_type_id product_id value active
1 2 1 { "color": "red", "size": 34, "speed": 342} 0
2 1 1 { "color": "blue", "size": 36, "speed": 123, "another_prop": "xxx"} 1
What I want to know is how can I get /product/1/ like this:
{
"id": 1,
"name": "car",
"type": "type1",
"color": "blue",
"size": 36,
"speed": 123,
"another_prop": "xxx",
}
and to create a new product posting a json similar with the one above.
The json fields are defined but some of them can miss (eg: "another_prop" in the productconfig.id=1
On update, anyway, it will create a new row in productconfig and it will put inactive=0 on the previous one.
So, every product can have different configuration and I want to go back to a specific configuration back in time in some specific cases). I am not really bound to this data model, so if you have suggentions for improvement I am open to them, but I don't want to have that properties as columns in the table.
The question is, what will be the best way to write the serializers for this model? There is any good example somewhere for a such use case?
Thank you.

Let's take this step by step:
In order to get a JSON like the one you posted, you must first transform your string (productConfig value field) to a dictionary. This can be done by using ast.literal_eval ( see more here).
Then, in your product serializer, you must specify the source for each field, like this:
class ProductSerializer(serializers.ModelSerializer):
color = serializer.Field(source='value_dict.color')
size = serializer.Field(source='value_dict.size')
type = serializer.Field(source='type.name')
class Meta:
model = Product
fields = (
'id',
'color',
'size',
'type',
)
This should work just fine for creating the representation that you want. However, this will not create automatically the product config, because DRF doesn't yet allow nested object creation.
This leads us to the next step:
For creating a product with a configuration from JSON, you must override the post method in your view, and create it yourself. This part shouldn't be so hard, but if you need an example, just ask.
This is more of a suggestion: if the json fields are already defined, wouldn't it be easier to define them as separate fields in your productConfig model?

Related

Hide the (obvious) relation from nested serializer in DRF

Django + rest framework. This seems like it should be a frequent and common issue, but I could not find anything like it, so here I ask:
I have a Document and its Items:
class DocumentSerializer(ModelSerializer):
...
items = ItemsSerializer(many=True, required=False)
class Meta:
model = Document
exclude = ()
class ItemsSerializer(ModelSerializer):
...
class Meta:
model = DocumentItem
exclude = ('document', ) # hide the ForeignKey as it should be obvious by nesting
Expected result for JSON serialized data something like:
{
"id": 1, "date": "2021-01-01T00:00:00Z", "title": "Test doc",
"items": [
{"code": 43, quantity: 3},
{"code": 28, quantity: 15}
]
}
It should be fairly obvious that the "document" field from ItemsSerializer should be derived from the parent serializer at the time of storage. The field itself being a ForeignKey to Document, of course.
However, I can't get past the ValidationError({"document":["This field is required."]}).
If I say the field is not required, then save()complains AssertionError: The '.create()' method does not support writable nested fields by default.
What is the accepted way of handling relations like this in serializers?
My suggestion:
Check if it makes sense to have a Document without Items.
Always when you have nested Serializer, method #create and #update should be overridde. Check documentation

Django - get serialized fields from related model

I can't figure out how to serialize a query that includes fields from a reverse related model. My models look like this. Every vote is linked to a single album:
# models.py
class Album(models.Model):
name = models.CharField(max_length=50)
class Vote(models.Model):
album = models.ForeignKey(Album, on_delete=models.CASCADE)
user_vote = models.BooleanField(default=0)
What I'd like to do is perform a query that returns all Album objects, as well as a sum of the votes attributed to that album. That's easy enough, but when I serialize the query, the "total_votes" field is lost:
# views.py
# this works fine
query = Album.objects.annotate(total_votes = Sum(vote__user_vote))
# after serialization, I lose the field "total_votes"
serialized = serializers.serialize('json', list(query))
return serialized
Unfortunately, the field "total_votes" doesn't appear in the serialized result since, according to Django documentation, "only the fields that are locally defined on the model will be serialized."
So my question is, how do I get the following serialized result (assuming there are 100 votes for Abbey Road and 150 for Astral Weeks)? Any help would be greatly appreciated.
[
{
"pk": 1,
"model": "app.album",
"fields": {
"name": "Abbey Road",
"total_votes": 100
},
{
"pk": 2,
"model": "app.album",
"fields": {
"name": "Astral Weeks",
"total_votes": 150
},
...
]
According to the source, there’s no way to do this using serializers.serialize. The base django serializer will only serialize local_fields on the model instance:
for field in concrete_model._meta.local_fields:
if field.serialize:
if field.remote_field is None:
if self.selected_fields is None or field.attname in self.selected_fields:
self.handle_field(obj, field)
else:
if self.selected_fields is None or field.attname[:-3] in self.selected_fields:
self.handle_fk_field(obj, field)
for field in concrete_model._meta.many_to_many:
if field.serialize:
if self.selected_fields is None or field.attname in self.selected_fields:
self.handle_m2m_field(obj, field)
In order to get what you want, you’d have to roll your own serialization function (e.g., something that converts your model to a dict and then uses the DjangoJSONEncoder).

Django REST Framework and related models with one field

I want to create a simple REST API for the following model:
I have a main entity called Product with some fields: name, price...
Also I have a related entity called Keyword with one single field: the keyword.
Each product can have one or more keywords.
I can easily translate this into two django models:
class Product(models.Model):
name = models.CharField("Name of the product", max_length=100)
description = models.TextField("Description of the product")
price = models.IntegerField("Price of the product")
received_at = models.DateField("Received at")
class Keyword(models.Model):
keyword = models.CharField(max_length=50)
product = models.ForeignKey(Product, related_name="keywords")
But I'm lost with serializers.
I want a simple verb like this to create:
POST /products
{
"name": "My product name",
"description": "My product description",
"price": 40,
"received_at": "2015-12-1",
"keywords": ["keyword1", "keyword2"]
}
And the common list all, list one, update and delete:
GET /products
[{"name": "Product 1"...}, {"name": "Product 2"...}]
GET /products/1
{"name": "Product 1", "description": ...}
PUT /products/1
{
"name": "My product name",
"description": "My product description",
"price": 40,
"received_at": "2015-12-1",
"keywords": ["keyword2"]
}
DELETE /products/1
The problem is manage the keywords.
When a new product is created all keywords are created
When a product is updated I have to check which keywords are news and which keywords have been deleted.
Also I want my API can be extensible in the near future with more relations like the "Keyword relation" with a simple string field.
Any ideas?
Try implementing the underlying keywords as members of a set. Then you can simply call the add method with each new keyword and the set will keep it a unique grouping, by the nature of how sets work.
a = set()
a.add('keyword1')
a.add('keyword2')
a.add('keyword1')
print(a)
Output:
set(['keyword1', 'keyword2'])

Django: Tastypie: Updating record after displaying it

I have a very simple model:
class challenge(models.Model):
challenge = models.CharField(max_length=255)
timestamp = models.DateTimeField('date published',null=True,blank=True)
code = models.CharField(max_length=255)
And that model is linked via Tastypie.
By default the timestamp field is empty when you add an entry.
I would like to update the timestamp field with the time that the user first accessed the resource.
For example, if admin added data is:
Challenge: Do something
Timestamp: Null
Code: 123
Then after accessing /api/challenge/1/?format=json to have the output be:
{"challenge": "Do something", "code": "sasdasd", "id": 2, "resource_uri": "/api/challenge/2/", "timestamp": "2015-05-11T12:18:54"}
Is this possible using Tastypie and how?

Translating between DRF's serializer and model design

I am using Django Rest Framework to support the use of Annotator.JS(http://annotatorjs.org/) in the frontend of my web application. The problem is the model I use to store any annotations that a user makes is different to the JSON that Annotator.JS sends from the front end in an AJAX request.
The structure of Annotator.JS' JSON is:
{
"id": "39fc339cf058bd22176771b3e3187329", # unique id (added by backend)
"annotator_schema_version": "v1.0", # schema version: default v1.0
"created": "2011-05-24T18:52:08.036814", # created datetime in iso8601 format (added by backend)
"updated": "2011-05-26T12:17:05.012544", # updated datetime in iso8601 format (added by backend)
"text": "A note I wrote", # content of annotation
"quote": "the text that was annotated", # the annotated text (added by frontend)
"uri": "http://example.com", # URI of annotated document (added by frontend)
"ranges": [ # list of ranges covered by annotation (usually only one entry)
{
"start": "/p[69]/span/span", # (relative) XPath to start element
"end": "/p[70]/span/span", # (relative) XPath to end element
"startOffset": 0, # character offset within start element
"endOffset": 120 # character offset within end element
}
],
"user": "alice", # user id of annotation owner (can also be an object with an 'id' property)
"consumer": "annotateit", # consumer key of backend
"tags": [ "review", "error" ], # list of tags (from Tags plugin)
}
The structure of my Annotation model is:
class Annotation(models.Model):
datapoint = models.ForeignKey('datapoint.Datapoint', related_name='%(class)s_parent_datapoint_relation')
owner = models.ForeignKey('users.User', related_name='%(class)s_creator_relation')
# Key fields from the Annotator JSON Format: http://docs.annotatorjs.org/en/latest/annotation-format.html
annotator_schema_version = models.CharField(max_length=8, blank=True)
text = models.TextField(blank=True)
quote = models.TextField()
uri = models.URLField(blank=True)
range_start = models.CharField(max_length=50, blank=True)
range_end = models.CharField(max_length=50, blank=True)
range_startOffset = models.BigIntegerField()
range_endOffset = models.BigIntegerField()
tags = TaggableManager(blank=True)
How can I create a serializer that can translate from the model structure to the JSON?
P.S. Annotator.JS allows the user to send extra information with the JSON structure noted above so the fact that Datapoint isn't included in the JSON structure isn't an issue. This can be passed along without any problem. Owner would hopefully equal User in the JSON.
Thanks for any help, it is greatly appreciated.
Just using a default ModelSerializer should get you serialization for all the simple fields for free (version, text, quote, uri), just by specifying which fields you want serialized. The other fields look straightforward as well:
To compose the ranges object, you can use a SerializerMethodField which lets you define a custom serializer method (return an array of dictionaries containing values from the range_* attributes on your model). Note that if you also need to be able to deserialize JSON into your model you'll need to define custom fields.
To go from owner -> user_id SlugRelatedField (assuming you can access the user id you want from the User object)
You can also use SlugRelatedField to go from TaggableManager -> ["tag", "values"] (again, assuming you can access the value you want through the model object managed by TaggableManager.
In general, everything you want is described in pretty good detail in the documentation.