Django ModelForms: Display ManyToMany field as single-select - django

In a Django app, I'm having a model Bet which contains a ManyToMany relation with the User model of Django:
class Bet(models.Model):
...
participants = models.ManyToManyField(User)
User should be able to start new bets using a form. Until now, bets have exactly two participants, one of which is the user who creates the bet himself. That means in the form for the new bet you have to chose exactly one participant. The bet creator is added as participant upon saving of the form data.
I'm using a ModelForm for my NewBetForm:
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
Notice the redefined widget for the participants field which makes sure you can only choose one participant.
However, this gives me a validation error:
Enter a list of values.
I'm not really sure where this comes from. If I look at the POST data in the developer tools, it seems to be exactly the same as if I use the default widget and choose only one participant. However, it seems like the to_python() method of the ManyToManyField has its problems with this data. At least there is no User object created if I enable the Select widget.
I know I could work around this problem by excluding the participants field from the form and define it myself but it would be a lot nicer if the ModelForm's capacities could still be used (after all, it's only a widget change). Maybe I could manipulate the passed data in some way if I knew how.
Can anyone tell me what the problem is exactly and if there is a good way to solve it?
Thanks in advance!
Edit
As suggested in the comments: the (relevant) code of the view.
def new_bet(request):
if request.method == 'POST':
form = NewBetForm(request.POST)
if form.is_valid():
form.save(request.user)
... # success message and redirect
else:
form = NewBetForm()
return render(request, 'bets/new.html', {'form': form})

After digging in the Django code, I can answer my own question.
The problem is that Django's ModelForm maps ManyToManyFields in the model to ModelMultipleChoiceFields of the form. This kind of form field expects the widget object to return a sequence from its value_from_datadict() method. The default widget for ModelMultipleChoiceField (which is SelectMultiple) overrides value_from_datadict() to return a list from the user supplied data. But if I use the Select widget, the default value_from_datadict() method of the superclass is used, which simply returns a string. ModelMultipleChoiceField doesn't like that at all, hence the validation error.
To solutions I could think of:
Overriding the value_from_datadict() of Select either via inheritance or some class decorator.
Handling the m2m field manually by creating a new form field and adjusting the save() method of the ModelForm to save its data in the m2m relation.
The seconds solution seems to be less verbose, so that's what I will be going with.

I don't mean to revive a resolved question but I was working a solution like this and thought I would share my code to help others.
In j0ker's answer he lists two methods to get this to work. I used method 1. In which I borrowed the 'value_from_datadict' method from the SelectMultiple widget.
forms.py
from django.utils.datastructures import MultiValueDict, MergeDict
class M2MSelect(forms.Select):
def value_from_datadict(self, data, files, name):
if isinstance(data, (MultiValueDict, MergeDict)):
return data.getlist(name)
return data.get(name, None)
class WindowsSubnetForm(forms.ModelForm):
port_group = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=PortGroup.objects.all())
class Meta:
model = Subnet

The problem is that ManyToMany is the wrong data type for this relationship.
In a sense, the bet itself is the many-to-many relationship. It makes no sense to have the participants as a manytomanyfield. What you need is two ForeignKeys, both to User: one for the creator, one for the other user ('acceptor'?)

You can modify the submitted value before (during) validation in Form.clean_field_name. You could use this method to wrap the select's single value in a list.
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
widgets = {
'participants': forms.Select()
}
def save(self, user):
... # save user as participant
def clean_participants(self):
data = self.cleaned_data['participants']
return [data]
I'm actually just guessing what the value proivded by the select looks like, so this might need a bit of tweaking, but I think it will work.
Here are the docs.

Inspired by #Ryan Currah I found this to be working out of the box:
class M2MSelect(forms.SelectMultiple):
def render(self, name, value, attrs=None, choices=()):
rendered = super(M2MSelect, self).render(name, value=value, attrs=attrs, choices=choices)
return rendered.replace(u'multiple="multiple"', u'')
The first one of the many to many is displayed and when saved only the selected value is left.

I found an easyer way to do this inspired by #Ryan Currah:
You just have to override "allow_multiple_selected" attribut from SelectMultiple class
class M2MSelect(forms.SelectMultiple):
allow_multiple_selected = False
class NewBetForm(forms.ModelForm):
class Meta:
model = Bet
participants = forms.ModelMultipleChoiceField(widget=M2MSelect, required=True, queryset=User.objects.all())

Related

Django widget that depends on model

I am pretty new to django and have a question. I got a ModelForm using Widgets. Since I have a field called discount which I only want to be editable if the displayed model fullfills some requirements, I make it read-only using a widget entry:
class Meta:
widgets = {'discount': forms.TextInput(attrs={'readonly': True})}
Now I want to make it possible to write to this field again, iff the Model (here called Order) has its field type set to integer value 0.
I tried to do so in the html template but failed.
So my next idea is to make the widget somehow dependent to the model it displays, so in kinda pseudocode:
class Meta:
widgets = {'discount': forms.TextInput(attrs={'readonly': currentModel.type == 0})}
Is there a proper way to do something like this?
Thanks in advance
You can overwrite __init__ of your model form class to modify the widget:
class MyModelForm(...):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs):
instance = kwargs['instance']
self.fields['discount'].widget.attrs['readonly'] = instance.type == 0
Might be worth noting that setting the widget to readonly does not prevent a malicious user to modify the field anyways. Make sure to properly validate on the server side.
Unless you define a model instance on the form creation, you have a generic model definition, not a model instance.
Looks like you may want to change the form behavior after user interaction, to do that you have to use a javascript.

Django ModelForm hide field from form and use value from url

Thanks in advance for reading this. I can't wrap my head around it and it's getting quite frustrating by now.
We have the following registration form:
class RegistrationForm(forms.ModelForm):
class Meta:
model = Register
fields = ('name', 'company_name')
def clean(self):
if is not self.cleaned_data.get('card').is_available():
raise forms.ValidationError(_('Error'))
The Register model includes a card linked to a Card model. This includes is_available() which functionally works.
Our flow is:
The end user selects the card which lists all registrations for it.
They click the 'Add registration'-button which brings them to cards/{PK}/add.
The Add registration-button is a generic.View. In post(self, request, pk) I have the following code:
form = RegistrationForm(request.POST)
But how do I pass it the contents of Card.objects.get(pk=pk) to it?
I tried:
data = request.POST.copy()
data['card'] = pk
form = RegistrationForm(data)
But I think because card is not included in fields it gets lost somewhere, which makes sense from a sanitize-all-input-point of view, but I would very much like to add the card dynamically, in this case.
Any ideas?
So, just use CreateView and study how it does things using the linked site.
There is no need to use generic.View as it's the basic of basics. You only want to implement all this logic using generic.View to get more familiar with the way things work or if you need some very special form handling.
The short version would be:
from django.views import generic
from myapp.forms import RegistrationForm
class CardCreateView(generic.CreateView):
form_class = RegistrationForm
ModelForm has a save method. The correct way to solve this is to use it with commit=False, that will return an object that hasn’t yet been saved to the database. Then you can alter that object before finally saving it.
This is explained here in the docs
So this is what your code should look like:
form = RegistrationForm(request.POST)
form.save(commit=False)
form.card = Card.objects.get(pk=pk)
form.save_m2m()
save_m2m should be used if your model has many-to-many relationships with other models. In my case, it was a OneToOne, so I used save() instead.
If you use a CreateView instead of the generic View, the snippet above should go into your overridden form_valid method

How to set a model field based on the current user in a Django CreateView [duplicate]

I have a model named Domain which looks like this:
class Domain(models.Model):
"""
Model for storing the company domains
"""
user = models.ForeignKey(
User
)
host = models.CharField(
null=False, verbose_name="Host", max_length=128, unique=True
)
I'd like to use Django's generic views for doing CRUD operations on this. There is one field in this model that needs user input but the foreign key field doesn't need any user input. How can I exclude that field from the form that my generic view generates but assign it the value of the current authenticated user.
Thanks.
Have a look at Russel's answer to a similar question on the django-users group earlier this week.
Quoting the answer*:
Forms and Views solve different problems.
The View is solving the problem of "how do I handle this request and
convert it into a response?". The Form is solving the problem of "How
do I convert the POST data in this request into a model object (or a
change to a model object)?".
Very roughly, a view is doing the following:
View gets a request
View works out whether this is a GET or a POST
If its a POST, View asks the Form to turn the Post into a model change
Form returns success or failure
View responds to the success or failure of the Form.
View returns a response.
The functionality of the Form is a complete subset of the
functionality of the View -- and for this reason, it's a completely
interchangable internal component.
Now, in simple situations, it's possible for a View to guess all the
defaults for the form -- all it needs to know is that you're dealing
with a Foo model, and it can construct a default Foo ModelForm.
However, if you have more sophisticated form requirements, you're
going to need a customized Form.
We could have implemented this by exposing all the options of
ModelForm on the View class; but in order to keep everything clean, we
kept the ModelForm isolated, and provided the View with a way to
specify which Form class it's going to use.
So - to cover your use case of excluding fields, you define a
ModelForm that excludes the fields, then let the CreateView know the
form you want to use:
class CampaignForm(forms.ModelForm):
class Meta:
model = Campaign
exclude = ('user', 'name', 'content_inlined')
class CreateCampaignView(CreateView):
form_class = CampaignForm
template_name = "forms/create.html"
I'm guessing when you say "fix a values for a field", you mean setting
the values of user, name and content_inlined before you save the new
Campaign instance; to do this, you need to inject some extra code into
the form processing logic of the form:
class CreateCampaignView(CreateView):
form_class = CampaignForm
template_name = "forms/create.html"
def form_valid(self, form):
form.instance.user = ... (something meaningful.. e.g., self.request.user)
return super(CreateCampaignView, self).form_valid(form)
This overrides the default behavior when the form is valid, and sets
the extra values. The super() implementation of form_valid() will then
save the instance.
For the record, this could also be done by overriding the save()
method on the ModelForm -- however, if you do that, you lose the
request object, which you will need if you're trying to set the
instance values to something that is request-sensitive.
*the original answer set self.object.user instead of form.instance.user. This gives an AttributeError so I have changed it above.

How to pass in a parameter to the form within the formset?

I would like to create a formset, where each form has a dropdown pointing to a set of sales items.
Model:
class SalesItem(models.Model):
item_description = models.CharField(max_length=40)
company = models.ForeignKey(Company)
Here I create a form with a dropdown, hoping to pass in the company as a source for the dropdown. Hold on to this thought, as I think that is not possible in my scenario.
Form:
class SalesItemFSForm(Form):
sales_item = forms.ModelChoiceField(required=False, queryset = '')
def __init__(self, company, *args, **kwargs):
super(SalesItemFSForm, self).__init__(*args, **kwargs)
self.fields.sales_item.queryset = company.salesitem_set.all()
Now within my view I would like to create a formset with this form:
formset_type = formset_factory(SalesItemFSForm, extra=0)
The problem becomes right away clear, as there seem to be no way that I could pass in the company to determine the source for the dropdown.
How am I supposed to do this?
Many Thanks,
Update:
it seems Jingo cracked it. :)
A ModelForm works better than a Form. On top of it I had to add fields = {} to SalesItemFSForm, to make sure that the SalesItem's fields are not showing up in the template. Because all we are interested in is our dropdown (SalesItem).
So far so good. But now I see as many dropdowns shown as I have Salesitems. It shouldn;t show any unless the user presses a jquery button.
And I think this is the problem, we should NOT pass in
formset_type = modelformset_factory(SalesItem, form=SalesItemFSForm, extra=0)
Because our form doesn't need any instance of the SalesItem. We need a dummy Model.
That was the reason I tried to solve it initially with classic Formset instead of ModelFormset. So its kind of half way there. :)
Update 2:
Jingo, good point. Effectively I was thinking of a custom save, where I just see how many formsets are added by the user via jQuery and save it myself within the view. Literally SalesItem is a ManyToMany field. But the standard M2m widget is horrible. Hence I wanted to replace it with formsets, where each salesItem is a dropdown. The user can then add as many dropdowns (forms in formset) to the page and submit them. Then I would add the relationship in the view.
class DealType(models.Model):
deal_name = models.CharField(_(u"Deal Name"), max_length=40)
sales_item = models.ManyToManyField(SalesItem)
price = models.DecimalField(decimal_places=2, max_digits=12)
Hope this makes it clear. Maybe there is an easier way to do this. :)
Btw I also found this excellent jquery snippet code how to add/remove forms to/from a formset.
Update 3:
Indeed when instantiating the object like this, we would only get one form in the formset and can add more via jquery. Perfect!! Unless there is an easier way to achieve this. :)
salesitem_formsets = formset_type(queryset=SalesItem.objects.filter(pk=1))
However this comes back hunting you in the request.POST, since you can't just do:
salesitem_formsets = formset_type(request.POST)
It still requires the queryset to be set. Tricky situation...
I hope I understood the goal you want to achieve right. Then maybe you could use ModelForm and its available instance like this:
class SalesItemFSForm(forms.ModelForm):
class Meta:
model = SalesItem
def __init__(self, *args, **kwargs):
super(SalesItemFSForm, self).__init__(*args, **kwargs)
self.sale_items = self.instance.company.salesitem_set.all()
self.fields['sales_item'] = forms.ModelChoiceField(queryset=self.sale_items)
This is untested though and just a thought. I hope this leads into the right direction, but if its totally wrong, let me know and i will remove my answer, so that others wont be confused :).

Django: Read only field

How do I allow fields to be populated by the user at the time of object creation ("add" page) and then made read-only when accessed at "change" page?
The simplest solution I found was to override the get_readonly_fields function of ModelAdmin:
class TestAdmin(admin.ModelAdmin):
def get_readonly_fields(self, request, obj=None):
'''
Override to make certain fields readonly if this is a change request
'''
if obj is not None:
return self.readonly_fields + ('title',)
return self.readonly_fields
admin.site.register(TestModel, TestAdmin)
Object will be none for the add page, and an instance of your model for the change page.
Edit: Please note this was tested on Django==1.2
There's two thing to address in your question.
1. Read-only form fields
Doesn't exist as is in Django, but you can implement it yourself, and this blog post can help.
2. Different form for add/change
I guess you're looking for a solution in the admin site context (otherwise, just use 2 different forms in your views).
You could eventually override add_view or change_view in your ModelAdmin and use a different form in one of the view, but I'm afraid you will end up with an awful load of duplicated code.
Another solution I can think of, is a form that will modify its fields upon instantiation, when passed an instance parameter (ie: an edit case). Assuming you have a ReadOnlyField class, that would give you something like:
class MyModelAdminForm(forms.ModelForm):
class Meta:
model = Stuff
def __init__(self, *args, **kwargs):
super(MyModelAdminForm, self).__init__(*args, **kwargs)
if kwargs.get('instance') is not None:
self.fields['title'] = ReadOnlyField()
In here, the field title in the model Stuff will be read-only on the change page of the admin site, but editable on the creation form.
Hope that helps.
You can edit that model's save method to handle such a requirement. For example, you can check if the field already contains some value, if it does, ignore the new value.
One option is to override or replace the change_form template for that specific model.