Hi !
I have a Team model and a Member model. The Team model has a captain field.
Now, in my ModelForm, for the captain field, I want to filter the autocomplete to show only members of the team that I'm editing. This means I need to pass a team_id parameter alongside the q parameter.
I think it's a fairly commun scenario, just like forward=['continent']. I googled the issue and found some links, but they all are quite complex hacks combining advanced django and javascript, and I couldn't make them work (I'm no javascript expert).
I'd suggest either :
forward but to send the modelform's instance's pk insteadThanks a lot for the awesome module !
Could you show what have you tried ?
Thanks !
@olivierdalang do you hide the team field in the form?
I think that he's talking about the TeamForm, so that's why there'd be no team_id field. So perhaps one way would be the pass the url of the form as referer in the ajax request, and reverse it from the autocomplete view. Not the prettiest option, but that would be the lightest. Any suggestion here would be nice !
hmm I just thought that if there would be an additional field ( pk ) rendered as a hidden Django field, then it could be passed via forward() like any other.. am I wrong?
This is absolutely correct, we just need to make sure that this field is read-only then. So that's a nicer option, but requires a bit more setup.
@jpic @toudi
indeed, there's no team_idfield.
The ID as hidden field is probably the easiest idea, but I didn't find a clean way to do it in django admin. It seems since the id is not editable, it's not possible to display it as a form.
I tried to edit select2's options directly in the html :
class TeamForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['captiain'].widget.attrs = {'data-ajax--data':self.instance.pk}
But this replaces the whole query, instead of extending it ( ?17 instead of ?q=blabla&17 ).
So I tried this (setting the pk as an attribute, then load it to select2 options via javascript)
class TeamForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['captiain'].widget.attrs = {'forward-pk':self.instance.pk}
yl.jQuery( document ).ready( function(){
yl.jQuery("[dal-pk-param]").each(function() {
var pk = yl.jQuery(this).attr('dal-pk-param');
yl.jQuery(this).select2({
ajax: {
data: function (term, page) {
return { 'q': term.term,'pk': pk };
},
}
});
});
});
This almost works, but it resets the whole select2 instead of extending it. So the URL for instance would have to be set again... I didn't find how to extend properties of a select2 object.
this looks like an interesting problem. I'll try tackling it, probably during the night.
cheers,
m.
@toudi Awesome ! I'll document how to extend the autocomplete script which will probably take a refactor to allow method override, for a wider range of use cases.
@olivierdalang so did you consider adding the team id as such ?
class TeamForm(forms.ModelForm):
class Meta:
fields = your_fields + ['id']
widgets = {'id': forms.HiddenInput, 'captain': autocomplete.ModelSelect2(forward=['id'])}
@jpic i can't try again right now, but I'm not sure it is easy/possible in the admin since the id is not editable, so it doesn't appear as a field. And wouldnt adding it to the form (even if hidden) make it possible for a malicious user to change the id?
Ok, chaps, sorry for the long delay, I couldn't get to the computer.
Anyways, I think this turned out to be pretty easy problem (or am I this naive?). Here it goes:
# the models:
class Team(models.Model):
name = models.CharField(max_length=255)
captain = models.OneToOneField(
'team.TeamMember',
related_name='+',
null=True,
blank=True
)
def __unicode__(self):
return self.name
class TeamMember(models.Model):
team = models.ForeignKey('team.Team')
name = models.CharField(max_length=32)
def __unicode__(self):
return '%s/%s' % (self.team.name, self.name)
# the forms:
from django import forms
from team.models import Team
from dal import autocomplete
class TeamForm(forms.ModelForm):
team_id = forms.IntegerField(
required=False, widget=forms.HiddenInput)
def __init__(self, *args, **kwargs):
instance = kwargs.get('instance', None)
if instance:
kwargs['initial'] = {'team_id': instance.pk}
super(TeamForm, self).__init__(*args, **kwargs)
class Meta:
model = Team
widgets = {
'captain': autocomplete.ModelSelect2(
url='team-member-autocomplete',
forward=['team_id']
)
}
exclude = []
def clean_captain(self):
captain = self.cleaned_data.get('captain')
if captain and captain.team_id != self.instance.id:
raise forms.ValidationError("Invalid captain for this team!")
return captain
# the views
from dal import autocomplete
from team.models import TeamMember
class TeamMemverAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
# Don't forget to filter out results depending on the visitor !
team_id = self.forwarded['team_id']
if not team_id:
return TeamMember.objects.none()
qs = TeamMember.objects.filter(team_id=team_id)
if self.q:
qs = qs.filter(name__icontains=self.q)
return qs
# the admin:
from django.contrib import admin
from team.models import Team, TeamMember
from team.forms import TeamForm
class TeamAdmin(admin.ModelAdmin):
form = TeamForm
admin.site.register(Team, TeamAdmin)
admin.site.register(TeamMember)
I don't know if this is the desired result for @olivierdalang , however this works in the following way:
yours truly,
@toudi Indeed, that's it ! I just tested and it works.
I was confused because my admin classes have fieldsets, and the additional field doesn't appear unless I list it in the fieldsets too... It then appears in the form, even if hidden, but it's acceptable if I set label=''
Thanks for the help !
you could also override the way formset is rendered, and manually inject the {{ form.fields.team_id }} into some other field.
@jpic do you think that this should be some built-in feature of the autocomplete app ? Or is this easy enough that it can be implemented per project?
cheers,
m.
Pretty awesome @toudi, as usual :guitar:
Well we could have something like that, however, I think what garantees the best stability for v3 is not only a more "light" design than v2 (more like v1, but a lot better !), it's also where the limit is set on this version. The limit is set by the fact that the bar is raised pretty high to implementing anything major, as it should be.
I mean, have you ever eard about how we were using only 2% of Microsoft Word's features ? Again a very nice proprietary software, but all in all I don't think this feature will be very used, people like to implement security themselves, often because it has to blend in very well and not affect performance too much, also our user base is largely composed of developers, from all over different horizons yes, but mostly developers.
I'm 100% sure your post deserves at least a blog article, it's technically very relevant and also it's a nifty trick that should be shared :+1:
Any of you guys going to europycon ? Or perhaps we could have a hangouts session to discuss this ?
This example should be included into the docs and into the test project.
I have spent couple of hours searching the docs and trying to implement this myself.
@toudi thank you for a good example!
@utapyngo agreed, please feel free to provide a PR on the documentation for others.