Passing additional attributes all the way thru to nested serializers - python

I've been having issues passing additional attributes thru using Django Rest Framework with nested serializers.
I've created a Document model that has a ForeignKey owner/creator relationship, and several other ForeignKey related models. Some of those other model have an owner/creator ForeignKey associated as well.
class Document(models.Model):
owner = models.ForeignKey('auth.User',related_name='document')
candidate = models.ForeignKey(
Candidate,
on_delete=models.CASCADE,
blank=True,
null=True,
)
class Candidate(models.Model):
owner = models.ForeignKey('auth.User', related_name='candidates')
first_name = models.CharField(max_length=30, blank=True, null=True)
When saving down the Document model with a nested serializer and a custom create() method, I can pass all fields down, however, the nested models don't seem to be able to pick up the Owner field, regardless of how I pass it in. Creating a Candidate alone is fine.
class CandidateSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
class Meta:
model = Candidate
fields = (
'pk',
'first_name',
'owner',
)
class DocumentSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username')
candidate = CandidateSerializer(required=True)
class Meta:
model = Document
fields = (
'owner',
'candidate',
)
def create(self, validated_data):
candidate_data = validated_data.pop('candidate')
document = Document.objects.create(**validated_data)
Candidate.objects.create(**candidate_data)
With DocumentSerializer set up like this, I get errors like this while trying to do a POST to Document with nested fields.
IntegrityError: NOT NULL constraint failed: dossier_candidate.owner_id
When I modify the DocumentSerializer.create() method to try to pick up the owner, it seems that owner = serializers.ReadOnlyField(source='owner.username') is now out of scope, even though it should be under the class.
i.e.,
When I try to create the Candidate object with
Candidate.objects.create(owner, **candidate_data)
I get this error :
NameError at /rest/documents/
global name 'owner' is not defined
When I try this
Candidate.objects.create(self.owner, **candidate_data)
I get this error:
AttributeError: 'DocumentSerializer' object has no attribute 'owner'
What's the proper method of making sure the nested Candidate object is able to be created successfully, picking up the owner field?

First thing first, you won't have the owner to create/update since it's read only.
If you want to get the current user for that, use CurrentUserDefault. It'll be added to the validated_data

Related

Unable to set many to many relationship in overwritten save method

I am trying to create a system where I set up an issue and it automatically creates custom fields that a user will have defined stored in another model. I set my current model up with a many to many relationship to the custom field model and overwrite the save method so that each of the custom defined fields will be added with a default value.
When I use the .add method after saving my issues model, nothing seems to happen, the many to many relationships are not created. The relationships are able to be made within the Django Admin interface.
class Issue(models.Model):
class Meta:
verbose_name = "Issues"
verbose_name_plural = "Issues"
title = models.TextField(null=False, blank=False)
description = models.TextField()
owner = models.ForeignKey(Organisation, on_delete=models.CASCADE)
category = models.ForeignKey(IssueCategory, on_delete=models.CASCADE)
state = models.ForeignKey(IssueStates, on_delete=models.CASCADE)
project = models.ForeignKey(Project, on_delete=models.CASCADE)
assignedTo = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
customFields = models.ManyToManyField(IssueCustomFields, blank=True)
def save(self, *args, **kwargs):
super(Issue, self).save(*args, **kwargs)
for x in IssueCustomFieldDefinitions.objects.filter(owner=self.owner):
issueCustom = IssueCustomFields.objects.create(
value=x.default,
fieldDefinition = x,
owner = self.owner,
)
self.customFields.add(issueCustom)
print(self.customFields.all())
I expect that when the Issue model is saved, it iterates through all the custom fields that th user has set up and creates an instance of it as well as establishing relationships. The relationship is never established (the instances are created though)
many to many relation is not depended on save method . if you assign relation between two model with many to many you don't need to save any of those model .

How to filter a one-to-one generic relation with Django?

I have a moderation model :
class ItemModeration(models.Model):
class Meta:
indexes = [
models.Index(fields=['object_id', 'content_type']),
]
unique_together = ('content_type', 'object_id')
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
item = GenericForeignKey('content_type', 'object_id')
published = models.BooleanField(default=False)
...
A descriptor to attach a moderation object on-the-fly :
class ItemModerationDescriptor(object):
def __init__(self, **default_kwargs):
self.default_kwargs = default_kwargs
def __get__(self, instance, owner):
ctype = ContentType.objects.get_for_model(instance.__class__)
try:
moderation = ItemModeration.objects.get(content_type__pk=ctype.id,
object_id=instance.pk)
except ItemModeration.DoesNotExist:
moderation = ItemModeration(item=instance,**self.default_kwargs)
moderation.save()
return moderation
And a model I want to moderate :
class Product(models.Model):
user = models.ForeignKey(
User,
null=True,
on_delete=models.SET_NULL)
created = models.DateTimeField(
auto_now_add=True,
blank=True, null=True,
)
modified = models.DateTimeField(
auto_now=True,
blank=True, null=True,
)
name = models.CharField(
max_length=PRODUCT_NAME_MAX_LENGTH,
blank=True, null=True,
)
moderation = ItemModerationDescriptor()
Now I can see a product 'published' state easily :
p=Product(name='my super product')
p.save()
print(p.moderation.published)
-> False
The generic relation is useful because I will be able to search the objects to moderate whatever the type is : it could be products, images, comments.
to_moderate_qs = ItemModeration.objects.filter(published=False)
Now, how can I get a filtered list of published products ?
I would like to do something like this
published_products_qs = Product.objects.filter(moderation__published=True, name__icontains='sony')
But, of course, it won't work as moderation attribute is not a Django model field.
How can I do that efficiently ? I am thinking a about an appropriate JOIN, but I cannot see how to do that with django without using raw SQL.
Django has a great built in answer for this: the GenericRelation. Instead of your descriptor, just define a generic relation on your Product model and use it as a normal related field:
from django.contrib.contenttypes.fields import GenericRelation
class Product(models.Model):
...
moderation = GenericRelation(ItemModeration)
Then handle creation as you normally would with a related model, and filtering should work exactly as you stipulated. To work as your current system, you'd have to put in a hook or save method to create the related ItemModeration object when creating a new Product, but that's no different from other related django models. If you really want to keep the descriptor class, you can obviously make use of a secondary model field for the GenericRelation.
You can also add related_query_name to allow filtering the ItemModeration objects based only on the Product content type.
WARNING if you do use a GenericRelation note that it has a fixed cascading delete behavior. So if you don't want ItemModeration object to be deleted when you delete the Product, be careful to add a pre_delete hook or equivalent!
Update
I unintentionally ignored the OneToOne aspect of the question because the GenericForeignKey is a one-to-many relation, but similar functionality can be effected via smart use of QuerySets. It's true, you don't have access to product.moderation as a single object. But, for example, the following query iterates over a filtered list of products and extracts their name, the user's username, and the published date of the related ModerationItem:
Product.objects.filter(...).values_list(
'name', 'user__username', 'moderation__published'
)
You'll have to use the content_type to query the table by specific model type.
like this:
product_type = ContentType.objects.get_for_model(Product)
unpublished_products = ItemModeration.objects.filter(content_type__pk=product_type.id, published=False)
For more details on the topic check contenttypes doc

Assigning current user in rest framework view

I have been getting my head around these basics but I am not getting it right. I am trying to associate my view to my user model using team which is a foreign key. When I try to create of a gps, I get an error saying "team is a required field" but instead it should be read only. The team attribute should be filled automatically with the id of the currentUser
Model
class User(models.Model):
first_name = models.CharField(max_length=200,blank=False)
last_name = models.CharField(max_length=200, blank=False)
class Gps(models.Model):
location = models.CharField(max_length=200,blank=False)
team= models.ForeignKey(User, on_delete=models.CASCADE)
serializers
class GpsSerializer(serializers.ModelSerializer):
class Meta:
model = Gps
fields = ('id','location','team')
view
class Gps_list(generics.ListCreateAPIView):
queryset = Gps.objects.all()
serializer_class = GpsSerializer
team = serializers.PrimaryKeyRelatedField(
read_only=True,
default=serializers.CurrentUserDefault()
)
There are two changes needed. First, team field definition should be moved to serializer class instead of view. Second, you should use Django's contrib.auth.User model instead of your definition of User, as because serializers.CurrentUserDefault() will bring request.user only. So you should remove your User definition and import that to your models.py:
from django.contrib.auth.models import User
Further steps would be to replace read_only=True with queryset=User.objects.all() to allow create.

Django auto save m2m relationship in form using through table

I have a m2m relationship between Servers and Products in Django with a through table called ServerProducts.
class ServerProduct(TimeStampedModel):
# Additional fields may be required in the future
server = models.ForeignKey('Server', on_delete=models.CASCADE)
product = models.ForeignKey('Product', on_delete=models.CASCADE)
class Server(TimeStampedModel):
name = models.CharField(max_length=35)
# ...
products = models.ManyToManyField('Product', through='ServerProduct',
related_name='products', blank=True)
class Product(TimeStampedModel):
name = models.CharField(max_length=45, unique=True)
# ...
servers = models.ManyToManyField(
'Server', through='ServerProduct', related_name='servers')
In my view I have a form which allows users to create a Server and select from a list of all products for the Server to be associted with.
In order to create the ServerProduct objects (for the through table) on each save I have to write the following code inside save().
class ServerForm(forms.ModelForm):
class Meta:
model = Server
fields = '__all__'
def save(self, commit=True):
instance = super(ServerForm, self).save(commit=False)
instance.save()
if instance.products.count():
instance.products.clear()
for product in self.cleaned_data['products']:
ServerProduct.objects.create(server=instance, product=product)
return instance
I want to be able to reuse the form for both Create and Update views. Hence why I have to check if the current Server is associated with any products, and then do instance.products.clear(). To make sure it removes any previous products if they get deselected by a user.
This entire process feels unecessary, especially when I've read a lot about Django's built-in form.save_m2m() method. My question is, is there a simpler way do achieve what I'm doing using Django built-in's?

How to limit the queryset of related fields of serializer based on some request parameters in Django Rest Framework

I have three models Transaction, Business, and Location. They are defined as follows:
class Business(models.Model):
# Can have zero or more locations. A user can have many businesses.
name = models.CharField(max_length=200, validators=[MinLengthValidator(1)])
# ... and some other fields ...
class Location(models.Model):
# Must have one business. Locations cannot exist without a business
suburb = models.CharField(max_length=150, validators=[MinLengthValidator(1)])
business = models.ForeignKey(Business, related_name='locations')
# ... and some other fields ...
class Transaction(models.Model):
# Can have zero or one business
# Can have zero or one location and the location must belong to the business. If business is empty, location must be empty
business = models.ForeignKey(Business, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions')
location = models.ForeignKey(Location, on_delete=models.SET_NULL, null=True, blank=True, related_name='transactions')
# ... and some other fields ...
And the serializers:
class BusinessRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
owner = get_owner_from_context(self.context)
return Business.objects.filter(owner=owner)
class LocationRelatedField(serializers.PrimaryKeyRelatedField):
def get_queryset(self):
params = self.context['request'].query_params
business_params = params.get('business')
if business_params is not None:
owner = get_owner_from_context(self.context)
return Location.objects.filter(owner=owner, business=business_params)
else:
return None
class TransactionSerializer(serializers.ModelSerializer):
business = BusinessRelatedField(required=False, allow_null=True)
location = LocationRelatedField(required=False, allow_null=True)
The problem I was facing was that I didn't know how to restrict the value of Location based on the value of Business. I was manually performing this check inside TransactionSerializer's validate method until it occurred to me to create a PrimaryKeyRelatedField subclass and override the get_queryset method. This seemed like a better approach to me (and it's actually working) but I'd like to know if this is the 'normal' way of doing it.
The other problem I'm now facing is that the 'browsable API' no longer shows any choices for Location which I feel is a hint that I might be doing something wrong.
Any help would be appreciated.
You can override the get_fields() method of the serializer and modify the queryset for business and location fields to the desired values.
get_fields() method is used by the serializer to generate the field names -> field instances mapping when .fields property is accessed on it.
class TransactionSerializer(serializers.ModelSerializer):
class Meta:
model = Transaction
fields = (.., business, transaction)
def get_fields(self):
# get the original field names to field instances mapping
fields = super(TransactionSerializer, self).get_fields()
# get the required parameters
owner = get_owner_from_context(self.context)
business_params = self.context['request'].query_params.get('business')
# modify the queryset
fields['business'].queryset = Business.objects.filter(owner=owner)
fields['location'].queryset = Location.objects.filter(owner=owner, business=business_params)
# return the modified fields mapping
return fields
This is a very late answer, however it would not be different back then.
With the information you provided (in the comments as well) and AFAIK there is no way of doing this unless you manipulate the javascript code of the browsable API's templates and add ajax calling methods to it.
DRF browsable API and DRF HTML and forms may help.

Categories

Resources