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?
Related
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.
I have created the following models. Many Accounts belong to one Computer.
class Computer(models.Model):
name = models.CharField(max_length=256)
def __str__(self):
return str(self.name)
class Accounts(models.Model):
computer = models.ForeignKey(Computer, on_delete=models.CASCADE, related_name='related_name_accounts')
username = models.CharField(max_length=10)
def __str__(self):
return str(self.username)
I now want to create a Form where i can choose one user from a dropdown-list of all users that belong to a Computer
class ComputerForm(forms.ModelForm):
class Meta:
model = Computer
exclude = ['name',]
def __init__(self, *args, **kwargs):
super(ComputerForm, self).__init__(*args, **kwargs)
self.fields['user']=forms.ModelChoiceField(queryset=Computer.related_name_accounts.all())
But i get an error
'ReverseManyToOneDescriptor' object has no attribute 'all'
How do i have to adjust the queryset in __init__ to display all users of a computer?
Edit:
>>> x = Computer.objects.filter(pk=3).get()
>>> x.related_name_accounts.all()
I would somehow need to pass the dynamic pk to the ModelForm?
Your issue is that you used a capital C in Computer.related_name_accounts.all(). This doesn't make sense, because the Computer model doesn't have related fields - it's instances do.
This line of code would work:
computer = Computer.objects.get(id=1)
computer.related_name_accounts.all()
But the issue is, I am not sure what you mean by ...of all users that belong to a Computer, since you don't have a ForeignKey or OneToOne relationship between User and Computer.
You can pass an instance of your model to the form like this:
x = Computer.objects.get(pk=3)
form = ComputerForm(instace=x)
And access it through self.instance in the form methods:
class ComputerForm(forms.ModelForm):
class Meta:
model = Computer
exclude = ['name',]
def __init__(self, *args, **kwargs):
super(ComputerForm, self).__init__(*args, **kwargs)
self.fields['user']=forms.ModelChoiceField(
queryset=self.instance.related_name_accounts.all()
)
I am wondering if it's possible to auto create a related model upon creation of the first model.
This is the models
class Team(models.Model):
name = models.CharField(max_length=55)
class TeamMember(models.Model):
team = models.ForeignKey('Team', on_delete=models.CASCADE, null=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, null=False)
So what I want to do is something like this on the 'Team' model
class Team(models.Model):
name = models.CharField(max_length=55)
#on_new.do_this
TeamMember.team = self
TeamMember.user = request.user
TeamMember.save()
I have tried to find any documentation about this. But only found some example about onetoonefields. But nothing about this.
Appreciate any help. Cheers!
I am assuming you are using forms to create team.
There is no direct way to create the TeamMember instance without the current user(via request). request is available in views only(unless you are using special middleware or third party library to access it), so we can send it form and create the user by overriding the save method of the modelform.
So you can try like this:
# Override the model form's save method to create related object
class TeamForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
self.request = kwargs.pop('request', None)
super(TeamForm, self).__init__(*args, **kwargs)
class Meta:
model = Team
def save(self, **kwargs):
user = self.request.user
instance = super(TeamForm, self).save(**kwargs)
TeamUser.objects.create(team=instance, user=user)
return instance
And use this form in View:
# Update get_form_kwargs methods in create view
class TeamCreateView(CreateView):
form_class = TeamForm
template = 'your_template.html'
def get_form_kwargs(self):
kw = super(TeamCreateView, self).get_form_kwargs()
kw['request'] = self.request
return kw
Update
(from comments)If you have the user FK availble in Team then you can use it to create TeamMember by overriding the save method. Try like this:
class Team(models.Model):
user = models.ForeignKey(User, on_delete=models.SET_NULL)
name = models.CharField(max_length=55)
def save(self, *args, **kwargs): # <-- Override
instance = super(Team, self).save(*args, **kwargs)
TeamMember.objects.create(user=instance.user, team=instance)
return instance
Is it possible show an attribute of foreign key in add_form django?
For example:
#models.py
class Test(models.Model):
name = models.CharField(max_length=60, db_column='Name') # Field name made lowercase.
product=models.ForeignKey(Product, on_delete=models.DO_NOTHING)
class Product(models.Model):
id = models.AutoField(db_column='Id', primary_key=True)
description = models.CharField(max_length=60, db_column='Description')
#admin.py
class TestAdmin(admin.ModelAdmin):
fields = ['name','product','get_description']
readonly_fields = ['get_description']
def get_description(self):
return self.product.description
Naturally this code raise exception the ErrorFields in 'get_description'.
But, does exist a way to show the 'get_description' when I insert an entry of model Test?
Thanks
You can override the ModelAdmin.get_form and alter the form that it uses, to add a field of your choosing, filled with whatever you like. For example:
#admin.py
class TestAdmin(admin.ModelAdmin):
fields = ['name','product']
def get_form(self, request, obj=None, **kwargs):
if obj.product:
# self.form.base_fields is an OrderedDict(field_name=form.Field, ...)
self.form.base_fields['product_description'] = forms.CharField(required=False, initial=obj.product.description)
self.form.base_fields['product_description'].widget.attrs['readonly'] = True
return super(TestAdmin, self).get_form(request, obj, **kwargs)
That will add a readonly text input field as the last field in your "add" form. If you wanted it placed in a different spot witin the form, you'd have to work out some logic to rebuild self.form.base_fields as a new OrderedDict in exactly the order you'd like.
How do I limit the values returned via the ManyToMany relationship and thus displayed in the <SELECT> field on my form to only show the spots which were created by the currently logged in user?
models.py
class Project(models.Model):
owner = models.ForeignKey(User, editable=False)
...
spots = models.ManyToManyField(to='Spot', blank=True, )
class Spot(models.Model):
owner = models.ForeignKey(User, editable=False)
spot_name = models.CharField(max_length=80, blank=False)
forms.py
from django import forms
from .models import Project, Spot
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
exclude = ('owner', )
class SpotForm(forms.ModelForm):
class Meta:
model = Spot
exclude = ('owner', )
I'm using GenericViews for Update and Create and currently see all of the entries everyone has made into Spots when I'm updating or creating a Project. I want to see only the entries entered by the logged in user. For completeness sake, yes, the project.owner and spot.owner were set to User when they were created.
I've tried def INIT in the forms.py and using limit_choices_to on the manytomany field in the model. Either I did those both wrong or that's not the right way to do it.
thank you!
in your forms.py
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
exclude = ('owner', )
def __init__(self, user_id, *args, **kwargs):
self.fields['spots'] = forms.ModelChoiceField(widget=forms.Select, queryset=Project.objects.filter(owner=user_id))
class SpotForm(forms.ModelForm):
class Meta:
model = Spot
exclude = ('owner', )
def __init__(self, user_id, *args, **kwargs):
self.fields['spot_name'] = forms.ModelChoiceField(widget=forms.Select, queryset=Spot.objects.filter(owner=user_id))
in your views.py
user_id = Project.objects.get(owner=request.user).owner
project_form = ProjectForm(user_id)
spot_form = SpotForm(user_id)
As I mentioned above, Dean's answer was really close, but didn't work for me. Primarily because request is not accessible in the view directly. Maybe it is in older Django versions? I'm on 1.9. Thank you Dean, you got me over the hump!
The gist of what's going on is adding User into the kwargs in the View, passing that to the ModelForm, remove User from the kwargs and use it to filter the Spots before the form is shown.
This is the code that worked for my project:
views.py
class ProjectUpdate(UpdateView):
model = Project
success_url = reverse_lazy('projects-mine')
form_class = ProjectForm
def dispatch(self, *args, **kwargs):
return super(ProjectUpdate, self).dispatch(*args, **kwargs)
def get_form_kwargs(self):
kwargs = super(ProjectUpdate, self).get_form_kwargs()
kwargs.update({'user': self.request.user})
return kwargs
forms.py
class ProjectForm(forms.ModelForm):
class Meta:
model = Project
exclude = ('owner', 'whispir_id')
def __init__(self, *args, **kwargs):
user_id = kwargs.pop('user')
super(ProjectForm, self).__init__(*args, **kwargs)
self.fields['spots'] = forms.ModelMultipleChoiceField(queryset=Spot.objects.filter(owner=user_id))
class SpotForm(forms.ModelForm):
class Meta:
model = Spot
exclude = ('owner', )
def __init__(self, *args, **kwargs):
user_id = kwargs.pop('user')
super(SpotForm, self).__init__(*args, **kwargs)
self.fields['spot_name'] = forms.ModelMultipleChoiceField(queryset=Spot.objects.filter(owner=user_id))