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
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
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}}
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
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
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