Django POST validation seeing number as string - django

I'm a bit confused where to go with this as I thought it would be part of Django's validation... I'm on 1.8 because I'm using an older database connection library that was last tested with 1.8 (rewriting a frontend for old data).
models.py:
class Order(models.Model):
#rest of class#
RequestorNumber = models.SmallIntegerField(db_column='requestor_no')
class Requestor(models.Model):
RequestorNumber = models.SmallIntegerField(primary_key=True, db_column="requester_no")
Requestor = models.CharField(max_length=20, db_column = "requester")
def __str__(self):
return self.Requestor
forms.py
class OrderForm(forms.ModelForm):
RequestorNumber = forms.ModelChoiceField(queryset=Requestor.objects.all().order_by('RequestorNumber'), label="Requestor")
So this creates a correct dropdown in the template, with values as integers and text as the descriptions ex:
<option value="1" selected="selected">JOHN DOE</option>
When the form is submitted, the POST QueryDict has a proper entry when printing the entire request:
...
'OrderForm-RequestorNumber': ['1']
...
but this is coming in as a string (as I would expect), but the validator when doing is_valid() kicks back and the webpage gets:
'JOHN DOE' value must be an integer.
Is this by design? I feel like it's trying ignore the value of the selected for the form and referring back to the object's __str__ definition as what needs to be saved. If this is dumb, i'm also all ears to figure out what a more correct method is, the only problem is I can't change the DB schema, and all tables are managed=False in the meta.
EDIT: I overwrote the clean_RequestorNumber in the form to literally output the value it thinks is supposed to be saved, and it's giving the value of the __str__ of the method rather than the primary key.
I need to change this behavior but I can't nail down the spot in source code where the validation is being done. Between models.py, fields.py, and widgets.py i can see the required, valid_choice, and other validations but I can't spot where this is being pushed around. Once I can spot it I can try writing my own class but I can't figure out what to overwrite.

class OrderForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
super(OrderForm, self).__init__(*args, **kwargs)
self.fields['RequestorNumber'].choices = [(x.pk, x.Requestor) for x in Requestor.objects.all()]
RequestorNumber = forms.ChoiceField()
Doing it the old way before ModelChoiceField was a thing validates correctly. Not sure if this was fixed past 1.8 but I'm still having trouble finding the spot in the source that would provide the incorrect behavior for the validation. If someone can point me in the correct direction I'd be happy as I would still rather fix the version I'm on so I can have some cleaner code to deal with. Going to leave this up for a week if someone can help and then just mark this as the answer.

Related

How can I support AutoField(primary_key=False) in django?

I need to add an autoinc field that is not the primary key. I am in the process of migrating a very large production database that uses autoincrementing fields to models.UUIDField. I have been doing a piecewise migration, and all of my relationships are now duplicated with both field types. I'm ready to make the primary key swap, but unfortunately I still need to keep the auto incrementing integer field for old clients as it becomes deprecated.
Since django will not allow me to define an autofield with primary_key=False (even though this is fully supported at the db layer), i'm looking for a simple solution. My initial strategy would be to simply change the field to models.BigIntegerField('GUID', db_index=True, null=True, unique=True) and then manually set the default nextval('my_guid_seq'::regclass) using migrations.RunSQL. So far so good, except not. It turns out, because of my null=True declaration, django at the ORM layer is taking over and inserting null which will not allow defaults at the database layer to do it's job.
The core developers are fast to reject this request because of bad design, which I most definetly agree with, but there are very valid use cases such as this. https://code.djangoproject.com/ticket/8576
I am a very weak django developer so I don't want to get in the weeds metaprogramming at the ORM layer. This is by definition a hack, so i'm looking for the least complex, creative solution that gets me around this limitation
You could subclass AutoField and override the _check_primary_key method.
from django.db.models.fields import AutoField
from django.db.models.fields import checks
class AutoFieldNonPrimary(AutoField):
def _check_primary_key(self):
if self.primary_key:
return [
checks.Error(
"AutoFieldNonPrimary must not set primary_key=True.",
obj=self,
id="fields.E100",
)
]
else:
return []
See AutoField source code here
Edit: Updated Link
I know, changing the primary key to UUID is such a pain.Hence the simple and better solution that I think of is to add another integer field that is auto-incrementing in nature.
Here is my solution:
class ModelName(models.Model):
auto_inc_id = models.IntegerField()
Then override the save model:
def save(self, *args, **kwargs):
self.object_list = ModelName.objects.order_by('auto_inc_id')
if len(self.object_list) == 0: # if there are no objects
self.auto_inc_id = 1
else:
self.auto_inc_id = self.object_list.last().auto_inc_id + 1
super(ModelName, self).save()
Couldn't format this as a comment, but modifying #Abhimanyu's answer to make the save method more concise (and issue only one query). Same model property:
class ModelName(models.Model):
auto_inc_id = models.IntegerField()
And here's the save method on the model:
def save(self, *args, **kwargs):
self.auto_inc_id = ModelName.objects.all().count() + 1
super(ModelName, self).save()

Django ModelForms: Display ManyToMany field as single-select

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

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 modelformset creates new record instead of updating existing one

I have a System that can have one or more Models. I have modeled this relationship in the database with a manytomany field. The code below is for editing the system and its associated methods in a single form.
Adding a new method by filling out its form and pressing submit works only the first time. If I then make a small change and submit again, I get the following message (generated by the code below):
METHODFORMSET.ERRORS: [{}, {'name': [u'Method with this Name already exists.']}]
This is caused by the fact that the name field is unique, but it should have updated, not created a new record, even though I am using the POST data to generate the methodformset instance...
Note that this behaviour only applies to the last appended method instance, not to ones that were already present in the table.
Here is the relevant code, can anyone let me know what I am doing wrong?
def sysedit(request, sys_id):
system = System.objects.get(id=sys_id)
MethodFormSet = modelformset_factory(Method, form=MethodForm)
post = None
if request.POST:
post = request.POST.copy()
if 'add_method' in request.POST:
post['method-TOTAL_FORMS'] = repr(int(
post['method-TOTAL_FORMS'])+ 1)
systemform = SystemForm(data=post, instance=system)
methodformset = MethodFormSet(data=post, prefix='method',
queryset=Method.objects.filter(id__in=system.method.all()))
if methodformset.is_valid():
mfs = methodformset.save()
print 'SAVED-method', mfs
for mf in mfs:
if systemform.is_valid():
sp = systemform.save(mf)
print 'SYSTEM', sp
else:
print 'SYSFORMSET.ERRORS:', systemform.errors
else:
print 'METHODFORMSET.ERRORS:', methodformset.errors
return render_to_response('sysedit.html',
{'systemform': systemform,
'methodformset': methodformset,
'system': system},
context_instance=RequestContext(request))
class System(models.Model):
method = models.ManyToManyField(Method)
...
class Method(models.Model):
name = models.CharField(unique=True)
...
class MethodForm(ModelForm):
class Meta:
model = Method
class SystemForm(ModelForm):
def save(self, new_method=None, commit=True, *args, **kwargs):
m = super(SystemForm, self).save(commit=False, *args, **kwargs)
if new_method:
m.method.add(new_method)
if commit:
m.save()
return m
class Meta:
model = System
exclude = ('method')
[EDIT after Sergzach's answer]:
The problem is not how to deal with the Method with this name already exists error, but to prevent that from occurring in the first place. I think the actual problem may have something to do with the way modelformsets deal with new forms. Somehow it looks like it always tries to create a new instance for the last formset, regardless of whether it already exits.
So if I do not add a new formset after the last one was appended, the modelformset will try to re-create the last one (even though it was just created on the previous submit).
The initial situation is that I have 1 valid Method instance and 1 new unbound instance in the methodformset. I then fill out the form and hit save, which validates both Methods and binds the 2nd one, which is then saved to the table.
So far all is well, but if I then hit save the 2nd time the error occurs. Maybe this has to do with the fact that method-TOTAL_FORMS=2 and method-INITIAL_FORMS=1. Could it be that this causes modelformset to force a create on the 2nd Method?
Can anyone confirm/deny this?
[Edit after a weekend of not looking at the code]:
The problem is caused by the fact that I am saving the forms in the view and after saving, I am sending the original methodformset instance (from before the save) to the template. The problem can be solved by re-instantiating modelformset after the save, using the queryset and NOT the POST data.
So the general rule to prevent errors like this, is either to go to a different page after a save (avoid it altogether), or use the above solution.
Before I post this as THE solution, I need to do more testing.
You can validate each form when saving a formset. I have created a simple example (similar to your code) and it works well for me. It creates new objects if there is no object with a such name otherwise it edits an existing object.
You need a form to edit your model objects:
class EditMethodForm( forms.ModelForm ):
class Meta:
model = Method
exclude = ( 'name', )
Then instead of methodformset.is_valid() you do the next:
for methodform in methodformset:
try:
instance = Method.objects.get( name = request.POST[ 'name' ] )
except Method.DoesNotExist:
methodform.save()
else:
editmethodform = EditMethodForm( request.POST, instance = instance )
if editmethodform.is_valid():
editmethodform.save()
There are some additional features in your code. I show the working principle. Is it enough to understand the solution?
I have solved the problem by re-instantiating modelformset after the save (see edit at the bottom of the question)

Dynamically added form fields are removed in form.cleaned_data

I put some client-side Javascript in my template that allows a user to dynamically add fields to a form. My problem is that these fields are cleaned in form.cleaned_data, so I can't access them that way.
All the fields are accessible in request.POST, so I could just solve this problem with that, but I want to do this the "right way" and I think that the solution lies somewhere in using django forms rather than reading the request directly.
I tried overriding form.clean(), but it seems like the data is already gone by the time it gets there.
Other details: I am naming these fields fieldname_x, where x is a number. In request.POST, request.POST['fieldname'] is a list of a all the values, but form.cleaned_data contains only the last value of each list.
Do you know what type these fields are going to be beforehand? Are they just going to be simple text fields? I've done something similar to this, creating dynamic forms.
# make sure these exist by examining request.POST
custom_fields = ['fieldname_1', 'fieldname_2']
attrs = dict((field, forms.CharField(max_length=100, required=False))
for field in custom_fields)
DynamicForm = type("DynamicForm", (YourBaseForm,), attrs)
submitted_form = DynamicForm(request.POST)
Your submitted form should now contain all the fields you require, along with their values. You might want to remove required=False, but that's up to you.
What this does, is perform a dynamic subclass of your base form, adding the attributes passed in as attrs to the class definition. So when you create an instance with post data, they should be mapped correctly.
Edit:
I read the question a little more closely. What you'll want to do is ensure that your dynamic input elements are named correctly, and the values map to those fieldnames once it reaches django. Otherwise, request.POST will not fill the form in correctly.
<input type='text' name='fieldname_1' value='value_for_field_1' />
etc
It is also possible to do this work in your form file, here is an excellent demonstration by Jacob Kaplan-Mosse for dynamic forms : http://jacobian.org/writing/dynamic-form-generation/ that applies quite well for this problem.
What is done is adding a methods to you form class that add the extra dynamic fields and then yields the information from the clean so that you can get it in your view.
class MyForm(forms.Form):
text = forms.CharField(max_length=30)
def __init__(self, *args, **kwargs):
extra = kwargs.pop('extra')
super(MyForm, self).__init__(*args, **kwargs)
for i, question in enumerate(extra):
self.fields['fieldname_%s' % i] = forms.CharField(label=question)
def extra_fields(self):
for name, value in self.cleaned_data.items():
if name.startswith('fieldname_'):
yield (self.fields[name].label, value)
And to call it from the view :
def doSomething(request, extra_fields):
form = MyForm(request.POST or None, extra=extra_fields)
if form.is_valid():
for (question, answer) in form.extra_answers():
save_answer(request, question, answer)
return redirect("create_user_success")
return render_to_response("template/form.html", {'form': form})
Very neat, congrats to Jacob Kaplan-Moss
I'm doing this in the project I'm currently working on. You'll want to have a look at formsets. But be warned, it isn't pretty. You have to modify this stupid hidden field that stores to the form count, and you have to rename the inputs correctly.. which involves a lot of pointless string parsing. If they had used arrays, like PHP, there wouldn't be an issue, but they're hell bent on making my life miserable... rambles off
Edit:
http://docs.djangoproject.com/en/dev/topics/forms/formsets/#empty-form they provide this which makes your life 5% less painful... you can just do a find and replace on prefix with the appropriate number.. but I think I had an issue where the empty form doesn't contain the proper default values if you initialize the form with post data, so I had to create a 2nd instance without post data to an actually empty empty_form, which is just retarded.