Reverse relationships in Nested serializer in django rest framework - django

My models.py is as as follows:
class Group(models.Model):
name = models.CharField(max_length=50, unique=True)
class Policy(models.Model):
name = models.CharField(max_length=50, unique=True)
source_group = models.ForeignKey(Group, related_name='source_group')
destination_group = models.ForeignKey(Group, related_name='destination_group')
Since I have two foreign keys, pointing to the same model, I am using related name to avoid clashes.
Now, when I try to create a serializer for Groups in order to list all Policies associated with it, I do the following:
class PolicySerializer(serializers.ModelSerializers):
class Meta:
model = Policy
fields = "__all__"
class GroupSerializer(serializers.ModelSerializer):
policy = PolicySnippetSerializer(source ='source_group', many=True)
class Meta:
model = Group
fields = ['id', 'name', 'policy']
However, this will give me only policies for a a source_group.How did i get all groups associated with a group, source and destination ?

There can be two ways to do this.
Using SerializerMethodField.
By overriding data property method and appending the destination_group data into policy key.
Method 1:
class GroupSerializer(serializers.ModelSerializer):
policy = serializers.SerializerMethodField()
def get_policy(self, obj):
source_groups = PolicySnippetSerializer(obj.source_group.all(), many=True).data
destination_groups = PolicySnippetSerializer(obj.destination_group.all(), many=True).data
return source_groups + destination_groups
# rest of the code
Method 2:
class GroupSerializer(serializers.ModelSerializer):
policy = PolicySnippetSerializer(source ='source_group', many=True)
#property
def data(self):
serializer_data = super().data()
serializer_data['policy'] += PolicySnippetSerializer(self.instance.destination_group.all(), many=True).data
return serializer_data
# rest of the code

Related

Serialize filtered related fields in Django Rest framework

In my project, I have three models: Group, User and Challenge. Each user is a member of some groups and each challenge is intended for one or more groups.
class Group(TimeStampedModel):
name = models.CharField(max_length=255)
class User(AbstractUser):
user_groups = models.ManyToManyField(Group)
class Challenge(TimeStampedModel):
...
groups = models.ManyToManyField(Group, null=True, blank=True)
I also have a serializer for Challenge models that serializes all challenge data and related groups using a GroupSerializer.
class ChallengeSerializer(serializers.ModelSerializer):
groups = GroupSerializer(many=True)
class Meta:
model = Challenge
fields = [..., "groups"]
My current APIView for serializing list of challenges.
class ChallengeList(generics.ListAPIView):
queryset = Challenge.objects.all()
serializer_class = ChallengeSerializer
permission_classes = [permissions.IsAuthenticated]
pagination_class = PageNumberPagination
def get_queryset(self):
user_groups = self.request.user.user_groups.all()
return Challenge.objects.filter(groups__in=user_groups).distinct()
When serializing a Challenge object, a list of all related groups is serialized.
Is it possible to only serialize related Group objects that are also related to our currently logged in user?
you could use a SerializerMethodField() to filter down the Challenges groups to just the user groups. To do this you may also need to pass in serializer context as well
To set up the serializer context:
class ChallengeList(generics.ListAPIView):
...
def get_serializer_context(self):
context = {user_groups: self.request.user.user_groups.all()}
return context
...
Then access this context in the SerializerMethodField in your serializer
class ChallengeSerializer(serializers.ModelSerializer):
groups_of_user = SerializerMethodField()
class Meta:
model = Challenge
fields = [..., "groups_of_user"]
def get_groups_of_user(self, challenge):
user_groups = self.context.get('user_groups')
groups_of_user = challenge.groups & user_groups
return GroupSerializer(groups_of_user, many=True).data
With this implementation you could also add prefetch_related('groups') on your queryset in ChallengeList to improve performance

How to define object-level permissions for foreign-key relationships

I'm having trouble defining object-level permissions for foreign-key relationships in my ModelViewSet. I'm not sure if it's entirely possible what I'm trying to do or if there's a better solution, but any hint in the right direction would be much appreciated. I've shortened the models and serializers for the sake of brevity.
I have the following models:
class Team(models.Model):
name = models.CharField(max_length=50)
class CustomUser(AbstractUser):
teams = models.ManyToManyField(Team)
class Client(models.Model):
name = models.CharField(max_length=50)
owner = models.ForeignKey(Team, on_delete=models.CASCADE)
class FinancialAccount(models.Model):
account_name = models.CharField(max_length=50)
client = models.ForeignKey(Client, on_delete=models.CASCADE)
Then I have the following serializers:
class ClientSerializer(serializers.ModelSerializer):
class Meta:
model = Client
fields = ('name', 'owner')
class FinancialAccountSerializer(serializers.ModelSerializer):
owner = serializers.SerializerMethodField()
class Meta:
model = FinancialAccount
fields = ('name', 'client', 'owner')
def get_owner(self, obj):
return client.owner.name
Then I'm trying to define a permission that I can use in all of my ModelViewSets. I'd like it to be somewhat dynamic as I have many more models than the ones above that are related to Client or even below FinancialAccount. The permission and viewset are as follows:
class IsOwnerTeam(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
teams = request.user.teams.values_list('name', flat=True)
return obj.owner in teams
class FinancialAccountViewSet(viewsets.ModelViewSet):
serializer_class = FinancialAccountSerializer
permission_classes = (IsOwnerTeam, )
def get_queryset(self):
teams = self.request.user.teams.all()
clients = Client.objects.filter(owner__in=teams)
return FinancialAccount.objects.filter(account__in=accounts)
So, right now I'm receiving this error: 'FinancialAccount' object has no attribute 'owner', which makes sense because I don't have an owner field on the FinancialAccount object. But, I thought if I had an owner field in the serializer (and put an owner field in each of the serializers) I could retrieve it that way. Any help would be appreciated!
You can do something like this:
class IsOwnerTeam(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if hasattr(obj, 'client'):
owner = obj.client.owner
else:
owner = obj.owner
teams = request.user.teams.values_list('name', flat=True)
return owner in teams

Nested objects save in DRF

Models:
class Owner(models.Model):
name = models.CharField(max_length=255)
def __unicode__(self):
return self.name
class SomeThing(models.Model):
own_id = models.IntegerField(unique=True)
description = models.CharField(max_length=255, blank=True)
owner = models.ForeignKey(Owner, blank=True, null=True)
def __unicode__(self):
return self.description
Serializers:
class OwnerNameField(serializers.RelatedField):
def to_internal_value(self, data):
pass
def to_representation(self, value):
return value.name
def get_queryset(self):
queryset = self.queryset
if isinstance(queryset, (QuerySet, Manager)):
queryset = queryset.all()
lista = [Owner(name="------------")]
lista.extend(queryset)
return lista
class OwnerSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Owner
fields = ('name', 'id')
class ThingSerializer(serializers.ModelSerializer):
owner = OwnerNameField(queryset=Owner.objects.all())
class Meta:
model = SomeThing
fields = ('own_id', 'description', 'owner')
Basically it works as intended. But when i add some fields to Owner class i would like to see all these fields in output of ThingSerializer (and be able to parse them - string doesn't suit here). I could change field owner to owner = OwnerSerializer() which gives me what i need. But when i want to add SomeThing object (tested in API browser) i also need add new Owner object - and i don't want it, i want use existing Owner object. How can i achieve it?
Finally i got it. This question describes exactly my problem and provided answers work as a charm!

Conditionally Limiting Related Resource Data in TastyPie

I am new to Tastypie (and Django) and am running into a problem with circular many-to-many relationships in my app's api.
I have 3 models RiderProfile, Ride, and RideMemebership. RiderProfilea can belong to multiple Rides, and Rides can have multiple RiderProfile. The many-to-many relationship is mediated by RideMembership. My models look like:
class RiderProfile(models.Model):
user = models.OneToOneField(User)
age = models.IntegerField(max_length=2)
rides = models.ManyToManyField('riderapp.Ride', through="RideMembership")
def __unicode__(self):
return self.user.get_username()
class Ride(models.Model):
name = models.CharField(max_length=64)
riders = models.ManyToManyField(RiderProfile, through="RideMembership")
def __unicode__(self):
return self.name
class RideMembership(models.Model):
rider = models.ForeignKey(RiderProfile)
ride = models.ForeignKey(Ride)
date_joined = models.DateField()
invite_reason = models.CharField(max_length=64)
def __unicode__(self):
return self.rider.user.get_username() + ' to ' + self.ride.name()
My TastyPie resources look like:
class UserResource(ModelResource):
...
class RideResource(ModelResource):
class Meta:
queryset = Ride.objects.all()
resource_name = 'rides'
riders = fields.ToManyField('riderapp.api.RiderProfileResource', 'riders', full=True)
class RiderProfileResource(ModelResource):
class Meta:
queryset = RiderProfile.objects.all()
resource_name = 'riders'
user = fields.ForeignKey(UserResource, 'user', full=True)
rides = fields.ToManyField('riderapp.api.RideResource', 'rides', full=True)
When I GET either a RiderProfile or Ride (list or detail), I get a recursion error because the models are fetching themselves infinitely. I have tried using the RelationalField.use_in parameter, which is very close to what I am trying to accomplish - as it prevents a field from being included based whether the request is for a list or a detail. However, I am trying to remove a resource field based on which endpoint is called.
For instance, a request for /rides:
I would like to have a list of all the RiderProfile items involved, but without their Ride list.
Likewise, a request for /riders:
I would like to have a list of all the Ride items for the RiderProfile, but without their Rider list.
What is the recommended solution for this? I have been playing the with the dehyrdate cycle, but am struggling to modify the set of related resources. I have also read answers about using multiple ModelResources for Rides and Riders. Is there a recommended way to accomplish this?
Thanks in advance for your advice!
Update
I added extra ModelResources for use with each endpoint (RiderProfileForRideResource and RideForRiderProfileResource), and it is working. I am just not sure this is the best approach. It creates additional endpoints that I don't really want to expose. Any thoughts on a better way?
class UserResource(ModelResource):
...
class RideResource(ModelResource):
class Meta:
queryset = Ride.objects.all()
resource_name = 'rides'
riders = fields.ToManyField('riderapp.api.RiderProfileForRideResource', 'riders', full=True)
class RideForRiderProfileResource(ModelResource):
class Meta:
queryset = Ride.objects.all()
resource_name = 'rides_for_riders'
class RiderProfileResource(ModelResource):
class Meta:
queryset = RiderProfile.objects.all()
resource_name = 'riders'
user = fields.ForeignKey(UserResource, 'user', full=True)
rides = fields.ToManyField('riderapp.api.RideForRiderProfileResource', 'rides', full=True)
class RiderProfileForRideResource(ModelResource):
class Meta:
queryset = RiderProfile.objects.all()
resource_name = 'riders_for_ride'
user = fields.ForeignKey(UserResource, 'user', full=True)
class RideMembershipResource(ModelResource):
class Meta:
queryset = RideMembership.objects.all()
resource_name = 'rider_membership'
This might not be the cleanest one way to do it but you could try to remove riders or rides in the dehydrate cycle by checking the resource uri path of the api call you have made
class RideResource(ModelResource):
class Meta:
queryset = Ride.objects.all()
resource_name = 'rides'
riders = fields.ToManyField('riderapp.api.RiderProfileResource', 'riders', full=True)
def dehydrate(self, bundle):
# You make api call to 'riders' and are dehydrating related source RideResource. Path should be of the form API/app_name/riders
# When call made directly to this resource then uri path will be API/app_name/rides and if statement will not be entered
if 'riders' in bundle.request.path:
del bundle.data['riders']
and vice versa for the opposite relation.
You can use a callable for the use_in attribute of your resource field instead of overriding dehydrate.
def riders_check(bundle):
return 'riders' in bundle.request.path
Something like,
riders = fields.ToManyField('riderapp.api.RiderProfileForRideResource', 'riders', full=True, use_in=riders_check)

How to combine mutiple resources in django-tastypie?

Lets say I have three models Submission, Contact and SubmissionContact.
class Submission(models.Model):
title = models.CharField(max_length=255, verbose_name='Title')
...
class Contact(models.Model):
name = models.CharField(max_length=200, verbose_name='Contact Name')
email = models.CharField(max_length=80, verbose_name='Contact Email')
...
class SubmissionContact(models.Model):
submission = models.ForeignKey(Submission)
contact = models.Foreign(Contact, verbose_name='Contact(s)')
Can I read / write to all these three tables using a single ModelResource using tastypie. (basically get and put actions on list and detail in tastypie)
Thanks in advance for any help.
You can nest one model into the other or use the dehydrate cycle to add extra resources to your output, for example consider a Foo and Bar model
class FooResource(ModelResource):
class Meta:
queryset = Foo.objects.all()
resource_name = 'foo'
serializer = Serializer(formats=['xml', 'json'])
excludes = ['date_created']
class BarResource(ModelResource):
foo = fields.ForeignKey(FooResource, attribute='foo', full=True, null=True)
class Meta:
queryset = Bar.objects.all()
resource_name = 'bar'
serializer = Serializer(formats=['xml', 'json'])
If there's no relationship you could also do something like (with large datasets this would cause a lot of database overhead, you might have to rethink your model definitions):
class FooResource(ModelResource):
class Meta:
queryset = Foo.objects.all()
resource_name = 'foo'
serializer = Serializer(formats=['xml', 'json'])
excludes = ['date_created']
def dehydrate(self, bundle):
obj = self.obj_get(id=bundle.data['id'])
bundle.data['bar'] = Bar.objects.get(id=1).name
return bundle