Serialize Generic relationships with Django Rest Framework, with write support - django

I have the following model:
class TaggedItem(models.Model):
tag = models.SlugField()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
I am trying to serialize this model in a way that I can assign the content object via an API endpoint
So far I have done this:
class TaggedItemSerializer(serializers.ModelSerializer):
content_object = serializers.RelatedField(read_only=True)
class Meta:
model = TaggedItem
However this is readonly. If I remove the read_only parameter, I must specify the queryset for the field. However, I have many different model types for this generic relationship. It seems like I am duplicating code if I specify all the possible model types both within the serializer and elsewhere in the model.
I could also set the content object through the object_id and content_type fields, but when I do this I get an error.
For example:
{
...
object_id: 1,
content_type: 'auth.User'
}
returns a 400 response with "detail": "JSON parse error - Expected object or value"
How can I make this content_object writable via the DRF api?

Override the .to_internal_value , .validate and .create methods like this:
from django.apps import apps
class TaggedItemSerializer(serializers.ModelSerializer):
class Meta:
model = TaggedItem
read_only_fields = ('content_type', 'object_id', 'content_object')
def to_internal_value(self, data):
object_id = data.pop('object_id')
content_type = data.pop('content_type')
ret = super(ConfigCalcSerializer, self).to_internal_value(data)
ret['object_id'] = object_id
ret['content_type'] = content_type
return ret
def validate(self, data):
object_id = data.pop('object_id')
content_type = data.pop('content_type')
Model = apps.get_model(content_type)
try:
content_object = Model.objects.get(id=object_id)
except Model.DoesNotExist:
raise serializers.ValidationError('Not found')
else:
data['content_object'] = content_object
return data
def create(self, validate_data):
return TaggedItem.objects.create(**validate_data)

Related

Django admin search function in GenerifForeignkey field with Content_object relation

I am trying to build an admin page that lets admins search through 2 fields of the model "SeasonalitiesCalculated". The fields for my search are called "fruit" and "object_id".
"fruit" is a Foreignkey field and returns the "name" field of the corresponding fruit.
"object_id" is Genericforeignkey field that sometimes points at a UUID in a model called "Countries" (with a "country_name" field: Germany) and sometimes points at a UUID in a model called "AdminZones" (with an "admin_zone_name" field: California)
The problem now is that django seems to not have any standard way of searching through GenericForeignkeys. So I tried defining a search function like this:
class SeasonalitiesCalculatedAdmin(admin.ModelAdmin):
list_per_page = 20
def country_or_admin_zone_name(self, obj):
return obj.country_or_admin_zone_name()
country_or_admin_zone_name.short_description = 'Country or Admin Zone'
def search_field(self, obj):
return obj.search_field()
list_display = ('fruit', 'country_or_admin_zone_name', 'content_type', ...)
search_fields = ('fruit__fruit_name', 'search_fields')
the admin page itself works and it also shows the country or admin zone names and other foreignkey related fields properly since I specified this in the SeasonalitiesCalculated model like this
class SeasonalitiesCalculated(LoggingMixinSeasons, Basemodel):
fruit = models.ForeignKey('IngredientOriginals', on_delete=models.PROTECT, related_name='%(class)s_related_ingredient_original')
content_type = models.ForeignKey(ContentType, on_delete=models.PROTECT)
object_id = models.UUIDField(default=uuid.uuid4, editable=False)
content_object = GenericForeignKey('content_type', 'object_id')
...
class Meta:
managed = True
db_table = 'seasonalities_calculated'
verbose_name_plural = 'Seasonalities Calculated'
constraints = [models.UniqueConstraint(fields=['ingredient_original_id', 'content_type_id', 'object_id',], name='unique_seasonalities_calculated')]
def country_or_admin_zone_name(self):
content_object = self.content_object
if isinstance(content_object, Countries):
return content_object.country_name
elif isinstance(content_object, AdminZones1):
return content_object.admin_zone_1_name
else:
return None
def search_field(self):
content_object = self.content_object
if isinstance(content_object, Countries):
return 'content_object__country_name'
elif isinstance(content_object, AdminZones1):
return 'content_object__admin_zone_1_name'
return None
but i just cant figure out how to make the search_fields work becuase of the content object relation.
i tried overriding the standard search query but without success...
any help is much appreciated

Error after overriding create method in serializer

I'm Overriding create method of serializer in order to manipulate validated_data and create object in a model, Although it works, in the end I get below error, i am not able to figure out why after lot of research.
AttributeError: Got AttributeError when attempting to get a value for field `shift_time` on serializer `PunchRawDataAndroidSerializer`.
The serializer field might be named incorrectly and not match any attribute or key on the `PunchRawData` instance.
Original exception text was: 'PunchRawData' object has no attribute 'shift_time'.
class PunchRawDataAndroidSerializer(serializers.ModelSerializer):
employee_id = serializers.CharField()
shift_id = serializers.CharField()
work_location_id = serializers.CharField()
shift_time = serializers.TimeField()
class Meta:
model = PunchRawData
fields = ['employee_id', 'shift_id','work_location_id', 'punch_type', 'actual_clock_datetime',
'emp_photo', 'created_at', 'updated_at','shift_time']
def create(self, validated_data):
validated_data.pop('shift_time')
request_data = self.context.get('request')
user = request_data.user
validated_data['user'] = user
data = validated_data
return PunchRawData.objects.create(**data)
class PunchRawDataAndroidViewSet(viewsets.ModelViewSet):
serializer_class = PunchRawDataAndroidSerializer
parser_classes = (MultiPartParser, FileUploadParser)
edit:
class PunchRawData(models.Model):
PUNCH_TYPES = [("in", "Punch IN"), ("out", "Punch Out")]
employee = models.ForeignKey(Employee, related_name="punch_employee", on_delete=models.CASCADE)
shift = models.ForeignKey(WorkShift, on_delete=models.CASCADE)
work_location = models.ForeignKey(HRMLocation, blank=True, on_delete=models.CASCADE,
null=True, related_name="punch_work_location")
punch_type = models.CharField(max_length=255, null=True, blank=True, choices=PUNCH_TYPES)
user = models.ForeignKey("useraccounts.User", on_delete=models.CASCADE)
actual_clock_datetime = models.DateTimeField(null=True, blank=True)
emp_photo = models.ImageField(upload_to="selfies/%Y/%m/%d/%I/%M/%S/")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
strl = "{emp_id} [{shift_id}]".format(emp_id=self.employee.emp_id,
shift_id=self.shift.shift_id)
return strl
class Meta:
verbose_name = "Punch Raw Data"
verbose_name_plural = "Punch Raw Data"
I get shift_time from frontend and it is not from model, hence i'm poping it out from validated_data in create method. is error related to modelviewset?
Your model doesn't have the shift_time attribute. So if you try to save it, you will end with
PunchRawData() got an unexpected keyword argument 'shift_time'
At the other hand you are getting AttributeError, because serializers.to_representation() tries to get a non-existing attribute when showing your freshly saved object.
If this should be a read-only attribute, you may do the following:
shift_time = serializers.TimeField(read_only=True)
and than remove the
validated_data.pop('shift_time')
from PunchRawDataAndroidSerializer.create(). You don't need this any more, because it is never submitted from your client.
If you need the opposite – your client should provide you that field, but you don't want it saved in your model, than the only thing, you should do, is:
shift_time = serializers.TimeField(write_only=True)
And if you need it to be bidirectional, than you should add it to your model.
Hope this helps.
Adding to #wankata's answer we can override __init__ method to have write_only field for only create method.
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.context['view'].action == 'create':
self.fields['shift_time'].write_only = True
Generic viewsets of django-rest-framework return the serialized representation of the model in response, so it's try to serialize the model including the shift_time key.
To avoid this problem you can specify the shift_time field as write_only. documentation
modify the Meta class on your model
class Meta:
model = PunchRawData
fields = ['employee_id', 'shift_id','work_location_id', 'punch_type', 'actual_clock_datetime',
'emp_photo', 'created_at', 'updated_at','shift_time']
extra_kwargs = {'shift_time': {'write_only': True}}

GenericForeinKey retrieve field value in models.py

I am trying to retrieve a field from a GenericForeignKey and cannot make it work in the model.py - it works in the admin.py though.
models.py:
class Run(models.Model):
name = models.CharField(max_length=100)
class TaskRunRelation(models.Model):
limit = models.Q(app_label = 'thisapp', model = 'run') | models.Q(app_label = 'thisapp', model = 'runb')
content_type = models.ForeignKey(ContentType, limit_choices_to = limit)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
def modeltestname(self):
self.content_object.name
admin.py:
class TaskRelationAdmin(admin.ModelAdmin):
list_display = ['modeltestname','mytestname']
def mytestname(self,obj):
return obj.content_object.name
So mytestname shows the correct value in the Admin whereas modeltestname shows "(None)". Why is this not working in the model.py? Am I missing something how Genericforeignkey works or is there any other mistake in there?
That is because you are not returning anything from modeltestname. If a function or method does not return anything explictly, it would return None by default. Hence the result
So change the class method to
def modeltestname(self):
return self.content_object.name

Django validation of generic relation from a GenericInlineModelAdmin

I'm trying to validate a generic relation object saved from a GenericInlineModelAdmin form.
When the object is created object_id and content_type are set to None, and I cannot access it's related object, but when the object is updated they are properly set.
Here is the sample code:
In models.py:
class Article(models.Model):
title = models.CharField(max_length=32)
body = models.TextField()
class TaggedItem(models.Model):
tag = models.SlugField()
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
def clean(self, exclude=None):
pass
In admin.py:
class InlineTags(generic.GenericTabularInline):
model = TaggedItem
class ArticleAdmin(admin.ModelAdmin):
inlines = [InlineTags]
admin.site.register(Article, ArticleAdmin)
If you add a tag, in TaggedItem.clean() method self.object_id and self.content_type are set to None. If the tag is being edited they are properly set.
I have tried this on both django 1.4.x and 1.5.x.
It seems this is an unresolved bug in Django (issue #19255).
I have yet to test it, but since you are saving the tags in the admin you may be able to work around this issue by adding a custom ModelForm like so:
class InlineTagsForm(forms.ModelForm):
def clean(self):
""" Validate object_id & content_type fields """
assert self.cleaned_data.get('object_id')
assert self.cleaned_data.get('content_type')
return self.cleaned_data
class InlineTags(generic.GenericTabularInline):
model = TaggedItem
form = InlineTagsForm

Generic Relation Constraints in Django

I want the a counterpart of Tag (BlogPost) to have at least 1 instance of Tag or it shouldn't be created. (same effect like null=False). I tried a lot but couldn't figure out to apply these contrains. Any ideas?
class Tag(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey('content_type', 'object_id')
text = models.CharField("text", max_length=255)
class Meta:
unique_together = ('content_type', 'object_id', 'text',)
class BlogPost(models.Model):
title = models.CharField("title", max_length=255)
tags = generic.GenericRelation(Tag, verbose_name="tags")
class TagInline(generic.GenericTabularInline):
model = Tag
extra = 1
class BlogPostAdmin(admin.ModelAdmin):
inlines = (TagInline,)
If you want this in the form of a Database constraint, then I'm not sure that such a thing exists.
Otherwise I would go with overriding the clean( self ) function on your model.
This can be used for custom validation.
def clean( self ):
# validate that this model has one or more tag