Django ForeignKey limit_choices_to a different ForeignKey id - python

I'm trying to limit Django Admin choices of a ForeignKey using limit_choices_to, but I can't figure out how to do it properly.
This code does what I want if the category id is 16, but I can't figure out how to use the current category id rather than hard-coding it.
class MovieCategory(models.Model):
category = models.ForeignKey(Category)
movie = models.ForeignKey(Movie)
prefix = models.ForeignKey('Prefix', limit_choices_to={'category_id': '16'},
blank=True, null=True)
number = models.DecimalField(verbose_name='Movie Number', max_digits=2,
blank=True, null=True, decimal_places=0)
Is it possible to refer to the id of the category ForeignKey somehow?

After hours of reading semi related questions I finally figured this out.
You can't self reference a Model the way I was trying to do so there is no way to make Django act the way I wanted using limit_choices_to because it can't find the id of a different ForeignKey in the same model.
This can apparently be done if you change the way Django works, but a simpler way to solve this was to make changes to admin.py instead.
Here is what this looks like in my models.py now:
# models.py
class MovieCategory(models.Model):
category = models.ForeignKey(Category)
movie = models.ForeignKey(Movie)
prefix = models.ForeignKey('Prefix', blank=True, null=True)
number = models.DecimalField(verbose_name='Movie Number', max_digits=2,
blank=True, null=True, decimal_places=0)
I simply removed limit_choices_to entirely.
I found a similar problem here with the solution posted by Kyle Duncan. The difference though is that this uses ManyToMany and not ForeignKey. That means I had to remove filter_horizontal = ('prefix',) under my class MovieCategoryAdmin(admin.ModelAdmin): as that is only for ManyToMany fields.
In admin.py I had to add from django import forms at the top to create a form. This is how the form looks:
class MovieCategoryForm(forms.ModelForm):
class Meta:
model = MovieCategory
fields = ['prefix']
def __init__(self, *args, **kwargs):
super(MovieCategoryForm, self).__init__(*args, **kwargs)
self.fields['prefix'].queryset = Prefix.objects.filter(
category_id=self.instance.category.id)
And my AdminModel:
class MovieCategoryAdmin(admin.ModelAdmin):
"""
Admin Class for 'Movie Category'.
"""
fieldsets = [
('Category', {'fields': ['category']}),
('Movie', {'fields': ['movie']}),
('Prefix', {'fields': ['prefix']}),
('Number', {'fields': ['number']}),
]
list_display = ('category', 'movie', 'prefix', 'number')
search_fields = ['category__category_name', 'movie__title', 'prefix__prefix']
form = MovieCategoryForm
This is exactly how Kyle describes it in his answer, except I had to add fields = ['prefix'] to the Form or it wouldn't run. If you follow his steps and remember to remove filter_horizontal and add the fields you're using it should work.
Edit: This solution works fine when editing, but not when creating a new entry because it can't search for the category id when one doesn't exits. I am trying to figure out how to solve this.

Another approach, if you don't want to add a custom ModelForm, is to handle this in your ModelAdmin's get_form() method. This was preferable for me because I needed easy access to the request object for my queryset.
class StoryAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
form = super(StoryAdmin, self).get_form(request, obj, **kwargs)
form.base_fields['local_categories'].queryset = LocalStoryCategory.\
objects.filter(office=request.user.profile.office)
return form

Keep in mind that limit_choices_to supports "Either a dictionary, a Q object, or a callable returning a dictionary or Q object" and should theoretically support any lookup that can be done using django's queryset filtering. A potential solution would then be filtering based on some property of the category that you control such as a slug field.
class MovieCategory(models.Model):
category = models.ForeignKey(Category)
movie = models.ForeignKey(Movie)
prefix = models.ForeignKey('Prefix', blank=True, null=True,
limit_choices_to=Q(category__slug__startswith='movie'))
number = models.DecimalField(verbose_name='Movie Number', max_digits=2,
blank=True, null=True, decimal_places=0)

I had the same question and your self-answer helped me get started. But I also found another post (question-12399803) that completed the answer, that is, how to filter when creating a new entry.
In views.py
form = CustomerForm(groupid=request.user.groups.first().id)
In forms.py
def __init__(self, *args, **kwargs):
if 'groupid' in kwargs:
groupid = kwargs.pop('groupid')
else:
groupid = None
super(CustomerForm, self).__init__(*args, **kwargs)
if not groupid:
groupid = self.instance.group.id
self.fields['address'].queryset = Address.objects.filter(group_id=groupid)
So, whether adding a new customer or updating an existing customer, I can click on a link to go add a new address that will be assigned to that customer.
This is my first answer on StackOverflow. I hope it helps.

Related

How can I restrict the list of objects in API view once an object has been added to a relationship?

I am working in django-rest-framework and I have three models: Event, Performer, and Link. I have many-to-many relationships established on the Event and Performer models as 'links' pointing to the Link model. In the API view, when I am creating or updating an event or performer, I am given a list of all links. I would like them to be removed as options once they've been associated with another object, but I can't seem to figure out how to. Below is my code:
class Link(models.Model):
created = models.DateTimeField(auto_now_add=True)
address = models.URLField()
def __str__(self):
return f"{self.address}"
class Meta:
ordering = ['created']
class Performer(models.Model):
created = models.DateTimeField(auto_now_add=True)
first_name = models.CharField(max_length=20)
last_name = models.CharField(max_length=20)
links = models.ManyToManyField(Link)
def __str__(self):
return f"{self.first_name} {self.last_name}"
class Meta:
ordering = ['created']
class Event(models.Model):
created = models.DateTimeField(auto_now_add=True)
sale_date = models.DateTimeField()
event_date = models.DateTimeField()
performer = models.ForeignKey(Performer, on_delete=models.CASCADE)
links = models.ManyToManyField(Link)
class Meta:
ordering = ['event_date']
and I'm using this for serializers:
class LinkSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Link
fields = ['url', 'address']
class PerformerSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Performer
fields = ['url', 'first_name', 'last_name', 'links']
class EventSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = Event
fields = ['url', 'performer', 'sale_date', 'event_date', 'links']
I thought about using
ManyToManyField.limit_choices_to
but I don't know what my selector would look like. I also thought I could use
Link.objects.exclude(...)
or
Link.objects.filter(...)
call somewhere but I just don't know where. Thanks to anyone who can help!
Edit: thought I’d add that what I thought would work is to use ‘limit_choices_to’ to filter out any links that are included in a relationship, but I couldn’t figure out how to test if an object was in a relationship (and since there’s multiple relationships only testing for one isn’t perfect either)
You should make use of the Serializer class' get_queryset method:
class LinkSerializer(serializers.HyperlinkedModelSerializer):
def get_queryset(self):
return super().get_queryset().filter(performer=None, event=None)
class Meta:
model = Link
fields = ['url', 'address']
I figured out what I was trying to accomplish with this: I needed to restrict the choices for the field at the model level, which I was able to do by passing a predetermined restriction to the 'limit_choices_to=' parameter. See code below and thank you to #anthony2261 for the suggestion, your filter section helped me to understand how to filter even though it wasn't the type of filtering I needed!
# create a dict of filter conditions(?)
restrict_choices = {'performer': None, 'event': None}
class Performer(...):
...
# refer to the restriction defined previously
# when defining the links relationship.
links = models.ManyToManyField(Link, limit_choices_to=restrict_choices)

Django Form: Only show manytomany objects from logged in user

The current problem is that my form shows the logged in user all Portfolios ever created. The form should only show portfolios that the logged-in user created.
Something like this:
associated_portfolios manytomany field = ...objects.filter(user=user_id)
I'm not sure if this should be implemented in the forms.py or views.py and if so how. I've been going through the django documentation and found 'formfield_for_manytomany' but not sure if this is only meant for admin.
Models.py
class Portfolio(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
name = models.CharField(max_length=20)
description = models.CharField(max_length=250, blank=True, null=True)
class Post(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
title = models.CharField(max_length=50)
body = RichTextUploadingField(blank=True, null=True)
associated_portfolios = models.ManyToManyField(Portfolio, blank=True)
created_on = models.DateField(auto_now_add=True, editable=False)
Views.py
class PostCreate(CreateView):
model = Post
form_class = PostCreateForm
def formfield_for_manytomany(self, db_field, request, **kwargs):
self.fields['associated_portfolios'] = Portfolio.objects.filter(user=self.request.user)
return super().formfield_for_manytomany(db_field, request, using=self.using, **kwargs)
forms.py
class PortfolioCreateForm(ModelForm):
class Meta:
model = Portfolio
fields = ['user', 'name', 'description']
class PostCreateForm(ModelForm):
class Meta:
model = Post
fields = ['user', 'title', 'body', 'category', 'associated_portfolios']
Since you're using a ModelForm, the associated_protfolios field will be a ModelMultipleChoiceField [docs]. This field has a queryset attribute [docs]. We want to modify that attribute.
Django's CreateView has a method get_form, which in this case will grab your PostCreateForm. This is a good spot to filter the field's queryset, since we have access to the user:
class PostCreate(CreateView):
model = Post
form_class = PostCreateForm
def get_form(self, *args, **kwargs):
form = super().get_form(*args, **kwargs) # Get the form as usual
user = self.request.user
form.fileds['associated_portfolios'].queryset = Portfolio.objects.filter(user=user)
return form
Did you try this
self.fields['associated_portfolios'] = Post.objects.filter(associated_portfolios__portfolio__user=request.user)
OR
user_posts = Post.objects.filter(user=request.user)
self.fields['associated_portfolios'] = user_posts.associated_portfolios.all()
read more about M2M relationships querying here, because I think your problem may be with it.
Also, I'm not sure about your actual data maybe it's right and it gives a correct result as filtering Portfolio model against current user to get its objects looks right for me, but anyway double check everything again.
And as a final note, add related_name to your model fields so you can use it easily for reverse relations rather than going with Django's default naming, it will be clearer and give a better understanding.

Specific Queryset for Input on Django ModelAdmin Change Form

I've got a 'Registration' object in place that users can create on the front end without issue.
It looks like this:
class Registration(models.Model):
person = models.ForeignKey(Person, on_delete=models.PROTECT)
course_detail = models.ForeignKey(CourseDetail, on_delete=models.PROTECT)
camp_shirt = models.ForeignKey(CampShirt, on_delete=models.PROTECT)
comments = models.CharField(max_length=200, blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return "%s" % (self.course_detail.course.camp)
When I am in the admin and click on a given Registration - it takes a while to load because there are thousands and thousands of Person objects.
For ease of use - there will never be a time when we would need to edit the 'person' associated with a given registration, so I would like to make the 'person' dropdown only show the selected user in the person queryset when editing from the django admin.
So when I go to http://myapp.com/admin/registration/23/change I want the form to only display the currently selected person as the only option in the dropdown.
My admin model looks like this:
class RegistrationAdmin(admin.ModelAdmin):
list_display = ("person", "course_detail")
class Meta:
# I think this is what I do in order to override the default admin form? Not sure.
form = RegistrationAdminForm
My RegistrationAdminForm looks like this:
class RegistrationAdminForm(forms.ModelForm):
# course_detail, person, camp_shirt, comments
person = forms.ModelChoiceField(queryset=Person.objects.filter(
id=registration.person.id)
)
def __init__(self, registration, *args, **kwargs):
super(RegistrationAdminForm, self).__init__(*args, **kwargs)
self.fields['person'].queryset = Person.objects.filter(
id=registration.person.id
)
class Meta:
model = Registration
fields = '__all__'
Main Question : How do I change the admin form so that a specific queryset is returned for one of the fields in the django admin?
If the person field will never be changed you can add the person field to readonly_fields, a select with all Person objects will not be rendered.
class RegistrationAdmin(admin.ModelAdmin):
list_display = ("person", "course_detail")
readonly_fields = ("person", )
Then you do not need your custom form. FYI when you want to add a custom form to a ModelAdmin you do not put it in Meta, you define it on the form itself
class MyModelAdmin(admin.ModelAdmin):
form = MyModelForm

Django: How to limit_choices_to when using custom intermediate table

Let me start by saying that I am working with a legacy database so avoiding the custom intermediate table is not an option.
I'm looking for an alternative way to get the limit_choices_to functionality as I need to only present the options flagged by the sample_option boolean in the Sampletype Model in my ModelForm:
class PlanetForm(ModelForm):
class Meta:
model = Planet
fields = ['name', 'samples']
Here is a simplified view of my models
class Planet(models.Model):
name= models.CharField(unique=True, max_length=256)
samples = models.ManyToManyField('Sampletype', through='Sample')
class Sample(models.Model):
planet = models.ForeignKey(Planet, models.DO_NOTHING)
sampletype = models.ForeignKey('Sampletype', models.DO_NOTHING)
class Sampletype(models.Model):
name = models.CharField(unique=True, max_length=256)
sample_option = models.BooleanField(default=True)
Sample is the intermediate table.
Normally, if the project had been started with Django in the first place, I could just define the ManyToManyField declaration as:
samples = models.ManyToManyField('Sampletype', limit_choices_to={'sample_option'=True})
But this is not an option.. So how do I get this functionality ?
Django clearly states in their documentation that:
limit_choices_to has no effect when used on a ManyToManyField with a
custom intermediate table specified using the through parameter.
But they offer no information on how to get that limit in place when you DO have a custom intermediate table.
I tried setting the limit_choices_to option on the ForeignKey in the Sample Model like so:
sampletype = models.ForeignKey('Sampletype', models.DO_NOTHING, limit_choices_to={'sample_option': True})
but that had no effect.
Strangely, I find no answer to this on the web and clearly other people must have to do this in their projects so I'm guessing the solution is really simple but I cannot figure it out.
Thanks in advance for any help or suggestions.
You could set the choices in the __init__ method of the form:
class PlanetForm(ModelForm):
class Meta:
model = Planet
fields = ['name', 'samples']
def __init__(self, *args, **kwargs):
super(PlanetForm, self).__init__(*args, **kwargs)
sample_choices = list(
Sampletype.objects.filter(sample_option=True).values_list('id', 'name')
)
# set these choices on the 'samples' field.
self.fields['samples'].choices = sample_choices

Help with django bugtrack comments system using ModelForm

I'm trying to add a comments component to a bug tracking application using django. I have a text field for comments and a by field--auto-propagated by user id.
I want the comments text field to become read-only after someone saves a comment. I've tried doing this several ways. The best way I have come up with so far is to pass my Comment model into a ModelForm and then use form widget attributes to convert my field to read only.
models.py
class CommentForm(ModelForm):
class Meta:
model = Comment
exclude = ('ticket', 'submitted_date', 'modified_date')
def __init__(self, *args, **kwargs):
super(CommentForm, self).__init__(*args, **kwargs)
instance = getattr(self, 'instance', None)
if instance and instance.id:
self.fields['comments'].widget.attrs['readonly'] = True
class Comment(models.Model):
ticket = models.ForeignKey(Ticket)
by = models.ForeignKey(User, null=True, blank=True, related_name="by")
comments = models.TextField(null=True, blank=True)
submitted_date = models.DateField(auto_now_add=True)
modified_date = models.DateField(auto_now=True)
class Admin:
list_display = ('comments', 'by',
'submitted_date', 'modified_date')
list_filter = ('submitted_date', 'by',)
search_fields = ('comments', 'by',)
My Comment model is associated with my Ticket model in the bug tracking program. I connect the comments to the tickets by placing the comments in an inline in admin.py. The problem now becomes: how do I pass the ModelForm into a TabularInline? TabularInline demands a defined model. However, once I've passed a model into my inline, passing a model form becomes moot.
admin.py
class CommentInline(admin.TabularInline):
model = Comment
form = CommentForm()
search_fields = ['by', ]
list_filter = ['by', ]
fields = ('comments', 'by')
readonly_fields=('by',)
extra = 1
Does anyone know how to pass a ModelForm into a TabularInline without having a regular Model's fields override the ModelForm? Thanks in advance!
Don't instantiate the form in the TabularInline subclass:
form = CommentForm

Categories