Django Admin formfield_for_foreignkey for manytomany - django

My Model (simpliefied)
we have tours:
class Tour(models.Model):
name = models.CharField(max_length=100)
strecke = models.ManyToManyField(Strecke, through="Streckenreihenfolge")
A tour has sections:
class Strecke(models.Model):
name = models.CharField(max_length=100)
auftrag = models.ForeignKey("Auftrag", on_delete=models.CASCADE, null=True, blank=True)
And the sections are put in order
class Streckenreihenfolge(models.Model):
strecke = models.ForeignKey(Strecke, on_delete=models.CASCADE)
tour = models.ForeignKey("Tour", on_delete=models.CASCADE)
reihenfolge = models.IntegerField()
In my admin, I want to give some restrictions on which sections (Strecke) to show. I thought about using formfield_for_foreignkey. It gets called, but it doesn't have any impact on the options to select from:
#admin.register(Tour)
class TourAdmin(admin.ModelAdmin):
class StreckenreihenfolgeAdminInline(admin.TabularInline):
model = Streckenreihenfolge
autocomplete_fields = ["strecke"]
ordering = ["reihenfolge"]
extra = 0
def formfield_for_foreignkey(self, db_field, request, **kwargs):
print(db_field.name)
if db_field.name == 'strecke':
kwargs['queryset'] = Strecke.objects.filter(auftrag_id__exact=8)
return super().formfield_for_foreignkey(db_field, request, **kwargs)
inlines = [StreckenreihenfolgeAdminInline, ]
Does formfield_for_foreignkey not work for manytomanyfields?
Update
Found some more infos here: Django Admin - Filter ManyToManyField with through model
Apparently formfield_for_manytomany doesn't work for inline forms.
I have then tried to use get_queryset(), which reduced the queryset, but somehow the autocomplete values are still unfiltered.
Maybe this image here is illustrating more what I'm try to achieve:

Finally found the answer here: https://forum.djangoproject.com/t/django-admin-autocomplete-field-search-customization/7455/5
Which results in the following code:
#admin.register(Strecke)
class StreckeAdmin(admin.ModelAdmin):
search_fields = ["name"]
def get_search_results(self, request, queryset, search_term):
print("get serach results")
queryset = queryset.filter(auftrag_id__exact=8)
qs = super().get_search_results(request, queryset, search_term)
print(qs)
return qs

Related

Django get objects created by users, with those users belonging to a list of users

I have a model for followers. Here is the model:
class Follow(models.Model):
followee = models.ForeignKey(User, on_delete=models.CASCADE, related_name="followee")
follower = models.ForeignKey(User, on_delete=models.CASCADE, related_name="follower")
created_at = models.DateTimeField(auto_now_add=True, verbose_name="created at")
updated_at = models.DateTimeField(auto_now=True, verbose_name="updated at")
class Meta:
unique_together = ["followee", "follower"]
def __str__(self):
return "{} is following {}".format(self.follower, self.followee)
def save(self, *args, **kwargs):
if self.followee == self.follower:
return "You cannot follow yourself"
else:
super().save(*args, **kwargs)
Users create multiple types of objects like posts and questions. I would like to show all the posts and questions of all the users that follow a specific user. Simplified show me all the posts and questions of users that follow me.
I am using a module called drf_multiple_model and here is my view, which I cannot get to work. It gives me the following error that I don't understand:
Cannot use QuerySet for "Follow": Use a QuerySet for "User".
Here is the view I am using:
def get_followers(queryset, request, *args, **kwargs):
id = kwargs['user']
user = User.objects.get(id=id)
followers = Follow.objects.all().filter(followee=user)
return queryset.filter(user__in=followers)
class HomeView(FlatMultipleModelAPIView):
permission_classes = [IsAuthenticated]
def get_querylist(self):
querylist = [
{'queryset':Post.objects.all(), 'serializer_class': UserPostSerializer, 'filter_fn': get_followers, 'label':'post'},
{'queryset':Question.objects.all(), 'serializer_class': QuestionSerializer, 'filter_fn': get_followers, 'label':'question'},
]
return querylist
What am I doing wrong please?
In order to be able to use the __in filter the followers should be an iterable of Users. Try this:
followers = [f.follower for f in Follow.objects.filter(followee=user)]
or
followers = [f.follower for f in user.follower.all()]
You can filter with a JOIN, like:
def get_followers(queryset, request, *args, **kwargs):
return queryset.filter(user__follower__followee_id=kwargs['user'])
This will fetch the items in a single query, and thus avoid first querying the followers and then obtain the items.

How do you hide Django model fields with foreign keys?

I have a seminar that has several multiple choice questions.
Here is the seminar model:
class Seminar(models.Model):
seminar_name = models.CharField(max_length = 60)
is_active = models.BooleanField(default = True)
seminar_date = models.DateField(null=True, blank=True)
And here is the model for the multiple choice questions:
class Page(models.Model):
seminar = models.ForeignKey(Seminar)
multiple_choice_group = models.ForeignKey(MultipleChoiceGroup, null=True, blank=True, verbose_name="Multiple Choice Question")
page_id = models.IntegerField(verbose_name="Page ID")
I have the following in admin.py:
class PageInline(admin.StackedInline):
model = Page
extra = 0
def get_ordering(self, request):
return ['page_id']
# Limit the multiple choice questions that match the seminar that is being edited.
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == "multiple_choice_group":
try:
parent_obj_id = request.resolver_match.args[0]
kwargs["queryset"] = MultipleChoiceGroup.objects.filter(seminar = parent_obj_id)
except IndexError:
pass
return super(PageInline, self).formfield_for_foreignkey(db_field, request, **kwargs)
class SeminarAdmin(admin.ModelAdmin):
model = Seminar
inlines = (PageInline,)
list_display = ('seminar_name', 'is_active')
def get_ordering(self, request):
return ['seminar_name']
This is how the seminar appears:
Screenshot showing seminar fields
As shown in the screenshot, I'd like to hide some of the fields from the Seminar model and make some of them read-only.
Any help would be appreciated.
You can use the Model.Admin exclude and readonly_fields attributes. See documentation here and here.
For example:
class SeminarAdmin(admin.ModelAdmin):
model = Seminar
inlines = (PageInline,)
list_display = ('seminar_name', 'is_active')
def get_ordering(self, request):
return ['seminar_name']
exclude = ('seminar_name',)
readonly_fields = ('is_active',)

Django Admin Inline hit database multiple times

DjangoVersion:2.1.7
PythonVersion:3.8.2
I have a StoreLocation model which has ForeignKeys to Store, City and Site (this is the default DjangoSitesFramework model). I have created TabularInline for StoreLocation and added it to the Store admin. everything works fine and there isn't any problem with these basic parts.
now I'm trying to optimize my admin database queries, therefore I realized that StoreLocationInline will hit database 3 times for each StoreLocation inline data.
I'm using raw_id_fields, managed to override get_queryset and select_related or prefetch_related on StoreLocationInline, StoreAdmin and even in StoreLocationManager, i have tried BaseInlineFormsSet to create custom formset for StoreLocationInline. None of them worked.
Store Model:
class Store(models.AbstractBaseModel):
name = models.CharField(max_length=50)
description = models.TextField(blank=True)
def __str__(self):
return '[{}] {}'.format(self.id, self.name)
City Model:
class City(models.AbstractBaseModel, models.AbstractLatitudeLongitudeModel):
province = models.ForeignKey('locations.Province', on_delete=models.CASCADE, related_name='cities')
name = models.CharField(max_length=200)
has_shipping = models.BooleanField(default=False)
class Meta:
unique_together = ('province', 'name')
def __str__(self):
return '[{}] {}'.format(self.id, self.name)
StoreLocation model with manager:
class StoreLocationManager(models.Manager):
def get_queryset(self):
return super().get_queryset().select_related('store', 'city', 'site')
class StoreLocation(models.AbstractBaseModel):
store = models.ForeignKey('stores.Store', on_delete=models.CASCADE, related_name='locations')
city = models.ForeignKey('locations.City', on_delete=models.CASCADE, related_name='store_locations')
site = models.ForeignKey(models.Site, on_delete=models.CASCADE, related_name='store_locations')
has_shipping = models.BooleanField(default=False)
# i have tried without manager too
objects = StoreLocationManager()
class Meta:
unique_together = ('store', 'city', 'site')
def __str__(self):
return '[{}] {} / {} / {}'.format(self.id, self.store.name, self.city.name, self.site.name)
# removing shipping_status property does not affect anything for this problem.
#property
def shipping_status(self):
return self.has_shipping and self.city.has_shipping
StoreLocationInline with custom FormSet:
class StoreLocationFormSet(BaseInlineFormSet):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.queryset = self.queryset.select_related('site', 'city', 'store')
class StoreLocationInline(admin.TabularInline):
model = StoreLocation
# i have tried without formset and multiple combinations too
formset = StoreLocationFormset
fk_name = 'store'
extra = 1
raw_id_fields = ('city', 'site')
fields = ('is_active', 'has_shipping', 'site', 'city')
# i have tried without get_queryset and multiple combinations too
def get_queryset(self, request):
return super().get_queryset(request).select_related('site', 'city', 'store')
StoreAdmin:
class StoreAdmin(AbstractBaseAdmin):
list_display = ('id', 'name') + AbstractBaseAdmin.default_list_display
search_fields = ('id', 'name')
inlines = (StoreLocationInline,)
# i have tried without get_queryset and multiple combinations too
def get_queryset(self, request):
return super().get_queryset(request).prefetch_related('locations', 'locations__city', 'locations__site')
so when i check Store admin change form, it will do this for each StoreLocationInline item:
DEBUG 2020-04-13 09:32:23,201 [utils:109] (0.000) SELECT `locations_city`.`id`, `locations_city`.`is_active`, `locations_city`.`priority`, `locations_city`.`created_at`, `locations_city`.`updated_at`, `locations_city`.`latitude`, `locations_city`.`longitude`, `locations_city`.`province_id`, `locations_city`.`name`, `locations_city`.`has_shipping`, `locations_city`.`is_default` FROM `locations_city` WHERE `locations_city`.`id` = 110; args=(110,)
DEBUG 2020-04-13 09:32:23,210 [utils:109] (0.000) SELECT `django_site`.`id`, `django_site`.`domain`, `django_site`.`name` FROM `django_site` WHERE `django_site`.`id` = 4; args=(4,)
DEBUG 2020-04-13 09:32:23,240 [utils:109] (0.000) SELECT `stores_store`.`id`, `stores_store`.`is_active`, `stores_store`.`priority`, `stores_store`.`created_at`, `stores_store`.`updated_at`, `stores_store`.`name`, `stores_store`.`description` FROM `stores_store` WHERE `stores_store`.`id` = 22; args=(22,)
after i have added store to the select_related, the last query for store disappeared. so now i have 2 queries for each StoreLocation.
I have checked the following questions too:
Django Inline for ManyToMany generate duplicate queries
Django admin inline: select_related
Problem is caused by ForeignKeyRawIdWidget.label_and_url_for_value()
as #AndreyKhoronko mentioned in comments, this problem is caused by ForeignKeyRawIdWidget.label_and_url_for_value() located in django.contrib.admin.widgets which will hit database with that key to generate a link to admin change form.
class ForeignKeyRawIdWidget(forms.TextInput):
# ...
def label_and_url_for_value(self, value):
key = self.rel.get_related_field().name
try:
obj = self.rel.model._default_manager.using(self.db).get(**{key: value})
except (ValueError, self.rel.model.DoesNotExist, ValidationError):
return '', ''
try:
url = reverse(
'%s:%s_%s_change' % (
self.admin_site.name,
obj._meta.app_label,
obj._meta.object_name.lower(),
),
args=(obj.pk,)
)
except NoReverseMatch:
url = '' # Admin not registered for target model.
return Truncator(obj).words(14, truncate='...'), url

django rest update_or_create

Background:
I am trying to create a system which allows users to vote on comments written by other users (similar to Reddit). Users have the choice of three vote values: -1, 0, or 1. I have created a POST API (using django rest-framework) that will store a user's vote with respect to a particular comment. If the user has already voted on the given comment, then it will update the existing user’s vote value to the new one.
I drew inspiration from this post:Django RF update_or_create
Problem:
Once a comment has had one user submit a vote on it, Django creates a duplicate comment object with the same ID/primary key whenever another user votes on the same comment. I have taken a screenshot of my admin page where it says I have 3 objects to select but only a single comment. Why is it doing this and how can I prevent it?
Screenshot of my comment admin page
I am new to Django. I suspect I might be doing something wrong when I define my own "create" method in my serializer. I'd appreciate any help. Thanks!
models.py:
Comment model:
class Comment(models.Model):
location_property_category = models.ForeignKey('locations.LocationPropertyCategory',on_delete=models.CASCADE,related_name='comments',null=True)
author = models.ForeignKey('auth.User',on_delete=models.PROTECT,related_name='comments')
location = models.ForeignKey('locations.Location',on_delete=models.CASCADE,related_name='comments')
text = models.TextField()
create_date = models.DateTimeField(default=timezone.now())
published_date = models.DateTimeField(blank=True,null=True)
approved_comment = models.BooleanField(default=False)
objects = CommentManager()
def approve(self):
self.approved_comment = True
self.save()
def __str__(self):
return self.text
def save(self, *args, **kwargs):
if self.approved_comment is True:
self.published_date = timezone.now()
super(Comment, self).save(*args, **kwargs)
def sum_vote(self):
return self.upvotedownvotes.aggregate(Sum('vote')).get('vote__sum') or 0
Vote model:
class UpVoteDownVote(models.Model):
UPVOTE = 1
NEUTRALVOTE = 0
DOWNVOTE = -1
VOTES = (
(UPVOTE, 'Upvote'),
(NEUTRALVOTE, 'Neutralvote'),
(DOWNVOTE, 'Downvote')
)
vote = models.SmallIntegerField(choices=VOTES)
user = models.ForeignKey('auth.User', related_name='upvotedownvotes', on_delete=models.CASCADE)
comment = models.ForeignKey(Comment, related_name='upvotedownvotes', on_delete=models.CASCADE)
date_voted = models.DateTimeField(default=timezone.now())
class Meta:
unique_together = (('user','comment'),)
Comment manager model:
class CommentManager(models.Manager):
def get_queryset(self):
return super(CommentManager, self).get_queryset().order_by('-upvotedownvotes__vote')
serializers.py
Vote serializer:
class UpVoteDownVoteSerializer(serializers.ModelSerializer):
class Meta:
model = UpVoteDownVote
fields = ('vote','comment')
def get_fields(self):
fields = super(UpVoteDownVoteSerializer, self).get_fields()
fields['comment'].queryset = Comment.objects.filter(approved_comment=True)
return fields
def create(self, validated_data):
votedata, created = UpVoteDownVote.objects.update_or_create(
user=validated_data.get('user', None),
comment=validated_data.get('comment', None),
defaults={'vote': validated_data.get('vote', None),
})
return votedata
views.py
class UpVoteDownVoteCreateApiView(generics.CreateAPIView):
serializer_class = UpVoteDownVoteSerializer
permission_classes = [IsAuthenticated]
def perform_create(self,serializer):
serializer.save(user=self.request.user)
Comment app admin.py
class CommentAdmin(admin.ModelAdmin):
readonly_fields = ('id',)
admin.site.register(Comment,CommentAdmin)
Welcome to StackOverflow!
Your problem is in CommentManager:
queryset.order_by('-upvotedownvotes__vote')
This query basically creates LEFT_OUTER_JOIN. So you result looks like:
comment#1 upvote#1
comment#1 upvote#2
comment#1 upvote#3
That is why you are seeing comment#1 3 times.
I believe that you want to use something like this: https://docs.djangoproject.com/en/2.1/topics/db/aggregation/#order-by

django admin - how to reverse filter on multiple foreign keys

Django 1.8:
As shown below, I have Location model with a foreign key to the Study model.
I want to display Studies that include country='usa' and is_active=True.
The problem with using the default filter is that if I have:
study1:
country='usa', is_active=False
country='canada', is_active=True
The filter displays this study but it should not so here is what I've tried:
##### models.py:
class Study(models.Model):
title = models.CharField(max_length=30)
def __unicode__(self):
return self.title
class Location(models.Model):
is_active = models.BooleanField()
country = models.CharField(max_length=30)
study = models.ForeignKey(Study, related_name='location')
def __unicode__(self):
return self.country
##### admin.py
class ActiveCountryFilter(admin.SimpleListFilter):
title = _('Active Country Filter')
parameter_name = 'country'
def lookups(self, request, model_admin):
# distinct keyword is not supported on sqlite
unique_countries = []
if DATABASES['default']['ENGINE'].endswith('sqlite3'):
countries = Location.objects.values_list('country')
for country in countries:
if country not in unique_countries:
unique_countries.append(country)
else:
unique_countries = Location.objects.distinct('country').values_list('country')
return tuple([(country, _('active in %s' % country)) for country in unique_countries])
def queryset(self, request, queryset):
if self.value():
return queryset.filter(location__country=self.value(), location__is_active=True)
else:
return queryset
#admin.register(Study)
class StudyAdmin(admin.ModelAdmin):
list_display = ('title',)
list_filter = ('location__country', ActiveCountryFilter)
The filter is not displaying anything.
Any ideas how to achieve what I want?
I've also read the doc about RelatedFieldListFilter but it's pretty confusing.
I figured out.
Just a minor bug in the lookups. I had to pick the country at index zero:
Correction:
return tuple([(country[0], _('active in %s' % country[0])) for country in unique_countries])
I hope this sample helps others.