I have two classes: Website and WordpressWebsite.
WordpressWebsite subclasses Website.
When an instance of WordpressWebsite is being encoded into JSON, only the attributes of WordpressWebsite are present (and none of the attributes of Website).
My goal is to write a custom encoder which will encode a WordpressWebsite as a Website instead.
This is what I have so far:
from django.core.serializers.json import DjangoJSONEncoder
from websites.models import Website
class WebsiteEncoder(DjangoJSONEncoder):
def default(self, obj):
raise Exception() # TEST
if isinstance(obj, Website) and hasattr(obj, 'website_ptr'):
return super().default(obj.website_ptr)
return super().default(obj)
I have the following test case:
from django.core import serializers
from django.test import TestCase
from websites.models.wordpress import WordpressWebsite
from websites.serialize import WebsiteEncoder
class SerializationTest(TestCase):
def setUp(self):
self.wordpress = WordpressWebsite.objects.create(
domain='test.com'
)
def test_foo(self):
JSONSerializer = serializers.get_serializer("json")
json_serializer = JSONSerializer()
json_serializer.serialize(
WordpressWebsite.objects.all(),
cls=WebsiteEncoder
)
data = json_serializer.getvalue()
print(data)
This test case runs fine. It does not raise an exception.
Does anyone know why WebsiteEncoder.default is not being invoked?
Django models are encoded natively with its serializers. Django's own DjangoJSONEncoder supplies a complete serializer for all possible models with any of the default Django datatypes. If you look at the JSONEncoder.default() documentation, you'll notice that you would only supply encoders for datatypes that are not yet known to the encoder.
Only if you were using a field type which Django doesn't natively support, you could provide an encoder for it - and only that field type - through .default(). Therefore DjangoJSONEncoder isn't what you're looking for.
Trying to make your example work I discovered you can actually customize the process by subclassing django.core.serializers.json.Serializer:
from django.core.serializers.json import Serializer
class WebsiteSerializer(Serializer):
def get_dump_object(self, obj):
return {
"pk": obj.pk,
**self._current,
}
After that, you can make your serializer work in the test case like so:
def test_foo(self):
serializer = WebsiteSerializer()
data = serializer.serialize(WordpressWebsite.objects.all())
print(data)
Related
I have a service in a Django app that I am building where I want to handle get and post requests. I though I should reuse a serializer I've built but in the examples I found, whenever someone wants to use a serializer they create a new object.
This is an implementation where the serializer class is called multiple times to create multiple instances, one each time a request arrives:
from django.http.response import JsonResponse
from django.http.request import RAISE_ERROR, HttpRequest
from rest_framework.parsers import JSONParser
from rest_framework import status
from models import Instrument
from serializers import InstrumentsSerializer
class InstrumentsService():
def __init__(self):
self.serializer: InstrumentsSerializer = None
def get_instruments_by_type(self, instrument_type: str):
if instrument_type is not None:
instruments = Instrument.objects.all()
instruments.filter(instrument_type__icontains=instrument_type)
instruments_serializer = InstrumentsSerializer(instruments, many=True)
else:
raise ValueError("Value type None is not acceptable for 'instrument_type'")
return instruments_serializer.data
def add_instrument(self, instrument_data: Instrument):
instrument_serializer = InstrumentsSerializer(data=instrument_data)
if instrument_serializer.is_valid():
instrument_serializer.save()
How can I use the same serializer and pass different data to it each time? Because in the example I presented, the data are being passed during initialization.
Why not use Python Class Objects?
class InstrumentsService():
serializer = InstrumentsSerializer
def __init__(self):
pass
def get_instruments_by_type(self, instrument_type: str):
if instrument_type is not None:
instruments = Instrument.objects.all()
instruments.filter(instrument_type__icontains=instrument_type)
instruments_serializer = InstrumentsService.serializer(instruments, many=True)
Long gone are the days of creating marshmallow schemas identical to my models. I found this excellent answer that explained how I could auto generate schemas from my SQA models using a simple decorator, so I implemented it and replaced the deprecated ModelSchema for the newer SQLAlchemyAutoSchema:
def add_schema(cls):
class Schema(SQLAlchemyAutoSchema):
class Meta:
model = cls
cls.Schema = Schema
return cls
This worked great... until I bumped into a model with a bloody Enum.
The error: Object of type MyEnum is not JSON serializable
I searched online and I found this useful answer.
But I'd like to implement it as part of the decorator so that it is generated automatically as well. In other words, I'd like to automatically overwrite all Enums in my model with EnumField(TheEnum, by_value=True) when generating the schema using the add_schema decorator; that way I won't have to overwrite all the fields manually.
What would be the best way to do this?
I have found that the support for enum types that was initially suggested only works if OneOf is the only validation class that exists in field_details. I added in some argument parsing (in a rudimentary way by looking for choices after stringifying the results _repr_args() from OneOf) to check the validation classes to hopefully make this implementation more universally usable:
def add_schema(cls):
class Schema(ma.SQLAlchemyAutoSchema):
class Meta:
model = cls
fields = Schema._declared_fields
# support for enum types
for field_name, field_details in fields.items():
if len(field_details.validate) > 0:
check = str(field_details.validate[0]._repr_args)
if check.__contains__("choices") :
enum_list = field_details.validate[0].choices
enum_dict = {enum_list[i]: enum_list[i] for i in range(0, len(enum_list))}
enum_clone = Enum(field_name.capitalize(), enum_dict)
fields[field_name] = EnumField(enum_clone, by_value=True, validate=validate.OneOf(enum_list))
cls.Schema = Schema
return cls
Thank you jgozal for the initial solution, as I really needed this lead for my current project.
This is my solution:
from marshmallow import validate
from marshmallow_sqlalchemy import SQLAlchemyAutoSchema
from marshmallow_enum import EnumField
from enum import Enum
def add_schema(cls):
class Schema(SQLAlchemyAutoSchema):
class Meta:
model = cls
fields = Schema._declared_fields
# support for enum types
for field_name, field_details in fields.items():
if len(field_details.validate) > 0:
enum_list = field_details.validate[0].choices
enum_dict = {enum_list[i]: enum_list[i] for i in range(0, len(enum_list))}
enum_clone = Enum(field_name.capitalize(), enum_dict)
fields[field_name] = EnumField(enum_clone, by_value=True, validate=validate.OneOf(enum_list))
cls.Schema = Schema
return cls
The idea is to iterate over the fields in the Schema and find those that have validation (usually enums). From there we can extract a list of choices which can then be used to build an enum from scratch. Finally we overwrite the schema field with a new EnumField.
By all means, feel free to improve the answer!
In the DRF source code, there's a get_serializer method. It wasn't inherited from object and it's not a method in the CreateModelMixin class. Where does this method come from?
serializer = self.get_serializer(data=request.data)
Here's the larger chunk of code for context.
from __future__ import unicode_literals
from rest_framework import status
from rest_framework.response import Response
from rest_framework.settings import api_settings
class CreateModelMixin(object):
"""
Create a model instance.
"""
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
def perform_create(self, serializer):
serializer.save()
def get_success_headers(self, data):
try:
return {'Location': data[api_settings.URL_FIELD_NAME]}
except (TypeError, KeyError):
return {}
There are a few SO posts that that also use this method. Like this, this, and this. But I still can't figure out where the implementation is.
CreateModelMixin along with all other mixin classes (Eg. ListModelMixin, UpdateModelMixin etc) are defined in rest_framework/mixins.py file.
These mixin classes provide all the basic CRUD operations on a model. You just need to define a serializer_class and queryset in your generic view to perform all these operations. DRF has separated out these common functionality in separate mixin classes so that they can be injected/mixed-in in a view and used as and when required.
In the DRF source code, there's a get_serializer method. It wasn't
inherited from object and it's not a method in the CreateModelMixin
class. Where does this method come from?
In the GenericAPIView, get_serializer method is defined. The combination of different mixin classes along with GenericAPIView class provide us different generic views for different use cases.
class GenericAPIView(views.APIView):
"""
Base class for all other generic views.
"""
def get_serializer(self, *args, **kwargs):
"""
Return the serializer instance that should be used for validating and
deserializing input, and for serializing output.
"""
serializer_class = self.get_serializer_class()
kwargs['context'] = self.get_serializer_context()
return serializer_class(*args, **kwargs)
Other generic views then inherit the relevant mixin along with GenericAPIView.
Eg. CreateAPIView inherit the CreateModelMixin along with GenericAPIView to provide create-only endpoints.
# rest_framework/generics.py
class CreateAPIView(mixins.CreateModelMixin,
GenericAPIView):
...
It helps if you understand class inheritance (not suggesting you don't, though).
CreateModelMixin will be used by a class based view, for example:
class YourViewSet(mixins.CreateModelMixin, viewsets.GenericViewSet):
get_serializer is a method available through GenericViewSet (although there are probably other View classes that it's available on, too).
So, basically if you would use CreateModelMixin only get_serializer would not be available. (You probably wouldn't do that anyway). It's only because you inherit from another class besides CreateModelMixin that the get_serializer method is available at the moment that the create method is executed.
I could give a simple example of what's happening, while I am at it. It's just to give a more simple overview of what's happening.
disclaimer: you might want to keep in mind that I am a junior developer, so this may not be the most Pythonic code out there :P.
class MyMixin:
def create(self):
serializer = self.get_serializer()
return serializer
class FakeView:
def get_serializer(self):
return "Sadly I am just a string, but I could've been a serializer."
class MyFakeView(MyMixin, FakeView):
pass
view = MyFakeView()
serializer = view.create()
print(serializer)
executing this would show you:
Sadly I am just a string, but I could've been a serializer.
Where if you would define MyFakeView like below,
class MyFakeView(MyMixin):
pass
you would receive the following error:
AttributeError: 'MyFakeView' object has no attribute 'get_serializer'
You can see the __file__ or __module__ member of the method (if it has them) to learn that. inspect also has getsourcefile and getsourcelines that use data from the function's code object, specifically, <function>.f_code.co_filename and .co_firstline.
For example, this retrieves source information for a method inherited from DictMixin:
>>> c=ConfigParser._Chainmap()
>>> f=c.__len__
>>> dump(f.__code__) # my custom function that dumps attributes,
# see https://github.com/native-api/dump
<...>
co_filename : /usr/lib/python2.7/UserDict.py
co_firstlineno : 179
<...>
# same with inspect
>>> inspect.getsourcefile(f)
'/usr/lib/python2.7/UserDict.py'
>>> inspect.getsourcelines(f)
([' def __len__(self):\n', ' return len(self.keys())\n'], 179)
>>> inspect.getsource(f)
' def __len__(self):\n return len(self.keys())\n'
# same with __file__
>>> sys.modules[f.__module__].__file__
'/usr/lib/python2.7/UserDict.pyc'
i'm trying to build sort of a "mini django model" for working with Django and MongoDB without using the norel Django's dist (i don't need ORM access for these...).
So, what i'm trying to do is to mimic the standart behavior or "implementation" of default models of django... that's what i've got so far:
File "models.py" (the base)
from django.conf import settings
import pymongo
class Model(object):
#classmethod
def db(cls):
db = pymongo.Connection(settings.MONGODB_CONF['host'], settings.MONGODB_CONF['port'])
#classmethod
class objects(object):
#classmethod
def all(cls):
db = Model.db() #Not using yet... not even sure if that's the best way to do it
print Model.collection
File "mongomodels.py" (the implementation)
from mongodb import models
class ModelTest1(models.Model):
database = 'mymongodb'
collection = 'mymongocollection1'
class ModelTest2(models.Model):
database = 'mymongodb'
collection = 'mymongocollection2'
File "views.py" (the view)
from mongomodels import ModelTest1, ModelTest2
print ModelTest1.objects.all() #Should print 'mymongocollection1'
print ModelTest2.objects.all() #Should print 'mymongocollection2'
The problem is that it's not accessing the variables from ModelTest1, but from the original Model... what's wrong??
You must give objects some sort of link to class that contains it. Currently, you are just hard-coding it to use Model()s atttributes. Because you are not instantiating these classes, you will either have to use either a decorator or a metaclass to create the object class for you in each subclass of Model().
Is it possible to use something similar to the inline relational items from the Django admin to represent embedded models in a ListField?
For Example, I've got the following models:
class CartEntry(model.Model):
product_name=model.CharField( max_length=20 )
quantity = model.IntegerField()
class Cart(model.Model):
line_items = ListField(EmbeddedModelField('CartEntry'))
I've tried using the standard inlining, but I know it's not right:
class CartEntryInline( admin.StackedInline ):
model=CartEntry
class CartAdmin(admin.ModelAdmin)
inlines=[CartEntryInline]
But obviously that doesn't work, since there's no foreign key relation. Is there any way to do this in django-nonrel?
This is not so easy to do out of the box. You will need to manage ListField and EmbeddedModelField type fields in Django's admin module and do some hacking to get it done. You'll have to implement two parts:
Use EmbeddedModelField in Django's admin
You need to define a class that handles EmbeddedModelField objects to make it work with Django's admin. Here is a link where you can find great sample codes. Below are just code blocks for demonstration:
Add this class into your models.py file and use EmbedOverrideField instead of EmbeddedModelField in Cart model:
class EmbedOverrideField(EmbeddedModelField):
def formfield(self, **kwargs):
return models.Field.formfield(self, ObjectListField, **kwargs)
Implement a class in forms.py that has two methods:
class ObjectListField(forms.CharField):
def prepare_value(self, value):
pass # you should actually implement this method
def to_python(self, value):
pass # Implement this method as well
Use ListFields in Django's admin
You also need to define a class that handles ListField objects to make it work with Django's admin. Here is a link where you can find great sample codes. Below are just code blocks for demonstration:
Add this class into your models.py file and ItemsField instead of ListField in Cart model:
class ItemsField(ListField):
def formfield(self, **kwargs):
return models.Field.formfield(self, StringListField, **kwargs)
Implement a class in forms.py that has two methods:
class StringListField(forms.CharField):
def prepare_value(self, value):
pass # you should actually implement this method
def to_python(self, value):
pass # Implement this method as well