"reverse editing" of generic inlines in django admin - django

I have a django model named Event with a generic inline relation to Relationship, like this:
# models.py
class Person(models.Model):
...
class Role(models.Model):
...
class Event(models.Model):
...
class Relationship(models.Model):
person = models.ForeignKey(Person)
role = models.ForeignKey(Role)
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = generic.GenericForeignKey("content_type", "object_id")
# admin.py
class RelationshipInline(generic.GenericTabularInline):
model = Relationship
extra = 0
class EventAdmin(admin.ModelAdmin):
inlines = [RelationshipInline]
I'd like to find a way to edit the inlines not only from the event admin page, but from the people admin page too. SO far I have added the following code to display inlines in the people page too
class ReverseRelationshipInline(admin.TabularInline):
model = Relationship
class IndividualAdmin(admin.ModelAdmin):
inlines = [ReverseRelationshipInline]
But I get content_type and object_id fields in the form and it is not very informational for admin users since it is just references to primary keys. I would rather prefer to resolve and show the content_object(even if it is not editable, to know at list to what objects are people related to).
Any direction to recommend?
Thanks.

Your "ReverseRelationshipInline" has to be a GenericTabularInline, not a TabularInline. That's all :-)
UPDATE
I think I now understand what you're after, and my answer is:
You won't be able to edit the content object inline of Person, but you want to show it nicely, maybe even as a link to its change form.
Add a function to Relationship which returns such an HTML link, provide your own ModelForm to your inline and specify the fields you want, which now includes your new function value (read-only). Something like this (untested):
# models.py
from django.core import urlresolvers
class Relationship(models.Model):
...
def link_content_object_changeform(self):
obj = self.content_object
change_url = urlresolvers.reverse(
'admin:%s_%s_change' % (
obj._meta.app_label,
obj._meta.object_name.lower()
),
args=(obj.id,)
)
return u'%s' % (change_url, obj.__unicode__())
link_content_object_changeform.allow_tags = True
link_content_object_changeform.short_description = 'in relation to'
# admin.py
class ReverseRelationshipInlineForm(forms.ModelForm):
class Meta:
model = Relationship
fields = ('person', 'role', 'link_content_object_changeform')
readonly_fields = ('link_content_object_changeform',)
class ReverseRelationshipInline(admin.TabularInline):
model = Relationship
form = ReverseRelationshipInlineForm

Related

Display information about linked models fields in the django admin

These are my models:
class Partner(models.Model):
name = models.CharField(max_length=200, verbose_name="Organisation name")
class ResearchActivity(models.Model):
title = models.CharField(max_length=200)
partner = models.ManyToManyField(ActivityPartner, blank=True)
I'd like, in the Django administration forms, to have a field in my Partner edit form representing the ResearchActivity linked to that Partner.
Can this be achieved by adding a field to my Partner model (say, naming it linked_partner) and then edit my admin.py like so:
#admin.register(ActivityPartner)
class ActivityPartnerAdmin(admin.ModelAdmin):
search_fields = ['academic',]
autocomplete_fields = ['partnership_type', 'relationship_type', 'academic_links']
def get_changeform_initial_data(self, request):
return {'live_contract': ResearchActivity.objects.all().filter(linked_partner__id=request.ResearchActivity.partner.id)}
?
I have just come across in the display() decorator, new from Django 3.2. With it, I can simply do:
#admin.register(ActivityPartner)
class ActivityPartnerAdmin(admin.ModelAdmin):
search_fields = ['academic',]
autocomplete_fields = ['partnership_type', 'relationship_type', 'academic_links',]
readonly_fields = ('get_ra',)
#admin.display(description='Live contract(s)')
def get_ra(self, obj):
return list(ResearchActivity.objects.filter(partner=obj.id))
to achieve what I want.
If I also wanted to edit those ManyToMany relations, I can use the inlines option:
class LiveContractsInline(admin.TabularInline):
model = ResearchActivity.partner.through
#admin.register(ActivityPartner)
class ActivityPartnerAdmin(admin.ModelAdmin):
inlines = [
LiveContractsInline,
]

ModelSerializer: include all fields and all properties of a model [duplicate]

I have two models, one with M2M relation and a related name. I want to include all fields in the serializer and the related field.
models.py:
class Pizza(models.Model):
name = models.CharField(max_length=50, unique=True)
toppings = models.ManyToManyField(Topping, null=True, blank=True, related_name='pizzas')
class Topping(models.Model):
name = models.CharField(max_length=50, unique=True)
price = models.IntegerField(default=0)
serializer.py:
class ToppingSerializer(serializers.ModelSerializer):
class Meta:
model = Topping
fields = '__all__'
This works but it doesn't include the related field.
fields = ['name', 'price', 'pizzas']
This works exactly as I want, but what happens when Toppings model has a lot of fields. I want to do something like :
fields = ['__all__', 'pizzas']
This syntax results in an error saying:
Field name __all__ is not valid for model
Is there a way to achieve the wanted behavior? Or the fields must be typed manually when using a related name ?
Like #DanEEStart said, DjangoRestFramework don't have a simple way to extend the 'all' value for fields, because the get_field_names methods seems to be designed to work that way.
But fortunately you can override this method to allow a simple way to include all fields and relations without enumerate a tons of fields.
I override this method like this:
class ToppingSerializer(serializers.ModelSerializer):
class Meta:
model = Topping
fields = '__all__'
extra_fields = ['pizzas']
def get_field_names(self, declared_fields, info):
expanded_fields = super(ToppingSerializer, self).get_field_names(declared_fields, info)
if getattr(self.Meta, 'extra_fields', None):
return expanded_fields + self.Meta.extra_fields
else:
return expanded_fields
Note that this method only change the behaviour of this serializer, and the extra_fields attribute only works on this serializer class.
If you have a tons of serializer like this, you can create a intermediate class to include this get_fields_names method in one place and reuse'em many times. Some like this:
class CustomSerializer(serializers.HyperlinkedModelSerializer):
def get_field_names(self, declared_fields, info):
expanded_fields = super(CustomSerializer, self).get_field_names(declared_fields, info)
if getattr(self.Meta, 'extra_fields', None):
return expanded_fields + self.Meta.extra_fields
else:
return expanded_fields
class ToppingSerializer(CustomSerializer):
class Meta:
model = Topping
fields = '__all__'
extra_fields = ['pizzas']
class AnotherSerializer(CustomSerializer):
class Meta:
model = Post
fields = '__all__'
extra_fields = ['comments']
I just checked the source code of Django Rest Framework.
The behaviour you want seems not to be supported in the Framework.
The fields option must be a list, a tuple or the text __all__.
Here is a snippet of the relevant source code:
ALL_FIELDS = '__all__'
if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)):
raise TypeError(
'The `fields` option must be a list or tuple or "__all__". '
'Got %s.' % type(fields).__name__
)
You cannot add 'all' additionally to the tuple or list with fields...
The fields="__all__" option can work by specifying an additional field manually as per the following examples. This is by far the cleanest solution around for this issue.
Nested Relationships
http://www.django-rest-framework.org/api-guide/relations/#nested-relationships
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = '__all__'
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True, read_only=True)
class Meta:
model = Album
fields = '__all__'
I would assume this would work for any of the other related field options listed on the same page: http://www.django-rest-framework.org/api-guide/relations/#serializer-relations
Reverse relation example
class TrackSerializer(serializers.ModelSerializer):
album = AlbumSerializer(source='album_id')
class Meta:
model = Track
fields = '__all__'
Note: Created using Django Rest Framework version 3.6.2, subject to change. Please add a comment if any future changes break any examples posted above.
Hi I could achieve the expected result by using Django's _meta API , which seems to be available since Django 1.11. So in my serializer I did:
model = MyModel
fields = [field.name for field in model._meta.fields]
fields.append('any_other_field')
In programming there's always many ways to achieve the same result, but this one above, has really worked for me.
Cheers!
If you are trying to basically just add extra piece of information into the serialized object, you don't need to change the fields part at all. To add a field you do:
class MySerializer(serializers.ModelSerializer):
...
new_field = serializers.SerializerMethodField('new_field_method')
def new_field_method(self, modelPointer_):
return "MY VALUE"
Then you can still use
class Meta:
fields = '__all__'
to include all the fields and the other fields defined in your serializer you can just say exclude = ()
class ToppingSerializer(serializers.HyperlinkedModelSerializer):
pizzas = '<>' #the extra attribute value
class Meta:
model = Topping
exclude = ()
This will list all the field values with the extra argument pizzas
This is how i did it, much more easier
class OperativeForm(forms.ModelForm):
class Meta:
model = Operative
fields = '__all__'
exclude = ('name','objective',)
widgets = {'__all__':'required'}
Building on top of #Wand's wonderful answer:
def build_fields(mdl,extra=[],exclude=[]):
fields = [field.name for field in mdl._meta.fields if field.name not in exclude]
fields += extra
return fields
Usage:
model = User
fields = build_fields(model, ['snippets'], ['password'])
Will return all fields from the User model, with the related field snippets, without the password field.

Django Rest framework, how to include '__all__' fields and a related field in ModelSerializer ?

I have two models, one with M2M relation and a related name. I want to include all fields in the serializer and the related field.
models.py:
class Pizza(models.Model):
name = models.CharField(max_length=50, unique=True)
toppings = models.ManyToManyField(Topping, null=True, blank=True, related_name='pizzas')
class Topping(models.Model):
name = models.CharField(max_length=50, unique=True)
price = models.IntegerField(default=0)
serializer.py:
class ToppingSerializer(serializers.ModelSerializer):
class Meta:
model = Topping
fields = '__all__'
This works but it doesn't include the related field.
fields = ['name', 'price', 'pizzas']
This works exactly as I want, but what happens when Toppings model has a lot of fields. I want to do something like :
fields = ['__all__', 'pizzas']
This syntax results in an error saying:
Field name __all__ is not valid for model
Is there a way to achieve the wanted behavior? Or the fields must be typed manually when using a related name ?
Like #DanEEStart said, DjangoRestFramework don't have a simple way to extend the 'all' value for fields, because the get_field_names methods seems to be designed to work that way.
But fortunately you can override this method to allow a simple way to include all fields and relations without enumerate a tons of fields.
I override this method like this:
class ToppingSerializer(serializers.ModelSerializer):
class Meta:
model = Topping
fields = '__all__'
extra_fields = ['pizzas']
def get_field_names(self, declared_fields, info):
expanded_fields = super(ToppingSerializer, self).get_field_names(declared_fields, info)
if getattr(self.Meta, 'extra_fields', None):
return expanded_fields + self.Meta.extra_fields
else:
return expanded_fields
Note that this method only change the behaviour of this serializer, and the extra_fields attribute only works on this serializer class.
If you have a tons of serializer like this, you can create a intermediate class to include this get_fields_names method in one place and reuse'em many times. Some like this:
class CustomSerializer(serializers.HyperlinkedModelSerializer):
def get_field_names(self, declared_fields, info):
expanded_fields = super(CustomSerializer, self).get_field_names(declared_fields, info)
if getattr(self.Meta, 'extra_fields', None):
return expanded_fields + self.Meta.extra_fields
else:
return expanded_fields
class ToppingSerializer(CustomSerializer):
class Meta:
model = Topping
fields = '__all__'
extra_fields = ['pizzas']
class AnotherSerializer(CustomSerializer):
class Meta:
model = Post
fields = '__all__'
extra_fields = ['comments']
I just checked the source code of Django Rest Framework.
The behaviour you want seems not to be supported in the Framework.
The fields option must be a list, a tuple or the text __all__.
Here is a snippet of the relevant source code:
ALL_FIELDS = '__all__'
if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)):
raise TypeError(
'The `fields` option must be a list or tuple or "__all__". '
'Got %s.' % type(fields).__name__
)
You cannot add 'all' additionally to the tuple or list with fields...
The fields="__all__" option can work by specifying an additional field manually as per the following examples. This is by far the cleanest solution around for this issue.
Nested Relationships
http://www.django-rest-framework.org/api-guide/relations/#nested-relationships
class TrackSerializer(serializers.ModelSerializer):
class Meta:
model = Track
fields = '__all__'
class AlbumSerializer(serializers.ModelSerializer):
tracks = TrackSerializer(many=True, read_only=True)
class Meta:
model = Album
fields = '__all__'
I would assume this would work for any of the other related field options listed on the same page: http://www.django-rest-framework.org/api-guide/relations/#serializer-relations
Reverse relation example
class TrackSerializer(serializers.ModelSerializer):
album = AlbumSerializer(source='album_id')
class Meta:
model = Track
fields = '__all__'
Note: Created using Django Rest Framework version 3.6.2, subject to change. Please add a comment if any future changes break any examples posted above.
Hi I could achieve the expected result by using Django's _meta API , which seems to be available since Django 1.11. So in my serializer I did:
model = MyModel
fields = [field.name for field in model._meta.fields]
fields.append('any_other_field')
In programming there's always many ways to achieve the same result, but this one above, has really worked for me.
Cheers!
If you are trying to basically just add extra piece of information into the serialized object, you don't need to change the fields part at all. To add a field you do:
class MySerializer(serializers.ModelSerializer):
...
new_field = serializers.SerializerMethodField('new_field_method')
def new_field_method(self, modelPointer_):
return "MY VALUE"
Then you can still use
class Meta:
fields = '__all__'
to include all the fields and the other fields defined in your serializer you can just say exclude = ()
class ToppingSerializer(serializers.HyperlinkedModelSerializer):
pizzas = '<>' #the extra attribute value
class Meta:
model = Topping
exclude = ()
This will list all the field values with the extra argument pizzas
This is how i did it, much more easier
class OperativeForm(forms.ModelForm):
class Meta:
model = Operative
fields = '__all__'
exclude = ('name','objective',)
widgets = {'__all__':'required'}
Building on top of #Wand's wonderful answer:
def build_fields(mdl,extra=[],exclude=[]):
fields = [field.name for field in mdl._meta.fields if field.name not in exclude]
fields += extra
return fields
Usage:
model = User
fields = build_fields(model, ['snippets'], ['password'])
Will return all fields from the User model, with the related field snippets, without the password field.

Add Serializer on Reverse Relationship - Django Rest Framework

I have a Cart model and a CartItem model. The CartItem model has a ForeignKey to the Cart model.
Using Django Rest Framework I have a view where the API user can display the Cart, and obviously then I want to include the CartItem in the respone.
I set up my Serializer like this:
class CartSerializer(serializers.ModelSerializer):
user = UserSerializer(read_only=True)
cartitem_set = CartItemSerializer(read_only=True)
class Meta:
model = Cart
depth = 1
fields = (
'id',
'user',
'date_created',
'voucher',
'carrier',
'currency',
'cartitem_set',
)
My problem is the second line, cartitem_set = CartItemSerializer(read_only=True).
I get AttributeErrors saying 'RelatedManager' object has no attribute 'product'. ('product' is a field in the CartItem model. If I exclude product from the CartItemSerializer I just get a new AttributeError with the next field and so on. No matter if I only leave 1 or all fields in the Serializer, I will get a error.
My guess is that for some reason Django REST Framework does not support adding Serializers to reverse relationships like this. Am I wrong? How should I do this?
PS
The reason why I want to use the CartItemSerializer() is because I want to have control of what is displayed in the response.
Ahmed Hosny was correct in his answer. It required the many parameter to be set to True to work.
So final version of the CartSerializer looked like this:
class CartSerializer(serializers.ModelSerializer):
cartitem_set = CartItemSerializer(read_only=True, many=True) # many=True is required
class Meta:
model = Cart
depth = 1
fields = (
'id',
'date_created',
'voucher',
'carrier',
'currency',
'cartitem_set',
)
It's important to define a related name in your models, and to use that related name in the serializer relationship:
class Cart(models.Model):
name = models.CharField(max_length=500)
class CartItem(models.Model):
cart = models.ForeignKey(Cart, related_name='cart_items')
items = models.IntegerField()
Then in your serializer definition you use those exact names:
class CartSerializer(serializers.ModelSerializer):
cart_items = CartItemSerializer(read_only=True)
class Meta:
model = Cart
fields = ('name', 'cart_items',)
It would be wise to share your whole code, that is model and serializers classes. However, perhaps this can help debug your error,
My serializer classes
class CartItemSerializer(serializers.ModelSerializer):
class Meta:
model = CartItem
fields = ('id')
class CartSerializer(serializers.ModelSerializer):
#take note of the spelling of the defined var
_cartItems = CartItemSerializer()
class Meta:
model = Cart
fields = ('id','_cartItems')
Now for the Models
class CartItem(models.Model):
_cartItems = models.ForeignKey(Subject, on_delete=models.PROTECT)
#Protect Forbids the deletion of the referenced object. To delete it you will have to delete all objects that reference it manually. SQL equivalent: RESTRICT.
class Meta:
ordering = ('id',)
class Cart(models.Model):
class Meta:
ordering = ('id',)
For a detailed overview of relationships in django-rest-framework, please refer their official documentation

Django admin GenericForeignKey widget

I'm creating a Django app where all the models can be related to each other in an order set by the user. I'm setting all this up using GenericForeignKeys. The kicker is that I need to be able to support multiple collections of these types of relationship/admin. So one object can have a more than one collection of related objects.
Does anyone know of a good GenericForeignKey widget for this situation? Preferably, it would be an autocomplete search that populates the admin form since I can end up having a large number of objects.
Here is the code for my app to get a better idea of what I mean.
from django.contrib import admin
from django.contrib.contenttypes import generic
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django import forms
# Models
class Base(models.Model):
title = models.CharField(max_length=255)
class Meta:
abstract = True
def __unicode__(self):
return self.title
class Related(Base):
""" A generic relationship model for relating resources.
"""
order = models.IntegerField(blank=True, null=True)
limit = models.Q(model = 'Apple') | models.Q(model = 'Orange') | models.Q(model = 'Pear')
content_type = models.ForeignKey(ContentType, related_name="related_%(class)s")
object_id = models.PositiveIntegerField(db_index=True)
object = generic.GenericForeignKey()
related_content_type = models.ForeignKey(ContentType, related_name="related_related_%(class)s", limit_choices_to = limit)
related_object_id = models.PositiveIntegerField(db_index=True)
related_object = generic.GenericForeignKey('related_content_type', 'related_object_id')
class Meta:
ordering = ('order',)
abstract = True
def __unicode__(self):
return self.object.title
class FreshFruit(Related):
pass
class OldFruit(Related):
pass
class Apple(Base):
pass
class Orange(Base):
pass
class Pear(Base):
pass
# Admin classes
class FreshFruitInline(generic.GenericStackedInline):
model = FreshFruit
extra = 1
# Admin classes
class OldFruitInline(generic.GenericStackedInline):
model = OldFruit
extra = 1
class AppleAdmin(admin.ModelAdmin):
inlines = [FreshFruitInline, OldFruitInline,]
admin.site.register(Apple, AppleAdmin)
class OrangeAdmin(admin.ModelAdmin):
inlines = [FreshFruitInline, OldFruitInline,]
admin.site.register(Orange, OrangeAdmin)
class PearAdmin(admin.ModelAdmin):
inlines = [FreshFruitInline, OldFruitInline,]
admin.site.register(Pear, PearAdmin)
I've searched and searched, and found widgets that do this for a ManyToMany relationship, but nothing for my situation.
Thanks for taking the time to look at this.
Have a look at Grappelli's generic foreign key widget, which works well:
django-grappelli/generic_2_2