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
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)
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.
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
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
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