Fetching custom rest data from Django - python

I am using Django as server side and angular as client side.
I want to fetch data from my django rest api backend.
I saw a lot of tutorials about fetching data from an already existent modules. But what if I want to retrieve data that is a combination of several modules?
For example, I have two modules Reservations and Clubs. I want to retrieve json object that contains data from both of these modules, for specific club id.
Modules -
class Club(models.Model):
name = models.CharField(max_length=1024)
image_path = models.CharField(max_length=1024)
class Reservation(models.Model):
club = models.ForeignKey('Club')
user = models.CharField(max_length=1024)
is_paid = models.BooleanField(default=False)
Serializers -
class ReservationSerializer(serializers.ModelSerializer):
class Meta:
model = Reservation
fields = ('club', 'user')
class ClubSerializer(serializers.ModelSerializer):
class Meta:
model = Club
fields = ('id', 'name', 'surfaces')
View sets -
class ReservationViewSet(generics.ListAPIView):
serializer_class = ReservationSerializer
queryset = Reservation.objects.all()
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('id', 'club')
class ClubViewSet(generics.ListAPIView):
queryset = Club.objects.all()
serializer_class = ClubSerializer
filter_backends = (filters.DjangoFilterBackend,)
filter_fields = ('id', 'name')
So, for this exmaple, I want that when I GET this url -
http://127.0.0.1:8000/api/initial_data?club=2
it will run a ViewSet that does some logic and then return a json of this format -
{club_id: 2, reservations: {1:"John", 2:"Doe", 3:"Bob"} }
And more generally speaking - How can I return my own custom json containing data from multiple modules, with given URL parameters (back to client side)?
EDIT - If I want to return a simple JSON, how should I do it with django DRF, considering the fact that each viewset is being mapped into a model/serializer?
Maybe using a simple JsonResponse..?

You can define one serializer as a field in another
class ReservationSerializer(serializers.ModelSerializer):
...
class ClubSerializer(serializers.ModelSerializer):
reservations = ReservationSerializer(many=True, read_only=True)
...
Now ClubSerializer will return Reservations inside each Club

You can use APIView and override get method.
def get(self, request)
club = request.query_params.get('club', None)
"""
Do anything, access data, prepare dictionary or list with result
"""
return Response(result_dictionary_or_list)

Related

Partial updating a ManyToMany field, but keeping its get representation

I've been scratching my head about this problem for a couple of hours now. Basically, I have two models: User and Project:
class User(AbstractUser):
username = None
email = models.EmailField("Email Address", unique=True)
avatar = models.ImageField(upload_to="avatars", default="avatars/no_avatar.png")
first_name = models.CharField("First name", max_length=50)
last_name = models.CharField("Last name", max_length=50)
objects = UserManager()
USERNAME_FIELD = "email"
class Project(models.Model):
name = models.CharField("Name", max_length=8, unique=True)
status = models.CharField(
"Status",
max_length=1,
choices=[("O", "Open"), ("C", "Closed")],
default="O",
)
description = models.CharField("Description", max_length=3000, default="")
owner = models.ForeignKey(
User, on_delete=models.SET_NULL, null=True, related_name="project_owner"
)
participants = models.ManyToManyField(User, related_name="project_participants", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
I use standard ModelViewSets for both of them, nothing changed. Then there's my Project serializer:
class ProjectSerializer(serializers.ModelSerializer):
class Meta:
model = Project
fields = "__all__"
status = serializers.CharField(source="get_status_display", required=False)
owner = UserSerializer()
participants = UserSerializer(many=True)
I use UserSerializers here, because having them achieved first of my two goals:
I wanted to get the user data when getting the project from the API -> owner is a serialized User with all the fields, same for participants, but it's a list of users
I want to be able to partially update the Project, for example add a participant
So I searched through the docs and SO and I always found answers that answer one of those questions, but never both of them.
The thing with my second goal is: when I do the partial update (via PATCH, of course), I get the response that: "Invalid data. Expected a dictionary, but got int." when I pass a list of ints (user ids) for the participants. I thought: okay, maybe I have to pass the whole user data to change it. But then I realised: when I remove the UserSerializer from ProjectSerializer - passing just the list of ints in Postman works just fine. And that is a life saver, cuz who wants to create a request with a whole bunch of data, when I can just pass user ids.
But then of course when I remove the UserSerializer, when I call get project, I get participants: [1,2,3,4,...], not participants: [{"id": 1, "name": "John", ...}, ...}]. And I really want this behavior, because I don't want to make additional API calls just to get the users' data by their IDs.
So summing up my question is: Is there a way to leave those serializers in place but still be able to partially update my model without having to pass whole serialized data to the API (dicts instead of IDs)? Frankly, I don't care about the serializers, so maybe the question is this: Can I somehow make it possible to partially update my Products' related fields like owner or participants just by passing the related entities IDs while still maintaining an ability to get my projects with those fields expanded (serialized entities - dicts, instead of just IDs)?
#Edit:
My view:
from rest_framework import viewsets, permissions
from projects.models import Project
from projects.api.serializers import ProjectSerializer
class ProjectViewSet(viewsets.ModelViewSet):
queryset = Project.objects.all()
serializer_class = ProjectSerializer
permission_classes = [permissions.IsAuthenticated]
lookup_field = "name"
def get_queryset(self):
if self.request.user.is_superuser:
return Project.objects.all()
else:
return Project.objects.filter(owner=self.request.user.id)
def perform_create(self, serializer):
serializer.save(owner=self.request.user, participants=[self.request.user])
Answer:
To anyone reading this, I've solved this problem and I actually created a base class for all my viewsets that I want this behavior to be in:
from rest_framework.response import Response
class ReadWriteViewset:
write_serializer_class = None
read_serializer_class = None
def update(self, request, *args, **kwargs):
partial = kwargs.pop("partial", False)
instance = self.get_object()
write_serializer = self.write_serializer_class(
instance=instance,
data=request.data,
partial=partial,
)
write_serializer.is_valid(raise_exception=True)
self.perform_update(write_serializer)
read_serializer = self.read_serializer_class(instance)
if getattr(instance, "_prefetched_objects_cache", None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(read_serializer.data)
Then you use it kinda like in here
I'm assuming that you are using a ModelViewSet. You could use different serializers for different methods.
class ProjectViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
if self.action in ['create', 'update']:
return WriteProjectSerializer # your serializer not using `UserSerializer` that works for updating
return ProjectSerializer # your default serializer with all data
Edit for using different serializers in same method:
# you can override `update` and use a different serializer in the response. The rest of the code is basically the default behavior
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
write_serializer = WriteProjectSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
instance = self.perform_update(serializer)
read_serializer = ProjectSerializer(instance)
if getattr(instance, '_prefetched_objects_cache', None):
# If 'prefetch_related' has been applied to a queryset, we need to
# forcibly invalidate the prefetch cache on the instance.
instance._prefetched_objects_cache = {}
return Response(read_serializer.data)
A good way to see the default code for all these methods is using Classy DRF. You can see all methods that come with using ModelViewSet and use that code with some changes. Here I'm using the default code for update but changing for a new serializer for the response.

how to set foreignkey value in child serializer in django rest framework when the parent record exists

I have two models first as parent model "Country", that filled before the second one as child model "City". as the following
class Country(models.Model):
name = models.CharField(max_length=35)
icon = models.ImageField()
def __str__(self):
return self.name
class City(models.Model):
name = models.CharField(max_length=35)
country = models.ForeignKey(to=Country, on_delete=models.CASCADE)
def __str__(self):
return self.name
My serializers.py for my need as following :
class CountrySerializer(ModelSerializer):
class Meta:
model = Country
fields = '__all__'
class CitySerializer(ModelSerializer):
country = serializers.PrimaryKeyRelatedField(queryset=Country.objects.all())
class Meta:
model = City
fields = ('name', 'country')
view.py
class CountryAPIView(ListAPIView):
queryset = Country.objects.all()
serializer_class = CountrySerializer
permission_classes = [AllowAny, AllowAnonymous]
class CityAPIView(ListAPIView):
queryset = City.objects.all()
serializer_class = CitySerializer
permission_classes = [AllowAny, AllowAnonymous]
def post(self, request):
serializer = CitySerializer(data=request.data)
if serializer.is_valid(raise_exception=ValueError):
serializer.create(validated_data=request.data)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.error_messages,
status=status.HTTP_400_BAD_REQUEST)
now when i run get api it run and gives me a result fine . But when im trying to create a new city and set "country":"id" in json i got this error
Cannot assign "2": "City.country" must be a "Country" instance.
So if i was not clear ,, what i need is exactly set foreign key to city when i create city ,, not create city and country,,
please any one had a solution help, because i tried many ways and read the django rest framework docs about this point but i didn't got it.
First of all, the raise_exception should be a boolean value (either True or False)
You could avoid this error by using inheriting the view class from ListCreateAPIView
from rest_framework.generics import ListCreateAPIView
class CityAPIView(ListCreateAPIView):
queryset = City.objects.all()
serializer_class = CitySerializer
permission_classes = [AllowAny, AllowAnonymous]
You don't want to use the post() method if you're using ListCreateAPIView, because DRF will take care of that part well.
Suggestion
Since you're dealing with CRUD functionalities of the model, you can use the DRF's ModelViewset class
you are not using the validated data to create a new city, just change this line:
serializer.create(validated_data=request.data)
to this:
serializer.save()
when you perform serializer.save(), the serializer will use its validated data.
also, DRF has a generic view(ListCreateAPIView) that covers your use-case.

Filtering results of multiple models under one API Endpoint with django-rest-framework

I am using DRF to generate API for my django app.
I am using the ViewSet class and exposing API endpoints for most of my models at their own path.
I want to allow viewing of my Endpoint and TLSCertificate models at an /assets/ path. As they are both children of an Organisation entity, I want to allow the results to be filtered Organisation.
So far I have:
serializers.py
class AssetSerializer(serializers.Serializer):
endpoints = EndpointSerializer(many=True)
certificates = TLSCertificateSerializer(many=True)
views.py
class AssetFilterSet(filters.FilterSet):
organisation = filters.ModelChoiceFilter(
name='organisation', queryset=Organisation.objects.all())
project = filters.ModelChoiceFilter(
name='project', queryset=Project.objects.all())
class Meta:
model = Endpoint
fields = ['organisation', 'project']
# the object type to be passed into the AssetSerializer
Asset = namedtuple('Asset', ('endpoints', 'certificates'))
class AssetViewSet(CacheResponseAndETAGMixin, viewsets.ViewSet):
"""
A simple ViewSet for listing the Endpoints and Certificates in the Asset list.
Adapted from https://stackoverflow.com/questions/44978045/serialize-multiple-models-and-send-all-in-one-json-response-django-rest-framewor
"""
# TODO filtering not functional yet
filter_class = AssetFilterSet
filter_fields = ('organisation', 'project',)
queryset = Endpoint.objects.all()
def list(self, request):
assets = Asset(
endpoints=Endpoint.objects.all(),
certificates=TLSCertificate.objects.all(), )
serializer = AssetSerializer(assets, context={'request': request})
return Response(serializer.data)
This works in returning the objects but does not allow for filtering to take place.
I would appreciate any guidance in how to enable the filtering in this situation?

Representing more than single object/list CRUD ops with Django REST Framework ViewSets

I've been writing a game picking style webapp with Django and recently decided to implement my views as API endpoints with DRF, to give me more flexibility when it comes to frontend approaches. I have basic serializers and ViewSets for each of my models, and I can browse them normally with the (excellent) browsable API. Here are a couple:
class SheetSerializer(serializers.HyperlinkedModelSerializer):
user = UserSerializer(read_only = True)
league = LeagueSerializer(read_only = True)
picks = serializers.HyperlinkedRelatedField(
source='pick_set',
many=True,
view_name='pick-detail',
read_only = True
)
class Meta:
model = Sheet
fields = ('url', 'id', 'league_week', 'user', 'league', 'picks')
class GameSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Game
fields = ('url', 'home_team', 'away_team', 'week', 'home_team_score', 'away_team_score')
class PickSerializer(serializers.HyperlinkedModelSerializer):
sheet = SheetSerializer()
game = GameSerializer()
class Meta:
model = Pick
fields = ('url', 'sheet', 'amount', 'spread', 'pick_type', 'pick_team', 'game')
With respective ViewSets:
class PickViewset(viewsets.ModelViewSet):
queryset = Pick.objects.all()
serializer_class = PickSerializer
class GameViewset(viewsets.ModelViewSet):
queryset = Game.objects.all()
serializer_class = GameSerializer
class SheetViewset(viewsets.ModelViewSet):
queryset = Sheet.objects.all()
serializer_class = SheetSerializer
What I'm currently having trouble with is how to represent more complex endpoints than single-object or list of same-type object CRUD operations. For example, I currently have a regular Django view for matchups which pulls the users Sheet (collection of Picks), another users Sheet, and displays the nested Picks against each other. I'm also planning to display other data on the same page from the other users in whatever League they're a part of. The implementation for the user + opponent data in vanilla Django looks like this:
class MatchupDetail(DetailView):
template_name = 'app/matchups.html'
context_object_name = 'pick_sheet'
def get_object(self):
#logic to find and return object
def get_opponent(self,username,schedule,week, **kwargs):
#logic to find and return the opponent in the matchup
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
#logic to pull the opponents details and set them in the context
I've been struggling on how to represent this in a ViewSet. With a regular Django view, it's easy to write the get_object (or get_list) as well as get_context_data, include the user's data + any other desired objects from the database, and pass all of that to the template. Currently, the (early) API endpoint version of the above looks like this:
class MatchupViewset(viewsets.ReadOnlyModelViewSet):
serializer_class = SheetSerializer
def get_queryset(self):
user = self.request.user
return Sheet.objects.filter(user=self.request.user)
def list(self, request, format=None):
sheets = self.get_queryset()
serializer = SheetSerializer(sheets, many=True, context={'request': request})
return Response(serializer.data)
def retrieve(self, request, pk, format=None):
sheet = Sheet.objects.get(user=self.request.user, league_week=pk)
serializer = SheetSerializer(sheet, context={'request':request})
return Response(serializer.data)
This code works, but only returns a limited subset of objects- a single users Sheet(s). My aim in this Viewset is to return a list of all the matchups (whi are just a collection of user Sheets) for a given league with list() and given league + week with retrieve(), but I'm not sure how to return that data. With the normal Django context variable, extra data you include can be named in any manner you like- how is the goal of collecting and returning an arbitrary number of potentially different objects accomplished with DRF? edit: this wasn't very clear initially- I've gotten nested representation working, but I'm wondering if there's a way to name/label the different serialized objects in the manner that information in the normal context can be set with any name
Say I want to pass the requesting users serialized Sheet, along with their opponents Sheet and all the other players Sheets for this particular league and week. Will the logic to determine which belongs to the user, which belongs to their opponent, and which are the other players' have to live in the frontend code?
Also, how would I configure the router so that the list() method always requires a URL parameter (for the league)? Anytime I change the registration from
router.register(r'matchup', MatchupViewset, base_name = 'matchup')
to something like
router.register(r'matchup/(?P<league>[0-9]+)/$', MatchupViewset, base_name = 'matchup')
the endpoint disappears from the browsable API root.
how is the goal of collecting and returning an arbitrary number of potentially different objects accomplished with DRF?
I'm not sure what the question is exactly here. I assume you'll want nested representations which is explained here.
the endpoint disappears from the browsable API root.
This is because the browsable API doesn't know how to resolve the extra kwargs. Since no url matches, he won't display the link

Save a many-to-many model in Django/REST?

I'm writing a REST API for my Django app, and can't get POST requests to work on one model.
Here's the model in question:
class ProjectNode(models.Model):
name = models.CharField(max_length=60)
place = models.CharField(max_length=150)
time_spent = models.BigIntegerField()
parent_project = models.ForeignKey(Project, related_name='tasks')
workers = models.ManyToManyField(User, related_name='tasks_can_do')
def __str__(self):
return self.name
The User model just holds a name field at the moment.
Here's my serializer for ProjectNode:
class ProjectNodeSerializer(serializers.ModelSerializer):
class Meta:
model = ProjectNode
fields = ('id', 'name', 'place', 'time_spent', 'workers',)
And here's the API view (from views.py):
class WebProjectNodeListView(generics.ListCreateAPIView):
queryset = ProjectNode.objects.all()
serializer_class = ProjectNodeSerializer
def pre_save(self, obj):
obj.parent_project = Project.objects.get(pk=self.request.DATA['parent_project'])
for worker_pk in self.request.DATA['workers']:
obj.workers.add(User.objects.get(pk=worker_pk))
obj.final_worker = User.objects.get(pk=self.request.DATA['final_workers'])
I tried a simpler version yesterday at first, which only had the Project ForeignKey relationship, and it seemed to work, so I thought that using add would work too, but I get an error when testing out the API with httpie (I already added some users and projects, and am sure I get their id's correctly).
Here's the request:
http POST :8000/api/tasks/ name="newtask" place="home" time_spent:=50 parent_project:=1 workers:=[1]
And I get this error:
"<ProjectNode: newtask>" needs to have a value for field "projectnode" before this many-to-many relationship can be used.
And the traceback also points to this line of code:
obj.workers.add(User.objects.get(id=worker_pk))
Now, I get the feeling that this is because I'm trying to update the relationship on the User object before a ProjectNode object is created in the database, but I'm not sure how to resolve this?
DRF doesn't works create models which are nested serializers objects or Many to Many fields.
So is necessary to override Serializer create method and create/get M2M models before create ProjectNode.
Try to override create(self, validated_data) in your serializer and work with your data inside this method..
Example:
My model Project has M2M relation with ProjectImages. In ProjectSerializer I override create method like this.
def create(self, validated_data):
try:
# Remove nested and M2m relationships from validated_data
images = validated_data.pop('projectimage_set') if 'projectimage_set' in validated_data else []
# Create project model
instance = Project(**validated_data)
if status:
instance.set_status(status)
project = instance.save()
# Create relations
for image in images:
ProjectImage.objects.create(project=project, **image)
except exceptions.ValidationError as e:
errors_messages = e.error_dict if hasattr(e, 'error_dict') else e.error_list
raise serializers.ValidationError(errors_messages)
return project
Hope this help!

Categories

Resources