For my app I have offices and HR users (users with an OneToOneField to HRProfile), and I want to be able to assign HRs to offices. The issue I'm facing is that I just can't access the User's 'email' field when trying to look it up through a HyperlinkedRelatedField on an OfficeSerializer.
Relevant models:
class User(AbstractBaseUser, PermissionsMixin):
...
email = models.EmailField(unique=True)
...
from polymorphic.models import PolymorphicModel
class Profile(PolymorphicModel):
...
related_user = models.OneToOneField(settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
related_name="profile",
)
...
class HRProfile(Profile):
some_hr_specific_field = models.CharField()
def __str__(self) -> str:
return self.related_user.email
class Office(models.Model):
...
assigned_hrs = models.ManyToManyField(
"users.HRProfile", related_name="offices", blank=True
)
...
View:
class UserViewSet(
GenericViewSet,
):
serializer_class = UserSerializer
queryset = User.objects.all()
lookup_field = "email"
lookup_url_kwarg = "email"
lookup_value_regex = "[\\w#.]+"
...
# the OfficeViewSet just has the two required fields (queryset and serializer)
And the OfficeSerializer I'm having trouble with:
class OfficeSerializer(serializers.HyperlinkedModelSerializer):
...
assigned_hrs = serializers.HyperlinkedRelatedField(
queryset=HRProfile.objects.all(),
view_name="api:user-detail",
lookup_field="related_user.email",
many=True,
)
...
The above raises 'HRProfile' object has no attribute 'related_user.email' which I don't know what to make of, since the HRProfile.__str__ has no issues resolving the self.related_user.email path correctly for a given user.
I tried it with source='assigned_hrs.related_user', lookup_field='email' but to no avail. Also, I tried replacing the HyperlinkedRelatedField with an SlugRelatedField just to test, and it's the same issue with the slug_field="related_user.email".
I also tried using the double underscores instead of a period. What goes through however is lookup_field='some_hr_specific_field' which is surely not what I want, but it does resolve.
So I'm really running out of ideas, and any hints would be greatly appreciated!
Solved!
Define get_hrs on Office model:
class Office(models.Model):
def get_hrs(self):
hrs = self.assigned_hrs.all()
return [hr.related_user for hr in hrs]
And simply use the method as source:
class OfficeSerializer(serializers.HyperlinkedModelSerializer):
assigned_hrs = serializers.HyperlinkedRelatedField(
many=True,
queryset=User.objects.filter(groups__name="human_resource"),
source="get_hrs",
lookup_field="email",
view_name="api:user-detail",
)
# slugrelated works too :)
# assigned_hrs = serializers.SlugRelatedField(
# many=True,
# queryset=User.objects.filter(groups__name="human_resource"),
# source="get_hrs",
# slug_field="email",
# )
Related
I am working with Django REST framework and django-filters and and I'd like to use the reverse relationship annotation_set as one of filters for a GET API that uses the model Detection.
The models are the following:
class Detection(models.Model):
image = models.ImageField(upload_to="detections/images")
def local_image_path(self):
return os.path.join('images' f"{self.id}.jpg")
class Annotation(models.Model):
detection = models.ForeignKey(Detection, on_delete=models.CASCADE)
attribute = models.CharField(max_length=255)
The serializer is:
class DetectionSerializer(UniqueFieldsMixin, serializers.ModelSerializer):
local_image_path = serializers.CharField()
class Meta:
model = Detection
fields = '__all__'
And the viewset is:
class DetectionTrainingViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet
):
queryset = Detection.objects.all()
serializer_class = DetectionSerializer
filterset_fields = ('annotation_set__id', )
#action(methods=['GET'], detail=False)
def list_ids(self, request):
queryset = self.get_queryset()
filtered_queryset = self.filter_queryset(queryset)
return Response(filtered_queryset.values_list('id', flat=True))
When I make a call to the endpoint, I get the error:
'Meta.fields' must not contain non-model field names: annotation_set__id
Shouldn't the field exist?
Note: I tried to add other fields to the Annotation model and then use annotation_set__newfield but I still have the error. I can confirm that the newfield exists because it is correctly serialized and return by the API when I comment out the line that set the filterset_fields.
Looking at the django filter docs, you may have missed a reference to DjangoFilterBackend, eg
queryset = Detection.objects.all()
serializer_class = DetectionSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ('annotation_set__id', )
(see: https://django-filter.readthedocs.io/en/stable/guide/rest_framework.html)
Apparently I had to explicitly state the name of the reverse relationship:
class Annotation(models.Model):
detection = models.ForeignKey(Detection, on_delete=models.CASCADE, related_name='annotation_set')
attribute = models.CharField(max_length=255)
If anybody knows why, I'd love to know it! Thanks!
Try this, I had similar problem it worked for me.
In your view file change the filterset_fields to the following.
class DetectionTrainingViewSet(
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
viewsets.GenericViewSet
):
queryset = Detection.objects.all()
serializer_class = DetectionSerializer
filter_backends = (filters.DjangoFilterBackend,)
filterset_fields = ('annotation__id', )
I have a model UserSite. Every User could have multiple sites. I now have to make them unique. So a site can't be added to a user if it already is appointed to that user. My Model code is:
class UserSite(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sites")
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="users")
class Meta:
unique_together = ("user", "site")
All fine. Now I want to make a test class who tests if this works. My test class:
from rest_framework.test import APITestCase
from models import UserSite
from factories import SiteFactory
from factories import UserFactory
class TestUniqueUserSite(APITestCase):
def setUp(self):
self.user = UserFactory()
self.test_site = SiteFactory()
self.test_site_2 = SiteFactory()
self.user_site = UserSite.objects.create(user=self.user, site=self.test_site)
def test_user_site_is_unique(self):
"""
Check if a new UserSite is unique
"""
self.user_site1 = UserSite.objects.create(user=self.user, site=self.test_site)
Factory:
class UserSiteFactory(factory.django.DjangoModelFactory):
class Meta:
model = UserSite
# Type hinting
def __new__(cls, *args, **kwargs) -> "UserSiteFactory.Meta.model":
return super().__new__(*args, **kwargs) # pragma: no cover
site = factory.SubFactory(SiteFactory)
user = factory.SubFactory(UserFactory)
user_role = factory.fuzzy.FuzzyChoice(UserSite.USER_ROLE_CHOICES)
This test gives no errors, the UserSite can be created. What do I wrong? The testing or the unique field? or both hehe, thanks!
When I run:
UserSite.objects.filter(user=self.user, site=self.test_site).all()
I got:
<QuerySet [<UserSite: pk: 1 - user_pk: 1 - site_pk: 1>, <UserSite: pk: 2 - user_pk: 1 - site_pk: 1>]>
So they are stored..
My guess is that the unique_constraint should be a list, not a tuple.
You are not setting the constraint correctly, it should be like
class UserSite(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sites")
site = models.ForeignKey(Site, on_delete=models.CASCADE, related_name="users")
class Meta:
unique_together = (("user", "site"),)
These are my models here:
class Site(models.Model):
siteID = models.CharField(max_length=255, primary_key=True)
class EndDevice(models.Model):
class Meta:
unique_together = ("edevID", "siteID")
edevID = models.CharField(max_length=255)
siteID = models.ForeignKey(Site, related_name='endDeviceList', on_delete=models.CASCADE)
deviceCategory = models.BigIntegerField()
This is my serilaizer:
class DeviceSerializer(serializers.ModelSerializer):
class Meta:
model = EndDevice
fields = ("edevID", "siteID", "deviceCategory")
class SiteSerializer(serializers.ModelSerializer):
endDeviceList = DeviceSerializer(many = True, read_only=True)
class Meta:
model = Site
fields = ("siteID", "endDeviceList")
This is my view:
class IndividualSite(generics.RetrieveUpdateDestroyAPIView):
'''
PUT site/{siteID}/
GET site/{siteID}/
DELETE site/{siteID}/
'''
queryset = EndDevice.objects.all()
serializer_class = SiteSerializer
I am trying to get/put/delete results using this class and I am trying to get all the EndDevice instances which have same siteID. But my serialzer only shows the siteID and doesn't show the endDeviceList (which should have the instants of the model EndDevice)
The problem is quite similar to this link:django rest-farmework nested relationships.
I have been trying different ways to serialize the objects, I think this is probably the smartest way, but have been really unsucccessful. Any help will be appreciated.
The urls.py:
urlpatterns = [
urlpatterns = [path('site/<str:pk>/', IndividualSite.as_view(), name = "get-site"),]
And it is connected to the main urls.
you are using read_only field for the Foreign relationship, remove that, as read_only wont display them
class SiteSerializer(serializers.ModelSerializer):
endDeviceList = DeviceSerializer(many = True)
models.py:
class Player(models.Model):
name = models.CharField(max_length=50)
email = models.EmailField(max_length=50)
class Tournament(models.Model):
name = models.CharField(max_length=50)
class TournamentPlayer(models.Model):
tournament = models.ForeignKey(Tournament)
player = models.ForeignKey(Player)
paid = models.BooleanField()
def player_email(self):
return self.player.email
admin.py:
class TournamentPlayerInline(admin.TabularInline):
model = TournamentPlayer
fields = ('player', 'paid', 'player_email')
#admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin):
inlines = [TournamentPlayerInline]
I have an Inline question. When I pull up a Tournament in the Admin Site, I can see which players are going, and if they paid. I would also like to display extra information contained in Player, for example email address.
In TournamentPlayerInline I thought I might be able to get away with fields = ('player', 'paid', 'player_email') but I get FieldError: Unknown field(s) (player_email) specified for TournamentPlayer.
I also tried fields = ('player', 'paid', 'player__email'), but I get FieldError: Unknown field(s) (player__email) specified for TournamentPlayer.
If I move player_email from fields to readonly_fields, I no longer get the error, but the player email also isn't displayed either.
This is what I'm after:
How can I access Player properties from TournamentPlayerInline?
Django 1.8.4
Monkey's answer is almost correct. The only change you have to make is to your admin.py, and it's merely adding 'player_email' to both fields as well as readonly_fields. Changing the position of 'player_email' in fields will allow you to order it as per your example.
class TournamentPlayerInline(admin.TabularInline):
model = TournamentPlayer
fields = ('player', 'player_email', 'paid',)
readonly_fields = ('player_email',)
#admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin):
inlines = [TournamentPlayerInline]
If you do not require the player_email to be editable from the inline, then you can accomplish this with the readonly_fields variable:
class TournamentPlayerInline(admin.TabularInline):
model = TournamentPlayer
fields = ('player', 'paid')
readonly_fields = ('player_email',)
#admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin):
inlines = [TournamentPlayerInline]
As an alternative, you do not have to define your custom property in your model if you're not using it directly, and just want to view it in admin -- you can create it in the Inline via a mixin:
models.py
class Player(models.Model):
name = models.CharField(max_length=50)
email = models.EmailField(max_length=50)
class Tournament(models.Model):
name = models.CharField(max_length=50)
class TournamentPlayer(models.Model):
tournament = models.ForeignKey(Tournament)
player = models.ForeignKey(Player)
paid = models.BooleanField()
admin.py
class PlayerEmailMixin(object):
def player_email(self, obj):
return obj.player.email
player_email.short_description = "Player Email"
class TournamentPlayerInline(PlayerEmailMixin, admin.TabularInline):
model = TournamentPlayer
fields = ('player', 'player_email', 'paid', )
readonly_fields = ('player_email',)
#admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin):
inlines = [TournamentPlayerInline]
You could also make it a mailto URI this way:
class PlayerEmailMixin(object):
def player_email(self, obj):
return '<strong>{0}</strong>'.format(obj.player.email)
player_email.short_description = "Player Email"
player_email.allow_tags = True
This is known to work in Django 1.9.5
class Product(models.Model):
products = models.CharField(max_length=256)
def __unicode__(self):
return self.products
class PurchaseOrder(models.Model):
product = models.ManyToManyField('Product')
vendor = models.ForeignKey('VendorProfile')
dollar_amount = models.FloatField(verbose_name='Price')
I have that code. Unfortunately, the error comes in admin.py with the ManyToManyField
class PurchaseOrderAdmin(admin.ModelAdmin):
fields = ['product', 'dollar_amount']
list_display = ('product', 'vendor')
The error says:
'PurchaseOrderAdmin.list_display[0]', 'product' is a ManyToManyField
which is not supported.
However, it compiles when I take 'product' out of list_display. So how can I display 'product' in list_display without giving it errors?
edit: Maybe a better question would be how do you display a ManyToManyField in list_display?
You may not be able to do it directly. From the documentation of list_display
ManyToManyField fields aren’t supported, because that would entail
executing a separate SQL statement for each row in the table. If you
want to do this nonetheless, give your model a custom method, and add
that method’s name to list_display. (See below for more on custom
methods in list_display.)
You can do something like this:
class PurchaseOrderAdmin(admin.ModelAdmin):
fields = ['product', 'dollar_amount']
list_display = ('get_products', 'vendor')
def get_products(self, obj):
return "\n".join([p.products for p in obj.product.all()])
OR define a model method, and use that
class PurchaseOrder(models.Model):
product = models.ManyToManyField('Product')
vendor = models.ForeignKey('VendorProfile')
dollar_amount = models.FloatField(verbose_name='Price')
def get_products(self):
return "\n".join([p.products for p in self.product.all()])
and in the admin list_display
list_display = ('get_products', 'vendor')
This way you can do it, kindly checkout the following snippet:
class Categories(models.Model):
""" Base category model class """
title = models.CharField(max_length=100)
description = models.TextField()
parent = models.ManyToManyField('self', default=None, blank=True)
when = models.DateTimeField('date created', auto_now_add=True)
def get_parents(self):
return ",".join([str(p) for p in self.parent.all()])
def __unicode__(self):
return "{0}".format(self.title)
And in your admin.py module call method as follows:
class categories(admin.ModelAdmin):
list_display = ('title', 'get_parents', 'when')
If you want to save extra queries, you can use prefetch_related in the get_queryset method like below:
class PurchaseOrderAdmin(admin.ModelAdmin):
fields = ['product', 'dollar_amount']
list_display = ('get_products', 'vendor')
def get_queryset(self, request):
qs = super().get_queryset(request)
return qs.prefetch_related('product')
def get_products(self, obj):
return ",".join([p.products for p in obj.product.all()])
According to the Docs, In this way, there would be just one extra query needed to fetch related Product items of all PurchaseOrder instances instead of needing one query per each PurchaseOrder instance.