Related
I'm using django-filter together with DRF. I have a favourite-model, which is linked to several other models through a GenericRelation.
To filter for entries which have a favourite-flag, I've created a custom FavouriteFilter, which I add to the respective model. I would like to query for the content_type_id of the respective model in order to limit the results from Favourite. However, I don't know how I can pass down the model to the filter-method in the FavouriteFilter.
Here's a code snippet to illustrate the issue:
class ProjectFilter(BaseFilter):
favourite_only = FavouriteFilter()
class FavouriteFilter(django_filters.BooleanFilter):
"""
A custom filter which returns a users favourites of an element
"""
def __init__(self, *args, **kwargs):
# gettext_lazy breaks the OpenAPI generation => use gettext instead
kwargs['label'] = gettext("My favourites")
super(FavouriteFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
if value == True:
user = get_current_user()
content_type = ContentType.objects.get_for_model(<model>)
return qs.filter(pk__in=Favourite.objects
.filter(owner_id=user)
.filter(content_type_id=content_type)
.values_list('object_id', flat=True)
)
else:
return qs
In this example, the <model>-attribute is missing. How can I pass down this information from Project to the filter?
Keyword arguments can be passed down to the filter, but they need to be removed from the kwarg-dict before the super()-method is called. Otherwise they get passed on to the superclass, the superclass's __init__()-method doesn't know the keyword and a TypeError is thrown:
TypeError: __init__() got an unexpected keyword argument 'model'
In the example above, the superclass is django_filters.BooleanFilter respectively django_filters.Filter.
Using the dict.pop()-method, the keyword is removed from the kwargs-dictionary and at the same time we can save it for further use. Since content_type never changes after initialization, it can already be set in __init__().
Here's a working example of the code above, where Project is the django-model I want to pass down to the filter:
class ProjectFilter(BaseFilter):
favourite_only = FavouriteFilter(model=Project)
class FavouriteFilter(django_filters.BooleanFilter):
"""
A custom filter which returns a users favourites of an element
"""
def __init__(self, *args, **kwargs):
# gettext_lazy breaks the OpenAPI generation => use gettext instead
kwargs['label'] = gettext("My favourites")
model = kwargs.pop('model')
self.content_type = ContentType.objects.get_for_model(model)
super(FavouriteFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
if value == True:
user = get_current_user()
return qs.filter(pk__in=Favourite.objects
.filter(owner_id=user)
.filter(content_type_id=self.content_type)
.values_list('object_id', flat=True)
)
else:
return qs
For my specific use-case, where I'm looking for the model that is using the filter, the model is available through the queryset as qs.model. The code-snippet looks like this:
class ProjectFilter(BaseFilter):
favourite_only = FavouriteFilter()
class FavouriteFilter(django_filters.BooleanFilter):
"""
A custom filter which returns a users favourites of an element
"""
def __init__(self, *args, **kwargs):
# gettext_lazy breaks the OpenAPI generation => use gettext instead
kwargs['label'] = gettext("My favourites")
super(FavouriteFilter, self).__init__(*args, **kwargs)
def filter(self, qs, value):
if value == True:
user = get_current_user()
content_type = ContentType.objects.get_for_model(qs.model)
return qs.filter(pk__in=Favourite.objects
.filter(owner_id=user)
.filter(content_type_id=content_type)
.values_list('object_id', flat=True)
)
else:
return qs
I created cusom QuerySet and Manager to serialize my data.
class UpdateQuerySet(models.QuerySet):
def serialize(self):
return serialize("json", self)
class UpdateManager(models.Manager):
def get_queryset(self):
return UpdateQuerySet(self.model, using=self._db)
class Update(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
objects = UpdateManager()
Then, when I'am trying to get this data, it worked with this:
json_data = Update.objects.filter(id=1).serialize()
but raises AttributeError ('Update' object has no attribute 'serialize') with this:
json_data = Update.objects.get(id=1).serialize()
You also want to add serialize to the UpdateManager
class UpdateManager(models.Manager):
def get_queryset(self):
return UpdateQuerySet(self.model, using=self._db)
def serialize(self):
return self.get_queryset().serialize()
Your Update.objects.filter(id=1) returns an UpdateQueryset, and that QuerySet offers a .serialize(…) method. The .get(id=1) function will return an Update model object, so not a QuerySet, and a model by default does not offer a .serialize(…) method.
You can however implement this yourself, and even generalize the QuerySet to work with all sorts of models, like:
class SerializableQuerySet(models.QuerySet):
def serialize(self):
return serialize('json', self)
class SerializableManager(models.Manager):
_queryset_class = SerializableQuerySet
def serialize(self, *args, **kwargs):
return self.get_queryset().serialize(*args, **kwargs)
class SerializableModel(models.Model):
objects = SerializableManager()
def serialize(self):
return serialize('json', [self])
class Meta:
abstract = True
class Update(SerializableModel):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
So now all models that inherit from SerializableModel, like Update for example now will use a SerializableManager, and furthermore will inherit a .serialize() method, such that you can also work with Update.objects.get(pk=1).serialize() for example.
I used django-taggit to add tags to my model. Django Version: 2.2.10, Python Version: 3.8.1
Now I'm trying to integrate tags with django rest-framework, e.g. CREATE/UPDATE/REMOVE model instances with/without tags.
My problem: I'm not able to create (via rest api) a new instance of my model with tags. I can GET model instances without problems.
My models.py:
from taggit.managers import TaggableManager
class Task(models.Model):
name = models.CharField(max_length=100, blank=False)
...
tags = TaggableManager(blank=True)
def get_tags(self):
""" names() is a django-taggit method, returning a ValuesListQuerySet
(basically just an iterable) containing the name of each tag as a string
"""
return self.tags.names()
def __str__(self):
return self.title
My serializers.py:
class TagsField(serializers.Field):
""" custom field to serialize/deserialize TaggableManager instances.
"""
def to_representation(self, value):
""" in drf this method is called to convert a custom datatype into a primitive,
serializable datatype.
In this context, value is a plain django queryset containing a list of strings.
This queryset is obtained thanks to get_tags() method on the Task model.
Drf is able to serialize a queryset, hence we simply return it without doing nothing.
"""
return value
def to_internal_value(self, data):
""" this method is called to restore a primitive datatype into its internal
python representation.
This method should raise a serializers.ValidationError if the data is invalid.
"""
return data
class TaskSerializer(serializers.ModelSerializer):
# tags field in Task model is implemented via TaggableManager class from django-taggit.
# By default, drf is not able to serialize TaggableManager to json.
# get_tags() is a method of the Task model class, which returns a Queryset containing
# the list of tags as strings. This Queryset can be serialized without issues.
tags = TagsField(source="get_tags")
class Meta:
model = Task
fields = [
"name",
...,
"tags",
]
Whenever I try to create a new instance of my Task model via POST api, I get the following error:
TypeError at /taskdrop/v1/task/
Got a `TypeError` when calling `Task.objects.create()`. This may be because you have a writable field on the serializer class that is not a valid argument to `Task.objects.create()`. You may need to make the field read-only, or override the TaskSerializer.create() method to handle this correctly.
Original exception was:
Traceback (most recent call last):
File "/home/daniele/prj/ea/TaskDrop/venv/lib/python3.8/site-packages/rest_framework/serializers.py", line 948, in create
instance = ModelClass._default_manager.create(**validated_data)
File "/home/daniele/prj/ea/TaskDrop/venv/lib/python3.8/site-packages/django/db/models/manager.py", line 82, in manager_method
return getattr(self.get_queryset(), name)(*args, **kwargs)
File "/home/daniele/prj/ea/TaskDrop/venv/lib/python3.8/site-packages/django/db/models/query.py", line 420, in create
obj = self.model(**kwargs)
File "/home/daniele/prj/ea/TaskDrop/venv/lib/python3.8/site-packages/django/db/models/base.py", line 501, in __init__
raise TypeError("%s() got an unexpected keyword argument '%s'" % (cls.__name__, kwarg))
TypeError: Task() got an unexpected keyword argument 'get_tags'
I'm kinda stuck right now...the field is definitely not read-only and regarding overriding the TaskSerializer.create() method, I don't know precisely how to do that.
Plus, I'm a bit confused about TagsField(serializers.Field) vs .create() method override. From my understanding, if I create a custom serializers field, there should be no additional need to override .create().
Finally, I tried to use django-taggit-serializer without success: the model gets created but the passed tags are just missing.
How can I fix this? Thanks.
Ok, I managed to make it work.
Leaving here the solution for others:
The reson I was getting TypeError: Task() got an unexpected keyword argument 'get_tags' is because drf was trying to use the return value of to_internal_value() to fill in the 'get_tags' field of my model.
Now, 'get_tags' is just method name of my model Task class, not a real field, hence the error. Drf learned about 'get_tags' as a field name when I used tags = TagsField(source="get_tags") in my serializer.
I worked around this issue overriding the create() method of my serializer, in this way:
class TaskSerializer(serializers.ModelSerializer):
# tags field in Task model is implemented via TaggableManager class from django-taggit.
# By default, drf is not able to serialize TaggableManager to json.
# get_tags() is a method of the Task model class, which returns a Queryset containing
# the list of tags as strings. This Queryset can be serialized without issues.
tags = TagsField(source="get_tags")
# variables = VariableSerializer()
def create(self, validated_data):
# using "source=get_tags" drf "thinks" get_tags is a real field name, so the
# return value of to_internal_value() is used a the value of a key called "get_tags" inside validated_data dict. We need to remove it and handle the tags manually.
tags = validated_data.pop("get_tags")
task = Task.objects.create(**validated_data)
task.tags.add(*tags)
return task
class Meta:
model = Task
# we exclude all those fields we simply receive from Socialminer
# whenever we get a task or its status
fields = [
"name",
...
"tags",
]
I think you may need some sort of tag serializer setup.
So in your TaskSerializer I would have: tags = TagSerializer(many=True, read_only=False)
from serializers import (
TagListSerializerField,
TagSerializer
)
class TaskSerializer(TagSerializer, serializers.ModelSerializer):
# tags field in Task model is implemented via TaggableManager class from django-taggit.
# By default, drf is not able to serialize TaggableManager to json.
# get_tags() is a method of the Task model class, which returns a Queryset containing
# the list of tags as strings. This Queryset can be serialized without issues.
tags = TagListSerializerField()
class Meta:
model = Task
fields = [
"name",
...,
"tags",
]
I implemented this before many years ago, the TagList, TagListSerializerField and TagSerializer you want is this:
import six
import json
from django.utils.translation import ugettext_lazy as _
from rest_framework import serializer
class TagList(list):
def __init__(self, *args, **kwargs):
pretty_print = kwargs.pop("pretty_print", True)
list.__init__(self, *args, **kwargs)
self.pretty_print = pretty_print
def __add__(self, rhs):
return TagList(list.__add__(self, rhs))
def __getitem__(self, item):
result = list.__getitem__(self, item)
try:
return TagList(result)
except TypeError:
return result
def __str__(self):
if self.pretty_print:
return json.dumps(
self, sort_keys=True, indent=4, separators=(',', ': '))
else:
return json.dumps(self)
class TagListSerializerField(serializers.Field):
child = serializers.CharField()
default_error_messages = {
'not_a_list': _(
'Expected a list of items but got type "{input_type}".'),
'invalid_json': _('Invalid json list. A tag list submitted in string'
' form must be valid json.'),
'not_a_str': _('All list items must be of string type.')
}
order_by = None
def __init__(self, **kwargs):
pretty_print = kwargs.pop("pretty_print", True)
style = kwargs.pop("style", {})
kwargs["style"] = {'base_template': 'textarea.html'}
kwargs["style"].update(style)
super(TagListSerializerField, self).__init__(**kwargs)
self.pretty_print = pretty_print
def to_internal_value(self, value):
if isinstance(value, six.string_types):
value = value.split(',')
if not isinstance(value, list):
self.fail('not_a_list', input_type=type(value).__name__)
for s in value:
if not isinstance(s, six.string_types):
self.fail('not_a_str')
self.child.run_validation(s)
return value
def to_representation(self, value):
if not isinstance(value, TagList):
if not isinstance(value, list):
if self.order_by:
tags = value.all().order_by(*self.order_by)
else:
tags = value.all()
value = [tag.name for tag in tags]
value = TagList(value, pretty_print=self.pretty_print)
return value
class TagSerializer(serializers.Serializer):
def create(self, validated_data):
to_be_tagged, validated_data = self._pop_tags(validated_data)
tag_object = super(TaggitSerializer, self).create(validated_data)
return self._save_tags(tag_object, to_be_tagged)
def update(self, instance, validated_data):
to_be_tagged, validated_data = self._pop_tags(validated_data)
tag_object = super(TaggitSerializer, self).update(
instance, validated_data)
return self._save_tags(tag_object, to_be_tagged)
def _save_tags(self, tag_object, tags):
for key in tags.keys():
tag_values = tags.get(key)
getattr(tag_object, key).set(*tag_values)
return tag_object
def _pop_tags(self, validated_data):
to_be_tagged = {}
for key in self.fields.keys():
field = self.fields[key]
if isinstance(field, TagListSerializerField):
if key in validated_data:
to_be_tagged[key] = validated_data.pop(key)
return (to_be_tagged, validated_data)
Is it possible to make a Django Proxy Field that has access to another field, but doesn't save anything to the database for it's own value(s), and doesn't have a database column for itself?
The use case for this is we'd like to store values in a JsonField, but be able to use the built in validations of Django Fields. A second benefit of this would being able to add new fields (with validation capability) without affecting the database schema.
The sudo code would probably look something like this:
from django.db import models
from django.contrib.postgres.fields import JsonField
class ProxyInitMixin(object):
def __init__(self, *args, *kwargs):
# some logic that will hold values if set on the Model
# but won't create a column or save anything to the
# database for this Field.
super(ProxyInitMixin, self).__init__(*args, **kwargs)
class ProxyIntegerField(ProxyInitMixin, models.Field):
pass
class ProxyCharField(ProxyInitMixin, models.Field):
pass
class MyModel(models.Model):
proxy_int = ProxyIntegerField()
proxy_char = ProxyCharField()
data = JsonField()
def save(self, *args, **kwargs):
self.data = {
'foo': self.proxy_int,
'bar': self.proxy_char
}
return super(MyModel, self).save(*args, **kwargs)
There are proxy models in django, But I am not sure if it has something like proxy fields.
For your use case, you can do as mentioned below:
Create a list of fields with each field containing name, type, nullable, etc.
Add a function in your model to return actual django rest framework (DRF) field class instance, corresponding to each field type passed to it.
Use DRF inbuilt field class validation to validate your field data against specified field type in save().
In addition to automatic validation, you will also get automatic type conversion. Eg. If user entered number 1 as text: "1" for a integer field then it will automatically convert it back to integer 1. Same way, it will work for float, Bool, Char, etc
`
from django.db import models
from rest_framework.fields import IntegerField, FloatField, BooleanField, DateTimeField, CharField
class MyModel(models.Model):
FIELDS = [{'field_name': 'proxy_int', 'field_type': 'int', 'null_allowed': 'True'},
{'field_name': 'proxy_char', 'field_type': 'string', 'null_allowed': 'True'}]
data = JsonField()
def field_type(self, field):
if field.field_type == 'int':
return IntegerField()
elif field.field_type == 'float':
return FloatField()
elif field.field_type == 'bool':
return BooleanField()
elif field.field_type == 'date':
return DateTimeField()
elif self.value_type == 'string':
return CharField()
return CharField()
def save(self, *args, **kwargs):
data = kwargs.get('data', {})
new_data = {}
for (field in FIELDS)
field_name = field['field_name']
field_type = field['field_type']
field_value = data.get(field_name, None)
validated_value = self.field_type(field_type).run_validation(field_value)
new_data[field_name] = validated_value
kwargs['data'] = new_data
return super(MyModel, self).save(*args, **kwargs)`
You may try and figure out django's field classes (Instead of DRF) and use them for validation, if required.
You can inherit this new MyModel class to achieve similar capability in other models and to reuse code.
In order to make the field virtual, you need to:
Override the Field.get_attname_column() method, which must return two-tuple attname, None as the value for attname and column.
Set the private_only parameter to True in the Field.contribute_to_class() method.
A proxy field must also have a reference to the concrete field in order to be able to access to it. Here I will use the concrete_field parameter.
class ProxyMixin(object):
"""
A mixin class that must be mixed-in with model fields.
The descriptor interface is also implemented in this mixin
class to keep value getting/setting logic on the Model.
"""
def __init__(self, *args, concrete_field=None, **kwargs):
self._concrete_field = concrete_field
super().__init__(*args, **kwargs)
def check(self, **kwargs):
return [
*super().check(**kwargs),
*self._check_concrete_field(),
]
def _check_concrete_field(self):
try:
self.model._meta.get_field(self._concrete_field)
except FieldDoesNotExist:
return [
checks.Error(
"The %s concrete field references the "
"nonexistent field '%s'." % (self.__class__.__name__, self._concrete_field),
obj=self,
id='myapp.E001',
)
]
else:
return []
def get_attname_column(self):
attname, column = super().get_attname_column()
return attname, None
def contribute_to_class(self, cls, name, private_only=False):
super().contribute_to_class(cls, name, private_only=True)
setattr(cls, name, self)
#property
def concrete_field(self):
"""
Returns the concrete Field instance.
"""
return self.model._meta.get_field(self._concrete_field)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
if self._concrete_field is not None:
kwargs['concrete_field'] = self._concrete_field
return name, path, args, kwargs
def __get__(self, instance, owner=None):
if instance is None:
return self
return getattr(instance, self._concrete_field)
def __set__(self, instance, value):
setattr(instance, self._concrete_field, value)
If you are sure that the concrete field represents a dict-like object, then you can change the logic for getting / setting value. Maybe something like this:
def __get__(self, instance, owner=None):
if instance is None:
return self
data = getattr(instance, self._concrete_field) or {}
return data.get(self.name, self.get_default())
def __set__(self, instance, value):
data = getattr(instance, self._concrete_field)
if data is None:
setattr(instance, self._concrete_field, {})
data = getattr(instance, self._concrete_field)
data[self.name] = value
Is there a way to have any #property definitions passed through to a json serializer when serializing a Django model class?
example:
class FooBar(object.Model)
name = models.CharField(...)
#property
def foo(self):
return "My name is %s" %self.name
Want to serialize to:
[{
'name' : 'Test User',
'foo' : 'My name is Test User',
},]
You can extend Django's serializers without /too/ much work. Here's a custom serializer that takes a queryset and a list of attributes (fields or not), and returns JSON.
from StringIO import StringIO
from django.core.serializers.json import Serializer
class MySerializer(Serializer):
def serialize(self, queryset, list_of_attributes, **options):
self.options = options
self.stream = options.get("stream", StringIO())
self.start_serialization()
for obj in queryset:
self.start_object(obj)
for field in list_of_attributes:
self.handle_field(obj, field)
self.end_object(obj)
self.end_serialization()
return self.getvalue()
def handle_field(self, obj, field):
self._current[field] = getattr(obj, field)
Usage:
>>> MySerializer().serialize(MyModel.objects.all(), ["field1", "property2", ...])
Of course, this is probably more work than just writing your own simpler JSON serializer, but maybe not more work than your own XML serializer (you'd have to redefine "handle_field" to match the XML case in addition to changing the base class to do that).
The solution worked well that is proposed by M. Rafay Aleem and Wtower, but it's duplicated lot of code. Here is an improvment:
from django.core.serializers.base import Serializer as BaseSerializer
from django.core.serializers.python import Serializer as PythonSerializer
from django.core.serializers.json import Serializer as JsonSerializer
class ExtBaseSerializer(BaseSerializer):
def serialize_property(self, obj):
model = type(obj)
for field in self.selected_fields:
if hasattr(model, field) and type(getattr(model, field)) == property:
self.handle_prop(obj, field)
def handle_prop(self, obj, field):
self._current[field] = getattr(obj, field)
def end_object(self, obj):
self.serialize_property(obj)
super(ExtBaseSerializer, self).end_object(obj)
class ExtPythonSerializer(ExtBaseSerializer, PythonSerializer):
pass
class ExtJsonSerializer(ExtPythonSerializer, JsonSerializer):
pass
How to use it:
ExtJsonSerializer().serialize(MyModel.objects.all(), fields=['field_name_1', 'property_1' ...])
This is a combination of M. Rafay Aleem and Wtowers answer and caots.
This is DRY and lets you only specify the extra props instead of all fields and props as in caots version.
from django.core.serializers.json import Serializer as JsonSerializer
from django.core.serializers.python import Serializer as PythonSerializer
from django.core.serializers.base import Serializer as BaseSerializer
class ExtBaseSerializer(BaseSerializer):
def serialize(self, queryset, **options):
self.selected_props = options.pop('props')
return super(ExtBaseSerializer, self).serialize(queryset, **options)
def serialize_property(self, obj):
model = type(obj)
for field in self.selected_props:
if hasattr(model, field) and type(getattr(model, field)) == property:
self.handle_prop(obj, field)
def handle_prop(self, obj, field):
self._current[field] = getattr(obj, field)
def end_object(self, obj):
self.serialize_property(obj)
super(ExtBaseSerializer, self).end_object(obj)
class ExtPythonSerializer(ExtBaseSerializer, PythonSerializer):
pass
class ExtJsonSerializer(ExtPythonSerializer, JsonSerializer):
pass
How to use it:
ExtJsonSerializer().serialize(MyModel.objects.all(), props=['property_1', ...])
Things have changed a bit since 2010, so the answer of #user85461 seems to no longer be working with Django 1.8 and Python 3.4. This is an updated answer with what seems to work for me.
from django.core.serializers.base import Serializer as BaseSerializer
from django.core.serializers.python import Serializer as PythonSerializer
from django.core.serializers.json import Serializer as JsonSerializer
from django.utils import six
class ExtBaseSerializer(BaseSerializer):
""" Abstract serializer class; everything is the same as Django's base except from the marked lines """
def serialize(self, queryset, **options):
self.options = options
self.stream = options.pop('stream', six.StringIO())
self.selected_fields = options.pop('fields', None)
self.selected_props = options.pop('props', None) # added this
self.use_natural_keys = options.pop('use_natural_keys', False)
self.use_natural_foreign_keys = options.pop('use_natural_foreign_keys', False)
self.use_natural_primary_keys = options.pop('use_natural_primary_keys', False)
self.start_serialization()
self.first = True
for obj in queryset:
self.start_object(obj)
concrete_model = obj._meta.concrete_model
for field in concrete_model._meta.local_fields:
if field.serialize:
if field.rel is None:
if self.selected_fields is None or field.attname in self.selected_fields:
self.handle_field(obj, field)
else:
if self.selected_fields is None or field.attname[:-3] in self.selected_fields:
self.handle_fk_field(obj, field)
for field in concrete_model._meta.many_to_many:
if field.serialize:
if self.selected_fields is None or field.attname in self.selected_fields:
self.handle_m2m_field(obj, field)
# added this loop
if self.selected_props:
for field in self.selected_props:
self.handle_prop(obj, field)
self.end_object(obj)
if self.first:
self.first = False
self.end_serialization()
return self.getvalue()
# added this function
def handle_prop(self, obj, field):
self._current[field] = getattr(obj, field)
class ExtPythonSerializer(ExtBaseSerializer, PythonSerializer):
pass
class ExtJsonSerializer(ExtPythonSerializer, JsonSerializer):
pass
Usage:
>>> ExtJsonSerializer().serialize(MyModel.objects.all(), fields=['myfield', ...], props=['myprop', ...])
You can get all of the properties of a class using some black magic:
def list_class_properties(cls):
return [k for k,v in cls.__dict__.iteritems() if type(v) is property]
For example:
>>> class Foo:
#property
def bar(self):
return "bar"
>>> list_class_properties(Foo)
['bar']
Then you can build the dictionary and serialize it from there.