mptt TreeNodeChoiceField filtered - django

I am starting to work with django.
In my project I am using MPTT.
I have a fairly simple models.
Category: (the tree)
Product1: a reference and a foreign key to the category.
I added the root to the Category model, just to simplify the search for the base category for one specific product
models.py
class Category(MPTTModel):
name = models.CharField(max_length=200)
parent = TreeForeignKey('self', null=True, blank=True, related_name='children')
#property
def root(self):
return self.get_root().name
class Product1(models.Model):
ref = models.CharField(max_length=200, blank=True) #YVX2311
category = TreeForeignKey(Category)
admin.py
admin.site.register(Category, MPTTModelAdmin)
admin.site.register(Product)
What I would like is to modify the admin form, to be able to say something like.
models.ForeignKey(Category, limit_choices_to = {root : 'Some
Category for products1'})
Of course this is not possible, but it does illustrate what I want.
I want to be able to filter the TreeChoiseField to show only some branches based on a property on the model (root in this case).
Thanks

After much scrambling, I found a solution.
I do not think this is the best way to do it, but as far as I can see, it works.
I overloaded the mptt TreeNodeChoiceField and copied the TreeForeignKeyFiltered
In products, I changed the foreingkey for
category = TreeForeignKeyFiltered(Category, root = 'MotherBoard')
I added the root keyword to the TreeForeingKeyFiltered
class TreeForeignKeyFiltered(models.ForeignKey):
"""
Extends the foreign key, but uses mptt's ``TreeNodeChoiceField`` as
the default form field.
This is useful if you are creating models that need automatically
generated ModelForms to use the correct widgets.
"""
def __init__(self, *args, **kwargs):
self._root = kwargs.pop('root', None)
super(TreeForeignKeyFiltered, self).__init__(*args, **kwargs)
def formfield(self, **kwargs):
"""
Use MPTT's ``TreeNodeChoiceField``
"""
kwargs.setdefault('root', self._root)
kwargs.setdefault('form_class', TreeNodeChoiceFieldFiltered)
return super(TreeForeignKeyFiltered, self).formfield(**kwargs)
And finally I added the TreeNodeChoiceFieldFiltered as
class TreeNodeChoiceFieldFiltered(TreeNodeChoiceField):
def __init__(self, queryset, *args, **kwargs):
_root = kwargs.pop('root', None)
if _root is not None:
pid = None
for i in queryset:
if i.root.name == _root:
pid = i.tree_id
break
if pid is not None:
queryset = queryset.filter(tree_id = pid)
super(TreeNodeChoiceFieldFiltered, self).__init__(queryset, *args, **kwargs)
Now I can pass a keyword root to the foreingkey that will filter the posibilities in the admin form.
Again, I do not know if this is the best way to do it. Of course it can be cleaner....

Related

Django (DRF) select related on is_valid()?

My problem is that when trying to run is_valid() on a big chunk of data in a POST-request, where the model has a foreign key, it will fetch the foreign key table for each incoming item it needs to validate.
This thread describes this as well but ended up finding no answer:
Django REST Framework Serialization POST is slow
This is what the debug toolbar shows:
My question is therefore, is there any way to run some kind of select_related on the validation? I've tried turning off validation but the toolbar still tells me that queries are being made.
These are my models:
class ActiveApartment(models.Model):
adress = models.CharField(default="", max_length=2000, primary_key=True)
company = models.ForeignKey(Company, on_delete=models.CASCADE)
class Company(models.Model):
name = models.CharField(blank=True, null=True, max_length=150)
These are my serializers:
I have tried not using the explicit PrimaryKeyRelatedField as well, having validators as [] doesn't seem to stop the validation either for some reason.
class ActiveApartmentSerializer(serializers.ModelSerializer):
company = serializers.PrimaryKeyRelatedField(queryset=Company.objects.all())
class Meta:
model = ActiveApartment
list_serializer_class = ActiveApartmentListSerializer
fields = '__all__'
extra_kwargs = {
'company': {'validators': []},
}
class ActiveApartmentListSerializer(serializers.ListSerializer):
def create(self, validated_data):
data = [ActiveApartment(**item) for item in validated_data]
# Ignore conflcits is the only "original" part of this create method
return ActiveApartment.objects.bulk_create(data, ignore_conflicts=True)
def update(self, instance, validated_data):
pass
This is my view:
def post(self, request, format=None):
# Example of incoming data in request.data
dummydata = [{"company": 12, "adress": "randomdata1"}, {"company": 12, "adress": "randomdata2"}]
serializer = ActiveApartmentSerializer(data=request.data, many=True)
# This will run a query to check the validity of their foreign keys for each item in dummydata
if new_apartment_serializer.is_valid():
print("valid")
Any help would be appreciated (I would prefer not to use viewsets)
Have you tried to define company as IntegerField in the serializer, pass in the view's context the company IDs and add a validation method in field level?
class ActiveApartmentSerializer(serializers.ModelSerializer):
company = serializers.IntegerField(required=True)
...
def __init__(self, *args, **kwargs):
self.company_ids = kwargs.pop('company_ids', None)
super().__init__(*args, **kwargs)
def validate_company(self, company):
if company not in self.company_ids:
raise serializers.ValidationError('...')
return company
The way I solved it was I took the direction athanasp pointed me in and tweaked it a bit as his solution was close but not working entirely.
I:
Created a simple nested serializer
Used the init-method to make the query
Each item checks the list of companies in its own validate method
class CitySerializer(serializers.ModelSerializer):
class Meta:
model = City
fields = ('name',)
class ListingSerializer(serializers.ModelSerializer):
# city = serializers.IntegerField(required=True, source="city_id") other alternative
city = CitySerializer()
def __init__(self, *args, **kwargs):
self.cities = City.objects.all()
super().__init__(*args, **kwargs)
def validate_city(self, value):
try:
city = next(item for item in self.cities if item.name == value['name'])
except StopIteration:
raise ValidationError('No city')
return city
And this is how the data looks like that should be added:
city":{"name":"stockhsdolm"}
Note that this method more or less works using the IntegerField() suggested by athan as well, the difference is I wanted to use a string when I post the item and not the primary key (e.g 1) which is used by default.
By using something like
city = serializers.IntegerField(required=True, source="city_id") works as well, just tweak the validate method, e.g fetching all the Id's from the model using values_list('id', flat=True) and then simply checking that list for each item.

Creating dynamic ModelChoiseField from users List objects

I have been trying for awhile now without any luck.. I have model Like this:
class List(models.Model):
name = models.CharField(max_length=100, default="")
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='lists')
def __str__(self):
returnself.name
class Meta:
unique_together = ['name', 'user']
Every user can create their own lists and add values to those lists. I have adding values and everything else working but to the form that adds these values I would somehow need to filter to show only users own lists, now its showing all lists created by every user... this is the form:
class data_form(forms.Form):
user_lists = List.objects.all()
selection = forms.ModelChoiceField(queryset=user_lists)
data = forms.IntegerField()
Any ideas how to filter it? I have tempoary "list.objects.all()" since dont want it to give error that crashes the server. I have watched a ton of examples on stackoverflow but none of them seems to be exact thing that I am looking for.. Thanks already for asnwers! :)
You need to get hold of the current user, e.g. like so or so.
That is, you pass request.user to the form when instantiating it in your view:
frm = DataForm(user=request.user)
In the __init__ of your form class, you can then assign the user-filtered queryset to your field:
class DataForm(forms.Form):
def __init__(self, *args, **kwargs):
user = kwargs.pop("user")
super(DataForm, self).__init__(*args, **kwargs)
self.fields['selection'].queryset = List.objects.filter(user=user)
You can set your form to take the user when initialized, and from there get a new queryset filtered by user.
class DataForm(forms.Form):
selection = forms.ModelChoiceField(queryset=List.objects.none())
data = forms.IntegerField()
def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields['selection'].queryset = List.objects.filter(user=user)
You would inititialize the form like this:
form = DataForm(request.user)

Django ModelChoice Field Conditional ForeignKey

I've surfed most of the afternoon and have been at this particular quandry for a while.
I am trying to figure out how to essentially present a foreign key as a dropdown choice if the user has driven that type of car. For example purposes and to keep this as easy as possible...
Let's say I have aCars, Manufacturers and a UserProfile model.
I have a model for Cars as so...
class Cars(models.Model):
car_name = models.CharField(max_length=80)
class = models.ForeignKey(Manufacturer,null=True,on_delete=models.DO_NOTHING,related_name='car_manufacturer')
I have a model for Manufacturers as so...
class Manufacturers(models.Model):
manu_name = models.CharField(max_length=80)
Then I have a UserProfile model....
class Userprofile(models.Model):
user = models.OneToOneField(User,on_delete=models.CASCADE)
user_name = models.CharField(max_length=80)
car_owned = models.ForeignKey(Car,null=True,on_delete=models.DO_NOTHING,related_name='car_owned')
All good so far...
I have a view where I am listing all of the Manufacturers and this works fine as well. It shows all of the manufacturers that I would expect in the form view below.
class ManufacturerForm(forms.Form):
dropdown = forms.ModelChoiceField(queryset=Manufacturer.objects.all())
def __init__(self, *args, **kwargs):
super(ManufacturerForm, self).__init__(*args, **kwargs)
self.fields['dropdown'].widget.attrs['class'] = 'choices1'
self.fields['dropdown'].empty_label = ''
I'm using the FORMVIEW below to display the form...
class ManufacturerView(LoginRequiredMixin,FormView):
form_class = ManufacturerForm
template_name = 'Directory/HTMLNAME.html'
def get_form_kwargs(self):
kwargs = super(ManufacturerView, self).get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_valid(self, form):
manufacturer = form.cleaned_data['dropdown']
return HttpResponseRedirect(reverse("NAME:manufacturer",kwargs={'pk':manufacturer.pk}))
This all works fine. However, I can't figure out how to limit the manufacturer dropdown to only the cars the user has driven. I'm trying to essentially limit the dropdown display to only the manufacturers that are pertinent to the cars the user has owned based on their profile. I've researched reverse look ups and have also tried something similar to what is outlined below to solve my problem...
class ManufacturerForm(forms.Form):
dropdown = forms.ModelChoiceField(queryset=Manufacturer.objects.filter(car_manufacturer=1)
def __init__(self, *args, **kwargs):
super(ManufacturerForm, self).__init__(*args, **kwargs)
self.fields['dropdown'].widget.attrs['class'] = 'choices1'
self.fields['dropdown'].empty_label = ''
But this obviously only gives me record 1 for the Manufacturer model. I am trying to figure out how to display only the records that are relevant to an individual user based on their car_owned data. I can list all of the manufacturers and then just display the records that are relevant in a ListView, but I am trying to limit the dropdown to only where there are relevant records in a ListView. Thanks in advance for any thoughts.
You missed just couple of points:
1) Pass UserProfile as kwargs['user']:
kwargs['user'] = UserProfile.objects.get(user=self.request.user)
2) Add user parameter in form's __init__ signature and override dropdown.queryset there:
class ManufacturerForm(forms.Form):
dropdown = forms.ModelChoiceField(queryset=Manufacturer.objects.all())
def __init__(self, user, *args, **kwargs):
super(ManufacturerForm, self).__init__(*args, **kwargs)
self.fields['dropdown'].widget.attrs['class'] = 'choices1'
self.fields['dropdown'].empty_label = ''
self.fields['dropdown'].queryset = Manufacturer.objects.filter(car_manufacturer__car_owned=user)
Also I would like to recommend to rewrite your Car - User relationship to ManyToMany. If I understand correctly your message, User can have multiple cars:
limit the dropdown display to only the manufacturers that are
pertinent to the cars the user has owned
Also if I understand correctly, you want to track cars that user used to have (but doesn't have anymore).
If you rewrite Car - User relationship, then you won't probably have any reason to keep UserProfile model only to hold additional username. If so, your models.py should look like this:
class Manufacturer(models.Model):
name = models.CharField(max_length=80)
class Car(models.Model):
name = models.CharField(max_length=80)
klass = models.ForeignKey(Manufacturer, null=True, on_delete=models.DO_NOTHING, related_name='car_manufacturer')
owners = models.ManyToManyField(User, through='Ownership')
class Ownership(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
car = models.ForeignKey(Car, on_delete=models.CASCADE)
active = models.BooleanField(default=True) # True if user owns the car at the moment
I think it all hinges on your order and relationships between models, perhaps try adding in a ManyToMany relation between Manufacturer and Car, so one manufacturer can make many cars:
class Manufacturer(models.Model):
name = models.CharField(max_length=80)
car = models.ManyToManyField(Car)
Then it may be a case of doing something such as:
qs = Manufacturer.objects.all(car=user.car_owned)
dropdown = forms.ModelChoiceField(qs)
And in your views.py file:
form = ManufacturerForm(request.POST, user=request.user)
(You may need to look up if the above is valid, as I'm not sure if Forms can have the request object passed in).
Disclaimer: I see you're using a class based view...so I may need to tweak the above.

Django Use Many To Many with Through in Form

I have a data model where I am using a manual intermediate table for a m2m relationship.
Building on the classical example from the django doc:
from django.db import models
INSTRUMENT_CHOICES = (
('guitar', 'Guitar'),
('bass', 'Bass Guitar'),
('drum', 'Drum'),
('keyboard', 'Keyboard'),
)
class Person(models.Model):
name = models.CharField(max_length=128)
def __str__(self):
return self.name
class Group(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(Person, through='Leadership')
def __str__(self):
return self.name
def get_leadership():
return self.leadership_set.first()
class Leadership(models.Model):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
group = models.ForeignKey(Group, on_delete=models.CASCADE)
instrument = models.CharField('Playing Instrument', choices=INSTRUMENT_CHOICES,
max_length=15,
null=True,
blank=False)
class Meta:
unique_together = ('person', 'group')
When I create a new group I also want to specify who is going to be the leader, and for this relationship also specify which instrument he will play in that group.
What really confuses me, given also the lack of documentation on this topic is how to handle this kind of relationship in forms.
This is the form I came with:
class InstrumentField(forms.ChoiceField):
def __init__(self, *args, **kwargs):
super().__init__(INSTRUMENT_CHOICES, *args, **kwargs)
class GroupForm(forms.ModelForm):
instrument = InstrumentField(required=False)
class Meta:
model = Group
fields = ['name',
'members'
'instrument'] # This works but it's not correctly initalized in case of edit form
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
if self.instance.pk is not None: # editing
# PROBLEM: this doesn't work
self.fields["instrument"].initial = self.instance.get_leadership().instrument
def save(self, commit=True):
group = super().save(commit=False)
if commit:
group.save()
if 'instrument' in self.changed_data:
leader = self.cleaned_data.get('members').first()
instrument = self.cleaned_data['instrument']
Leadership.objects.update_or_create(person=leader, group=group, defaults={'instrument': instrument})
return group
As suggested in the django doc I am manually instantiating Leadership objects (see the form save method).
What I couldn't solve is how to populate the instrument field in case of form editing. I try to do this in the __init__: first I check that we are in "edit" mode (the instance has a pk) then I get the relevant Leadership object (see Group.get_leadership) and from that I extract the instrument and I assign it to the fields["instrument"].initial.
This doesn't work.
I could inspect that the initial value was set but then when I render the form the default choice value is shown (the first value of the INSTRUMENT_CHOICES).
What am I missing here?
Is there a better way or a better docs on how to handle m2m with through model in forms?

How do I add a Foreign Key Field to a ModelForm in Django?

What I would like to do is to display a single form that lets the user:
Enter a document title (from Document model)
Select one of their user_defined_code choices from a drop down list (populated by the UserDefinedCode model)
Type in a unique_code (stored in the Code model)
I'm not sure how to go about displaying the fields for the foreign key relationships in a form. I know in a view you can use document.code_set (for example) to access the related objects for the current document object, but I'm not sure how to apply this to a ModelForm.
My model:
class UserDefinedCode(models.Model):
name = models.CharField(max_length=8)
owner = models.ForeignKey(User)
class Code(models.Model):
user_defined_code = models.ForeignKey(UserDefinedCode)
unique_code = models.CharField(max_length=15)
class Document(models.Model):
title = models.CharField(blank=True, null=True, max_length=200)
code = models.ForeignKey(Code)
active = models.BooleanField(default=True)
My ModelForm
class DocumentForm(ModelForm):
class Meta:
model = Document
In regards to displaying a foreign key field in a form you can use the forms.ModelChoiceField and pass it a queryset.
so, forms.py:
class DocumentForm(forms.ModelForm):
class Meta:
model = Document
def __init__(self, *args, **kwargs):
user = kwargs.pop('user','')
super(DocumentForm, self).__init__(*args, **kwargs)
self.fields['user_defined_code']=forms.ModelChoiceField(queryset=UserDefinedCode.objects.filter(owner=user))
views.py:
def someview(request):
if request.method=='post':
form=DocumentForm(request.POST, user=request.user)
if form.is_valid():
selected_user_defined_code = form.cleaned_data.get('user_defined_code')
#do stuff here
else:
form=DocumentForm(user=request.user)
context = { 'form':form, }
return render_to_response('sometemplate.html', context,
context_instance=RequestContext(request))
from your question:
I know in a view you can use
document.code_set (for example) to
access the related objects for the
current document object, but I'm not
sure how to apply this to a ModelForm.
Actually, your Document objects wouldn't have a .code_set since the FK relationship is defined in your documents model. It is defining a many to one relationship to Code, which means there can be many Document objects per Code object, not the other way around. Your Code objects would have a .document_set. What you can do from the document object is access which Code it is related to using document.code.
edit: I think this will do what you are looking for. (untested)
forms.py:
class DocumentForm(forms.ModelForm):
class Meta:
model = Document
exclude = ('code',)
def __init__(self, *args, **kwargs):
user = kwargs.pop('user','')
super(DocumentForm, self).__init__(*args, **kwargs)
self.fields['user_defined_code']=forms.ModelChoiceField(queryset=UserDefinedCode.objects.filter(owner=user))
self.fields['unique_code']=forms.CharField(max_length=15)
views.py:
def someview(request):
if request.method=='post':
form=DocumentForm(request.POST, user=request.user)
if form.is_valid():
uniquecode = form.cleaned_data.get('unique_code')
user_defined_code = form.cleaned_data.get('user_defined_code')
doc_code = Code(user_defined_code=user_defined_code, code=uniquecode)
doc_code.save()
doc = form.save(commit=False)
doc.code = doc_code
doc.save()
return HttpResponse('success')
else:
form=DocumentForm(user=request.user)
context = { 'form':form, }
return render_to_response('sometemplate.html', context,
context_instance=RequestContext(request))
actually you probably want to use get_or_create when creating your Code object instead of this.
doc_code = Code(user_defined_code=user_defined_code, code=uniquecode)