Access Django model instance in ModelForm's FormField definions - django

in one of my admin forms, I override a model form field (location) in order to use a different, map specific form field (olwidget). This MapField should include a layer (InfoLayerField) that displays all other model instances but the one that is being edited at the moment. Right now, it displays all the model instances (see MyModel.objects.all()) which means, if a model is edited the current location is displayed twice.
In order to achieve this, I'd have to exclude the current edited model instance from the QuerySet used in InfoLayerField (something like MyModel.objects.exclude(pk=self.instance.pk)). But since form fields are defined as static variables, I can't access self.instance.
Is there any way to achieve this?
# models.py
class MyModel(models.Model):
name = models.CharField(max_length=200)
location = models.PointField(blank=True, null=True)
# admin.py
from olwidget.fields import MapField, EditableLayerField, InfoLayerField
from olwidget.utils import get_ewkt
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = MyModel
location = MapField([
EditableLayerField({
'geometry': 'point',
'name': 'location',
}),
InfoLayerField(
[(get_ewkt(m.location), m.name) for m in MyModel.objects.all() if m.location ], {
'geometry': 'point',
'name': 'other locations',
'cluster': True,
'cluster_display': 'list',
}
)
])
class MyModelOlwidgetAdmin(admin.ModelAdmin, GeoModelAdmin):
form = MyModelAdminForm
...
Thanks for any hint's.

I think you just need to override the __init__ on your form:
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = MyModel
def __init__(self,*args,**kwargs):
super(MyModelAdminForm, self).__init__(*args,**kwargs)
qs = MyModel.objects.exclude(pk = self.instance.pk) #grab instance.pk here
self.fields['location'] = MapField([
EditableLayerField({
'geometry': 'point',
'name': 'location',
}),
InfoLayerField(
[(get_ewkt(m.location), m.name) for m in qs if m.location ], {
'geometry': 'point',
'name': 'other locations',
'cluster': True,
'cluster_display': 'list',
}
)
])

Related

Dynamic Fields On A Serializer For A ForeignKey

I set up a serializer that is able to dynamically serialize the desired fields as specified in the Django Rest Framework docs.
class DynamicFieldsModelSerializer(serializers.ModelSerializer):
"""
A ModelSerializer that takes an additional `fields` argument that
controls which fields should be displayed.
"""
def __init__(self, *args, **kwargs):
# Don't pass the 'fields' arg up to the superclass
fields = kwargs.pop('fields', None)
# Instantiate the superclass normally
super(DynamicFieldsModelSerializer, self).__init__(*args, **kwargs)
if fields is not None:
# Drop any fields that are not specified in the `fields` argument.
allowed = set(fields)
existing = set(self.fields)
for field_name in existing - allowed:
self.fields.pop(field_name)
>>> class UserSerializer(DynamicFieldsModelSerializer):
>>> class Meta:
>>> model = User
>>> fields = ['id', 'username', 'email']
>>>
>>> print(UserSerializer(user))
{'id': 2, 'username': 'jonwatts', 'email': 'jon#example.com'}
>>>
>>> print(UserSerializer(user, fields=('id', 'email')))
{'id': 2, 'email': 'jon#example.com'}
How would I then add in a model that is related by ForeignKey to this and dynamically serialize the desired fields from that model?
We could make the models
class User(models.Model):
id = IntegerField()
username = CharField()
email = EmailField()
class Vehicle(models.Model):
color = CharField()
type = CharField
year = DateField()
driver = ForeignKey(User)
and then based on the view either include color, type, year, or any combination of those.
I would like something like this.
{
'id': 27,
'username': 'testuser',
'vehicle: {
'color': 'Blue',
'type': 'Truck',
}
}
If you want a nested dict you could simply do
class VehicleSeralizer(serialzers.ModelSerializer):
class Meta:
model = Vehicle
fields = '__all__'
class UserSerializer(seralizers.ModelSerializer):
vehicle = VehicleSerializer()
class Meta:
model = User
fields = ['id', 'username', 'email'] # you might need to add 'vehicle'

How to serialize some nested relational models in django using DRF?

I have some Django models with different relations to each other — Many-to-many, and Foreignkey. By that means, I want to serialize them using djnago-rest.
Here are the models:
class CommonFieldsAbstract(models.Model):
name = models.CharField(max_length=30, unique=True)
class ServerModel(CommonFieldsAbstract):
server_ip = models.GenericIPAddressField(default='172.17.0.1')
server_port = models.IntegerField(default='9001')
class SNMPLineModel(CommonFieldsAbstract):
ip_address = models.GenericIPAddressField()
port = models.IntegerField(default=161)
class SNMPModel(CommonFieldsAbstract): # target
line = models.ForeignKey(SNMPLineModel, on_delete=CASCADE)
servers = models.ManyToManyField(ServerModel)
class MetaDataModel(models.Model):
key = models.CharField(max_length=20)
value = models.CharField(max_length=20)
snmp_device = models.ForeignKey(SNMPModel, on_delete=CASCADE)
Before, I used to use the following approach to create the JSON manually:
def meta_data_json(meta_data):
meta_data_list = []
for meta in meta_data:
meta_data_list.append({
meta.key: meta.value
})
return meta_data_list
def server_json(servers):
return [{'ip': server.server_ip,
'port': server.server_port}
for server in servers]
def create_json():
snmp = SNMPModel.objects.filter(name__contains='a-name')
return {
'name': snmp.name,
'address': snmp.line.ip_address,
'port': snmp.line.port,
'servers': server_json(snmp.servers.all()),
'meta_data': meta_data_json(MetaDataModel.objects.filter(
snmp_device=snmp.pk
)
),
'device_pk': snmp.pk
}
My Question:
Now, how can I create such an above json via django-rest-framework instead?
I don't have any problem with many-to-many fields. In fact, my problem is the foreignkey(s).
Here's what I've done so far:
# serializers.py
from rest_framework import serializers
class MetaDataSerializer(serializers.ModelSerializer):
class Meta:
fields = [
'id',
'key',
'value',
]
model = MetaDataModel
class ServerSerializer(serializers.ModelSerializer):
class Meta:
fields = [
'id',
'server_ip',
'server_port',
]
model = ServerModel
class LineSerializer(serializers.ModelSerializer):
port = serializers.RelatedField(many=True)
class Meta:
fields = '__all__'
model = SNMPLineModel
class SNMPSerializer(serializers.ModelSerializer):
servers = ServerSerializer(many=True, read_only=True) # It is ok
meta_data = MetaDataSerializer(many=True, read_only=True) # It's not ok
line = LineSerializer(many=True, read_only=True) # It's not ok
address = serializers.CharField(source=SNMPLineModel.ip_address) # It's not ok
port = serializers.CharField(source=SNMPLineModel.port) # It's not ok
class Meta:
fields = [
'id',
'servers',
'name',
'address',
'port',
'line',
'meta_data'
]
model = SNMPModel
# views.py
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse, JsonResponse
#csrf_exempt
def snippet_detail(request, name):
try:
snmp_conf = SNMPModel.objects.filter(name__contains=name)
except SNMPModel.DoesNotExist:
return HttpResponse(status=404)
if request.method == 'GET':
serializer = SNMPSerializer(snmp_conf, many=True)
return JsonResponse(serializer.data, status=200, safe=False)
# urls.py
from django.urls import path
urlpatterns = [
path('snippets/<name>/', views.snippet_detail)
]
Any help would be greatly appreciated.
The serializers.SerializerMethodField() is a useful method to add in relations like this.
get_meta_data() is a bit of magic evaluating the fieldname to call the method.
Address and port seem to be a simple relation and line.FOO should work.
class SNMPSerializer(serializers.ModelSerializer):
servers = ServerSerializer(many=True, read_only=True) # It is ok
meta_data = serializers.SerializerMethodField()
line = serializers.SerializerMethodField()
address = serializers.CharField(source="line.ip_address", read_only=True)
port = serializers.CharField(source="line.port" , read_only=True)
class Meta:
fields = ['id', 'servers', 'name', 'address', 'port', 'line', 'meta_data']
model = SNMPModel
def get_meta_data(self, instance):
metadatamodels = MetaDataModel.objects.filter(snmp_device=instance)
serializer = MetaDataSerializer(instance=metadatamodels, many=True, read_only=True)
return serializer.data
def get_line(self, instance):
serializer = LineSerializer(instance.line, read_only=True)
return serializer.data
For solving the mentioned problems I did the following solutions:
For a Forward Foreignkey you do not need many=True
For a Reverse Foreignkey you need using related-model_set while you haven't defined related_name.
For an extra field you just need the exact queryset.
Therefore, I reached the following code snippet without using .SerializerMethodField() as Michael used on his answer:
class SNMPSerializer(serializers.ModelSerializer):
servers = ServerSerializer(many=True)
meta_data = MetaDataSerializer(many=True, source="metadatamodel_set")
line = LineSerializer()
address = serializers.CharField(source="line.ip_address")
port = serializers.CharField(source="line.port")
class Meta:
fields = (
"id",
"servers",
"name",
"address",
"port",
"line",
"meta_data",
)
model = SNMPModel

Data Dissapearing in Django on validation

I am trying to create nested objects (documentregulation) when I create a Document object. To achieve this, I have overwritten the create method on the DocumentSerializer, as per Django Docs. However, when I attempt validated_data.pop('documentregulation_set'), it is empty, even when it is populated in the incoming request.data of my view. Is there something causing my incoming data to not be validated? How would I go about debugging this if so?
// serializers.py
class DocumentRegulationSerializer(serializers.ModelSerializer):
class Meta:
model = DocumentRegulation
fields = ('regulation',)
class DocumentSerializer(serializers.ModelSerializer):
documentregulation_set = DocumentRegulationSerializer(many=True, required=False)
class Meta:
model = Document
fields = ('documentregulation_set', 'id', 'name', 'file', 'text', 'uploaded_at')
extra_kwargs = {
'id': {'read_only': True},
'uploaded_at': {'read_only': True},
}
def create(self, validated_data):
documentregulation_set = validated_data.pop('documentregulation_set')
# create document first
document = Document.objects.create(**validated_data)
# serialize associated regulations
for documentregulation in documentregulation_set:
# get ID of newly-created document, use for relation
#documentregulation['regulation'] = documentregulation['id']
DocumentRegulation.objects.create(document=document, **documentregulation)
return document
//views.py
class DocumentView(generics.ListCreateAPIView):
def create(self, request):
#pprint(self.request.FILES['profile_pic'])
request.data['documentregulation_set'] = json.loads(request.data['documentregulation_set'])
request.data['documentdomain_set'] = json.loads(request.data['documentdomain_set'])
pprint(request.data)
document = DocumentSerializer(data=request.data)
if document.is_valid():
document.save()
return Response(document.data, status=status.HTTP_201_CREATED)
else:
return Response(document.errors, status=status.HTTP_400_BAD_REQUEST)
my incoming data (printed in request.data) looks like:
{'documentregulation_set': [{'label': 'Regulation 1',
'regulation': 2,
'value': 2},
{'label': 'Regulation 2',
'regulation': 4,
'value': 4}],
'file': <InMemoryUploadedFile: test.docx >,
'name': 'testing',
'text': 'test'}
but then my validated data prints out to be:
{'documentregulation_set': [],
'file': <InMemoryUploadedFile: test.docx >,
'name': 'testing',
'text': 'test'}
Problem seems to be with the validation of the documentregulation_set field in the DocumentSerializer serializer. But you can escape the validation in the Meta Class as:
class Meta:
model = Document
fields = '__all__'
extra_kwargs = {
'documentregulation_set': {'validators': []} # escape validation
}
If you need to write custom validators have a look at Writing custom validators
So the final serializer looks like:
class DocumentRegulationSerializere(serializers.ModelSerializer):
"""Serializers for DocumentRegulation object"""
class Meta:
model = DocumentRegulation
fields = ('regulation',)
class DocumentSerializer(serializers.ModelSerializer):
"""Serializer for Document objects"""
documentregulation_set = DocumentRegulationSerializere(many=True)
class Meta:
model = Document
fields = ('documentregulation_set', 'id', 'name', 'file', 'text', 'uploaded_at')
extra_kwargs = {
'id': {'read_only': True},
'uploaded_at': {'read_only': True},
'documentregulation_set': {'validators': []} # escape validation
}
def create(self, validated_data):
doc_reg = []
document_regulation_set = validated_data.pop('documentregulation_set')
document = Document.objects.create(**validated_data)
for document_regulation in document_regulation_set:
reg = DocumentRegulation.objects.create(**document_regulation)
reg.save()
doc_reg.append(reg)
document.documentregulation_set.add(reg)
return document
View
class DocumentView(generics.ListCreateAPIView):
"""List or create new Document ojects"""
queryset = Document.objects.all()
serializer_class = DocumentSerializer

django rest framework: limit fields that can be updated

I want users to be able to update only one specific field. For example:
models.py
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
class Meta:
ordering = ('created',)
serializer.py
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
views.py
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
Once the Snippet is created, the user should only be able to update title field.
I know I can achieve that by something like this:
serializers.py
def update(self, instance, validated_data):
"""
Update and return an existing `Snippet` instance, given the validated data.
"""
instance.title = validated_data.get('title', instance.title)
instance.save()
return instance
In serializer class. But I want to know, is there a way that browsable API show only title field in edit form? And also skip validation for fields that are not required?
Django REST Framework provides the read_only and write_only attributes for controlling what is used for editing and what is not.
serializers.py
class SnippetSerializer(serializers.ModelSerializer):
class Meta:
model = Snippet
fields = ('id', 'title', 'code', 'linenos', 'language', 'style')
extra_kwargs = {
'id': {'read_only': True},
'code': {'read_only': True},
'lineos': {'read_only': True},
'language': {'read_only': True},
'style': {'read_only': True}
}
The above will return all the fields on read requests but only title will be writable.
You can find more at the official documentation:
http://www.django-rest-framework.org/api-guide/serializers/#specifying-read-only-fields
While #petkostas answer is correct, it doesn't give you a full picture of how to achieve it.
First, Create a new serializer; let's call it SnippetUpdateSerializer
Now, you may have custom serializer fields like serializers.MethodFieldSerializer that you would have defined in SnipperSerializer; which you may not want to write again in your new serializer. A good approach is to use inheritance.
Taking the example from the question
class SnippetUpdateSerializer(SnippetSerializer): #<- pay attention here
class Meta(SnippetSerializer.Meta): # <- pay attention here
SnippetSerializer.Meta.extra_kwargs.update({ # update the dictionary
'id': {'read_only': True},
'code': {'read_only': True}, # you can also use {write_only: True} if you want this to be write only
'lineos': {'read_only': True},
'language': {'read_only': True},
'style': {'read_only': True}
}) # you may completely override by just using extra_kwargs, instead of SnippetSerializer.Meta.extra_kwargs
Now in your SnippetUpdateView, use the above serializer.
If you are using class based views then set serializer_class = SnippetUpdateSerializer
Another approach is to return bad request, from your update view if the user requests contain read_only fields. (not recommended)

DRF: Manipulating serializer field layout

I have a model that represents a house:
class House(models.Model):
name = models.CharField(...)
long = models.FloatField(...)
lat = models.FloatField(...)
and a serializer to return a list of houses in their most basic representation:
class HouseSerializer(serializers.ModelSerializer):
class Meta:
model = House
fields = ('id', 'name')
and the view
class HouseList(generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
this works fine. I can visit /api/house/ and I see a json list of houses:
{
'id': 1,
'name': 'Big House'
},
{
'id': 1
'name': 'Small House',
}...
Now I want to create a second view/resource at /api/maps/markers/ that returns my houses as a list of Google-Map-Friendly markers of the format:
{
'id': 1,
'long': ...,
'lat': ...,
'houseInfo': {
'title': "Big House",
}
} ...
I can foresee two approaches:
perform this as a separate serializer (using the same view as before) and mapping out the alternative field layout.
perform this as a separate view (using the same serializer as before) and simply layout the fields before creating a Response
but in neither approach am I clear on how to go about it nor which approach is preferable?
Answer 1
Looks to me like you need both - different view and serializer.
Simply because the view endpoint is not a sub-url of the first one, so they are not related - different view, even if they use the same model.
And different serializer - since you have a different field layout.
Not really sure how complicated is your case, but any code duplication can probably be solved by mixins anyway.
Answer 2
Depending on the use case:
if you also need to write data using the same struct, you need to define your own field class and handle the parsing correctly
if it's just reading data, you should be fine with this:
class HouseGoogleSerializer(HouseSerializer):
houseInfo = serializers.SerializerMethodField('get_house_info')
class Meta:
model = House
fields = [...]
def get_house_info(self, obj):
return {'title': obj.name}
where HouseSerializer is your base house serializer.
this code come from a running project and offer somethig more that you ask
but can easily adapted for your need if you want remove some features.
The current implemetation allow you:
use only one url one serializer and one view
choose the output using query string param (?serializer=std)
how to use in your code:
Case 1 (one url with ability to choose the serializer via querystring)
class HouseSerializer(HouseSerializer):
houseInfo = serializers.SerializerMethodField('get_house_info')
class Meta:
model = House
def get_house_info(self, obj):
return {'title': obj.name}
class HouseList(DynamicSerializerMixin, generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
serializers_fieldsets = {'std': ('id', 'name'),
'google' : ('id', 'long', 'lat', 'houseInfo')}
Case 2 (different views)
class HouseList(DynamicSerializerMixin, generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
serializers_fieldsets = {'std': ('id', 'name')}
class GoogleHouseList(DynamicSerializerMixin, generics.ListAPIView):
queryset = House.objects.all()
serializer_class = HouseSerializer
serializers_fieldsets = {'std': ('id', 'long', 'lat', 'houseInfo')}
==============
def serializer_factory(model, base=BaseHyperlinkedModelSerializer,
fields=None, exclude=None):
attrs = {'model': model}
if fields is not None:
attrs['fields'] = fields
if exclude is not None:
attrs['exclude'] = exclude
parent = (object,)
if hasattr(base, 'Meta'):
parent = (base.Meta, object)
Meta = type(str('Meta'), parent, attrs)
if model:
class_name = model.__name__ + 'Serializer'
else:
class_name = 'Serializer'
return type(base)(class_name, (base,), {'Meta': Meta, })
class DynamicSerializerMixin(object):
"""
Mixin that allow to limit the fields returned
by the serializer.
Es.
class User(models.Model):
country = models.ForeignKey(country)
username = models.CharField(max_length=100)
email = models.EmailField()
class UserSerializer(BaseHyperlinkedModelSerializer):
country = serializers.Field(source='country.name')
class MyViewSet(DynamicSerializerViewSetMixin, BaseModelViewSet):
model = User
serializer_class = UserSerializer
serializers_fieldsets = {'std': None,
'brief' : ('username', 'email')
}
this allow calls like
/api/v1/user/?serializer=brief
"""
serializers_fieldsets = {'std': None}
serializer_class = ModelSerializer
def get_serializer_class(self):
ser = self.request.QUERY_PARAMS.get('serializer', 'std')
fields = self.serializers_fieldsets.get(ser, 'std')
return serializer_factory(self.model,
self.serializer_class,
fields=fields)