Getting rest history from Django simple History - django-rest-framework

I am using django-simple-history (1.8.1) and DRF (3.5.3). I want to get a rest service containing the history of each element. Let's take an example !
models.py
class Product(models.Model):
name = models.CharField(max_length=50)
price = models.IntegerField()
history = HistoricalRecords()
def __str__(self):
return self.name
So, what must be serializers.py ? I'd like to GET something like :
[
{
"id": 1,
"name": "Apple",
"price": 8,
"history": [
{
"history_id": 1,
"id": 1,
"name": "Apple",
"price": 0,
"history_date": "2016-11-22T08:02:08.739134Z",
"history_type": "+",
"history_user": 1
},
{
"history_id": 2,
"id": 1,
"name": "Apple",
"price": 10,
"history_date": "2016-11-22T08:03:50.845634Z",
"history_type": "~",
"history_user": 1
},
{
"history_id": 3,
"id": 1,
"name": "Apple",
"price": 8,
"history_date": "2016-11-22T08:03:58.243843Z",
"history_type": "~",
"history_user": 1
}
]
}
]
After searching whitout finding the solution, I finally found it by myself. But if someone have a better solution...

I know it's been a year, but anyway, maybe someone finds it useful. Here is my solution (it seems far easier to me):
A new serializer field:
class HistoricalRecordField(serializers.ListField):
child = serializers.DictField()
def to_representation(self, data):
return super().to_representation(data.values())
Now simply use it as a a field in your serializer:
history = HistoricalRecordField(read_only=True)
This makes use of DRF's built in list and dict serializers, only trick is to pass it the correct iterable, which is being done by calling .values() on the simple-history model manager class.

Here's my solution.
In serializers.py :
from rest_framework import serializers
from .models import Product
class sHistory(serializers.ModelSerializer):
def __init__(self, model, *args, fields='__all__', **kwargs):
self.Meta.model = model
self.Meta.fields = fields
super().__init__()
class Meta:
pass
class sProduct(serializers.ModelSerializer):
class Meta:
model = Product
fields = '__all__'
history = serializers.SerializerMethodField()
def get_history(self, obj):
model = obj.history.__dict__['model']
fields = ['history_id', ]
serializer = sHistory(model, obj.history.all().order_by('history_date'), fields=fields, many=True)
serializer.is_valid()
return serializer.data
It works ! I'm quite proud about it ! any suggestions ?

There seems to be an even clearer and simpler way
class AnySerializer(serializers.ModelSerializer):
history = serializers.SerializerMethodField()
class Meta:
model = MyModel
fields = (....
....
'history',
)
read_only_fields = ('history',)
def get_history(self, obj):
# using slicing to exclude current field values
h = obj.history.all().values('field_name')[1:]
return h

You can create a serializer like this:
class ProductHistorySerializer(serializers.ModelSerializer):
class Meta:
model = Product.history.model
fields = '__all__'
Then in view, You can have the code below:
#...
logs = ProductHistorySerializer(Product.history.filter(price__gt=100), many=True)
return Response({'isSuccess': True, 'data': logs.data})

Related

Alter Queryset before returning in DRF

I have a model structure similar to the one below:
Store -> some store fields
Books -> some book fields, FK to Store
BookProperty -> name, value, FK to Books (a one to many relationship), FK to store
The book property can store any info for the book eg. no_of_pages, publisher etc added by the store.
I need to make an API endpoint where I can get all BookProperty for a store.
I used the url:
/stores/:store_id/bookproperty
Used a ModelSerializer for BookProperty with fields = [publisher, no_of_pages]
Used a genericViewSet with a ListModelMixin.
The endpoint turned out like this below:
{
"count": 4,
"next": null,
"previous": null,
"results": [
{
"name": "publisher",
"value": "somePublisher"
},
{
"name": "pages",
"value": "1000"
},
{
"name": "publisher",
"value": "someOtherPublisher"
},
{
"name": "publisher",
"value": "somePublisher"
}
]
}
The problem with this is that multiple objects can have the same name, value pairs. I need this information in a way where all the objects are unique and grouped kind of like this:
{
{"name":"publisher", "value":["somePublisher", "someOtherPublisher"]},
{"name":"pages", "value":["1000"]},
}
I'm trying to override the get_queryset(self) but it's not working.
Any help would be appreciated. Thanks in advance!
EDIT:
models.py
class BookProperty(models.Model):
books = models.ForeignKey(
Books,
on_delete=models.CASCADE,
)
name = models.CharField(max_length=100)
value = models.CharField(max_length=100)
store = models.ForeignKey(
"Store",
on_delete=models.CASCADE,
)
serializers.py
class BookPropertySerializer(serializers.ModelSerializer):
class Meta:
model = models.BookProperty
fields = ["name", "value"]
views.py
class BookPropertyViewSet(mixins.ListModelMixin, viewsets.GenericViewSet):
serializer_class = serializers.BookPropertySerializer
I think that instead of overriding the get_queryset(self) I should try changing the def list(self, request, *args, **kwargs) :
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
tag_dictionary = defaultdict()
things = list(queryset)
for key, group in itertools.groupby(things, lambda x: x.name):
for thing in group:
if key not in tag_dictionary.keys():
tag_dictionary[key] = [thing.value]
else:
tag_dictionary[key].append(thing.value)
for key in tag_dictionary.keys():
tag_dictionary[key] = list(set(tag_dictionary[key]))
return Response(json.dumps(tag_dictionary))
The above solution is working but might not be the best one.

Nested serializer doesn't pick up correct ID

There are two models, they are defined this way:
class ShoppingList(models.Model):
id = models.CharField(max_length=40, primary_key=True)
name = models.CharField(max_length=40)
session_id = models.CharField(max_length=40)
config_file = models.FileField(upload_to=upload_config_file)
def __str__(self):
return self.id
class FetchedData(models.Model):
model_id = models.CharField(max_length=40)
config_id = models.ForeignKey(BillOfMaterial, on_delete=models.CASCADE, default=0)
config_name = models.CharField(max_length=40)
def __str__(self):
return self.model_id
And serialized like this:
class FetchedDataSerializer(serializers.ModelSerializer):
file_fields = serializers.SerializerMethodField()
class Meta:
model = FetchedData
fields = ('model_id', 'config_id', 'config_name', 'file_fields')
def get_file_fields(self, obj):
print(obj)
# queryset = ShoppingList.objects.filter(config_file = obj) ## (1)
queryset = BillOfMaterial.objects.all() ## (2)
return [ShoppingListSerializer(cf).data for cf in queryset]
I was advised* to implement the solution marked as (1) in the serializer above, but when it's on, I get responses with an empty array, for example:
[
{
"model_id": "6553",
"config_id": "2322",
"config_name": "Config No. 1",
"file_fields": []
}
]
Meanwhile, with option (2) turned on and option (1) commented out, I get all the instances displayed:
[
{
"model_id": "6553",
"config_id": "2322",
"config_name": "Config No. 1",
"file_fields": [
{
"id": "2322",
"name": "First Example",
"session_id": "9883",
"config_file": "/uploads/2322/eq-example_7DQDsJ4.json"
},
{
"id": "4544",
"name": "Another Example",
"session_id": "4376",
"config_file": "/uploads/4544/d-jay12.json"
}
]
}
]
The print(obj) method always gives a model_id value. And it should output file_fields.id, I guess.
How should I re-build this piece of code to be able to display only the file_field with id matching config_id of the parent?
*This is a follow-up of an issue described here: TypeError: 'FieldFile' object is not callable
In FetchedData model I added this method:
def config_link(self):
return self.config_id.config_file
(it binds config_file from ShoppingList model).
FetchedDataSerializer should then look like this:
class FetchedDataSerializer(serializers.ModelSerializer):
file_link = serializers.SerializerMethodField()
class Meta:
model = FetchedData
fields = ('model_id', 'config_id', 'config_name', 'file_link')
def get_file_link(self, obj):
return obj.config_link()

How to delete any filed in ViewSet

class DepartSerializer(serializers.HyperlinkedModelSerializer):
attrs = AttrSerializer(source="depattr", many=True)
people = PeopleSerializer(source='perdepart', many=True)
class Meta:
model = Departs
fields = ('url', 'name', 'describe', 'pinyin', 'attrs', 'people')
class DepartsViewSet(viewsets.ReadOnlyModelViewSet):
"""
i want delete people field in List , and Retain people fieled in retrieve.
"""
queryset = Departs.objects.filter(disabled=False).order_by('-uptime')
serializer_class = DepartSerializer
1.I want the result like this:
2.get /depart
[
{"name":"depart1","id":1},
{"name":"depart2","id":2},
]
3.get /depart/1
{
"name": "depart1",
"id": 1,
"people": [{
"id": 1,
"name": "per1"
},
{
"id": 2,
"name": "per2"
}
]
}
You can use different serializers in your viewset depending on the action by overriding the viewset's get_serializer_class:
def get_serializer_class(self):
if self.action == 'retrieve':
return DepartSerializerWithoutPeople
return DepartSerializer
retrieve is the action called by get /depart/1/. And then you can define your DepartSerializerWithoutPeople like this:
class DepartSerializerWithoutPeople(serializers.HyperlinkedModelSerializer):
attrs = AttrSerializer(source="depattr", many=True)
class Meta:
model = Departs
fields = ('url', 'name', 'describe', 'pinyin', 'attrs',)

Get data in multiple level nested serializer using Django Rest

Need to serialize three models nested in three levels.
There are users assigned areas and these contains point. The users contains multiple areas. Areas have multiple points associated.
Users links areas using many to many relationship.
Areas Links with point using Foreign in the points.
Users can be assigned to multiple areas. Areas can have multiple points.
User Profile Model
class UserProfile(AbstractBaseUser,PermissionsMixin):
phone_number= PhoneNumberField( unique=True)
name=models.CharField(max_length=255)
organisation=models.CharField(max_length=255)
is_active=models.BooleanField(default=True)
is_staff=models.BooleanField(default=False)
added_by=models.ForeignKey(settings.AUTH_USER_MODEL,default=1)
group = models.ForeignKey('auth.Group', null=True)
areas=models.ManyToManyField('area.Area',blank=True)
objects=UserProfileManager()
Areas Model
from django.db import models
from django.conf import settings
# Create your models here.
class Area(models.Model):
areaName =models.TextField()
timestamp = models.DateTimeField(auto_now=False, auto_now_add=True)
updated = models.DateTimeField(auto_now=True, auto_now_add=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL )
def __str__(self):
return self.areaName
Point Model
from django.db import models
from django.conf import settings
# Create your models here.
class Point(models.Model):
name =models.TextField()
area = models.ForeignKey('area.Area', on_delete=models.CASCADE)
latitude=models.CharField(max_length=200)
longitude=models.CharField(max_length=200)
timestamp=models.DateTimeField(auto_now=False,auto_now_add=True)
updated=models.DateTimeField(auto_now=True,auto_now_add=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL )
def __str__(self):
return self.name
I want a result like following:
{
"id": 3,
"phone_number": "+919999999999",
"name": "Ak",
"organisation": "sp",
"group": 1,
"areas": [
{
"id": 1,
"areaName": "Area 51",
"user": 1
points:[{
}]
},
{
"id": 2,
"areaName": "Rrea 343",
"user": 1
point:[{}]
}
]
},
{
"id": 4,
"phone_number": "+918888888888",
"name": "Chitra Sahu",
"organisation": "sd",
"group": 2,
"areas": [
{
"id": 1,
"areaName": "Area 51",
"user": 1
point:[{
latitude:'23.2323',
longitude:'23.2323'
},
{
latitude:'21.1223',
longitude:'32.34345'
}
]
},
{
"id": 2,
"areaName": "Rrea 343",
"user": 1
point:[{
latitude:'23.2323',
longitude:'23.2323'
},
{
latitude:'21.1223',
longitude:'32.34345'
}]
}
]
},
So Far I have tried the following
class AreasSerializer(serializers.ModelSerializer):
class Meta:
model=Area
fields=('id','areaName','user')
class AreasUserSerializer(serializers.ModelSerializer):
areas = AreasSerializer(many=True, read_only=True)
class Meta:
model = UserProfile
fields = ('id','phone_number','name','organisation','group','areas')
class AreasUserPointSerializer(serializers.ModelSerializer):
areasUsers=AreasUserSerializer()
class Meta:
model=Point
fields =('id','areasUsers' )
Views
'''Fetch list all question '''
class AreasPointsUsersListApiView(ListAPIView):
serializer_class=serializers.AreasUserPointSerializer
def get_queryset(self):
queryset=UserProfile.objects.all()
user=self.request.query_params.get('user_id',None)
if user is not None:
queryset = queryset.filter(id=user)
#if areas is not None:
# queryset = queryset.filter(areas=areas)
return queryset
.py
This code is not working properly.
I need to serialize it so that the Users consists Areas based on Many to Many relationship. These areas are linked to point using the foreign key in Point.
EDIT
Edit:
Areas serializer
I have resolved this using LocationSerializer which invoked by AreasSerializer.
I am sharing the code snippet. It was pretty easy.
class PointSerializer(serializers.ModelSerializer):
class Meta:
model = Point
fields=('id','latitude','longitude')
class AreasLocationSerializer(serializers.ModelSerializer):
points = PointSerializer(many =True, read_only=True)
class Meta:
model=Area
fields=('id','areaName','points','user')
class AreasUserLocationSerializer(serializers.ModelSerializer):
areas =AreasLocationSerializer(many=True, read_only=True)
class Meta:
model=UserProfile
fields =('id','phone_number','name','areas')

In the Django Rest Framework, how do you add ManyToMany related objects?

Here's my code:
Models
class Recipe(models.Model):
name = models.CharField(max_length=50, unique=True)
ingredient = models.ManyToManyField(Ingredient)
class Ingredient(models.Model):
name = models.CharField(max_length=50, unique=True)
View
class RecipeDetailAPIView(RetrieveUpdateDestroyAPIView):
permission_classes = (IsAdminOrReadOnly,)
serializer_class = RecipeSerializer
queryset = Recipe.objects.all()
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
def perform_update(self, serializer):
serializer.save(updated_by_user=self.request.user)
Serializers
class IngredientSerializer(serializers.ModelSerializer):
class Meta:
model = Ingredient
fields = [
'id',
'name',
]
class RecipeSerializer(serializers.ModelSerializer):
ingredient = IngredientSerializer(many=True, read_only=False)
class Meta:
model = Recipe
fields = [
'id',
'name',
'ingredient',
]
I'm starting with the following Recipe object:
{
"id": 91
"name": "Potato Salad"
"ingredient": [
{
"id": 5,
"name": "Potato"
}
]
}
Now, I am attempting to update that object by putting the following JSON object to the IngredientSerializer:
{
"id": 91
"name": "Potato Salad"
"ingredient": [
{
"id": 5,
"name": "Potato"
},
{
"id": 6,
"name": "Mayo"
}
]
}
What I want is it to recognize that the relationship to Potato already exists and skip over that, but add a relationship to the Mayo object. Note that the Mayo object already exists in Ingredients, but is not yet tied to the Potato Salad object.
What actually happens is the Serializer tries to create a new Ingredient object and fails because "ingredient with this name already exists."
How do I accomplish this?
DRF does not have any automatic "write" behavior for nested serializers, precisely because it does not know things like how to go about updates in the scenario you mentioned. Therefore, you need to write your own update method in your RecipeSerializer.
class IngredientSerializer(serializers.ModelSerializer):
def validate_name(self, value):
# manually validate
pass
class Meta:
model = Ingredient
fields = ['id', 'name']
extra_kwargs = {
'name': {'validators': []}, # remove uniqueness validation
}
class RecipeSerializer(serializers.ModelSerializer):
ingredient = IngredientSerializer(many=True, read_only=False)
def update(self, instance, validated_data):
ingredients = validated_data.pop('ingredient')
# ... logic to save ingredients for this recipe instance
return instance
class Meta:
model = Recipe
fields = ['id', 'name', 'ingredient']
Relevant DRF documentation:
Saving Instances
Writable Nested Serializer
Updating nested serializers
Update:
If DRF validation fails for the uniqueness constraint in the name field, you should try removing validators for that field.
Alternative solution: Only use full serializer as read only field
You can change you RecipeSerializer to the following:
class RecipeSerializer(serializers.ModelSerializer):
ingredient_details = IngredientSerializer(many=True, read_only=True, source='ingredient')
class Meta:
model = Recipe
fields = ['id', 'name', 'ingredient', 'ingredient_details']
And that's it. No need to override update or anything. You'll get the detailed representation when you get a recipe, and you can just PUT with the ingredient ids when updating. So your json when updating will look something like this:
{
"id": 91
"name": "Potato Salad"
"ingredient": [5, 6]
}

Resources