Django: How To use SplitArrayField? - django

I am trying to test out SplitArrayField,
class MYForm(forms.Form):
places = SplitArrayField(forms.IntegerField(), size=4)
Now, When I do:
form = MYForm({'places': [1,2,14,3]})
form.is_valid() returns False
and form.cleaned_data is {}
I checked the official docs, cant find a example. please fix me.
https://docs.djangoproject.com/es/1.9/ref/contrib/postgres/forms/#django.contrib.postgres.forms.SplitArrayField

The way to pass data for a SplitArrayField is like this:
form = MYForm({
'places_0': '1',
'places_1': '2',
'places_2': '14',
'places_3': '3',
})
form.is_valid() # True
form.cleaned_data # {'places': [1, 2, 14, 3]}
The field names need to be "split", and an index needs to be appended. The general format of a field name is '{field_name}_{index}'.
If you were to use a SimpleArrayField, the input data would look like this instead:
class MYForm(forms.Form):
places = SimpleArrayField(forms.IntegerField())
form = MYForm({
'places': '1,2,14,3'
})
form.is_valid() # True
form.cleaned_data # {'places': [1, 2, 14, 3]}

Related

Flask: Save session data with redirect

I am using sessions to store products info to shopping cart. Now that works, however when I add it to the session and then redirect to the cart view, the newly added product id is not saved.
class CartAddProduct(View):
def dispatch_request(self, product_id):
print("*** Product id to add: ", product_id)
if not 'cart' in session:
session['cart'] = []
product_info = {}
product_info['product_id'] = str(product_id)
product_info['amount'] = 1
print("going to append: ", product_info)
print("before: ", session['cart'])
session['cart'].append(product_info)
print("after: ", session['cart'])
return redirect(url_for('cart'))
Response:
*** Product id to add: 5
going to append: {'product_id': '5', 'amount': 1}
before: [{'amount': 1, 'product_id': '9'}]
after: [{'amount': 1, 'product_id': '9'}, {'product_id': '5', 'amount': 1}]
But then after I redirect to "cart" and I print it, its unchanged. The session does not pass with the redirect?
[{'amount': 1, 'product_id': '9'}]
I forgot to add the session.modified = True
https://flask.palletsprojects.com/en/1.1.x/api/#flask.session
Be advised that modifications on mutable structures are not picked up
automatically, in that situation you have to explicitly set the
attribute to True yourself.
So objects of built-in types like (list, set, dict) are mutable.

Testing formsets in Django

How do I pass the values of a formset in a unit test using client self.client.post('/url/', {})? I am getting a validation error saying management_form tampered.
Thanks in advance!
This is an example formset getting user's skills
class SkillForm(forms.Form):
level_id = forms.ChoiceField(choices=LEVEL_CHOICES, required = True)
txt_skills = forms.CharField(max_length=250, required = True)
SkillFormset = formset_factory(SkillForm,extra=1, max_num=10)
and this the sample POST data for test cases.
self.post_data.update({
'skillform-0-level_id': '2',
'skillform-0-txt_skills': 'Python',
'skillform-1-level_id': '3',
'skillform-1-txt_skills': 'Java',
'skillform-TOTAL_FORMS': '2',
'skillform-INITIAL_FORMS': '1',
'skillform-MAX_NUM_FORMS': '10'
})

Is this an error in the documentation?

Today I started reading the documentation for django.forms. The API seems easy to use and I started experimenting with it. Then I started experimenting with django.forms.ModelForm but I cannot really see where I went wrong.
My problem starts here: the save method when creating a form with an instance.
My model is
class Process(models.Model):
key = models.CharField(max_length=32, default="")
name = models.CharField(max_length=30)
path = models.CharField(max_length=215)
author = models.CharField(max_length=100)
canparse = models.NullBooleanField(default=False)
last_exec = models.DateTimeField(null = True)
last_stop = models.DateTimeField(null = True)
last_change = models.DateTimeField(null = True, auto_now=True)
and my form is
class ProcessForm(ModelForm):
class Meta:
model = Process
fields = ('name', 'path', 'author')
I only wanted the name, path and author fields since the other ones are automatically set when saving the model. Anyway, in my test database I already have entries and I've chosen one whose fields are all set and valid.
In the documentation you can read:
# Create a form to edit an existing Article.
>>> a = Article.objects.get(pk=1)
>>> f = ArticleForm(instance=a)
>>> f.save()
Very well, I wanted to do the same with my own code:
>>> from remusdb.models import Process
>>> from monitor.forms import ProcessForm
>>>
>>> proc = Process.objects.get(name="christ")
>>> pf = ProcessForm(instance=proc)
>>> pf.save()
Traceback (most recent call last):
File "<console>", line 1, in <module>
File "/home/shaoran/devpython/lib/python2.6/site-packages/django/forms/models.py", line 364, in save
fail_message, commit, construct=False)
File "/home/shaoran/devpython/lib/python2.6/site-packages/django/forms/models.py", line 87, in save_instance
save_m2m()
File "/home/shaoran/devpython/lib/python2.6/site-packages/django/forms/models.py", line 78, in save_m2m
cleaned_data = form.cleaned_data
AttributeError: 'ProcessForm' object has no attribute 'cleaned_data'
>>> pf.is_bound
False
>>> pf.is_valid()
False
Even though proc is a valid Process object the form object doesn't seem to agree with me. If I do as the next example
>>> post = { "name": "blabla", "path": "/somewhere", "author": "me" }
>>> pf = ProcessForm(post, instance=proc)
>>> pf.is_bound
True
>>> pf.is_valid()
True
>>> pf.cleaned_data
{'path': u'/somewhere', 'name': u'blabla', 'author': u'me'}
then it works like in third example of the documentation.
Am I missing something or is there an error in the documentation? Or is my Model code somewhat wrong?
This is the content of proc
proc.dict
{'name': u'christ', 'last_stop': datetime.datetime(2012, 10, 5, 16, 49, 13, 630040, tzinfo=), 'author': u'unkown', '_state': , 'canparse': False, 'last_exec': datetime.datetime(2012, 10, 5, 16, 49, 8, 545626, tzinfo=), 'key': u'aed72c9d46d2318b99ffba930a110610', 'path': u'/home/shaoran/projects/cascade/remusdb/test/samples/christ.cnf', 'last_change': datetime.datetime(2012, 10, 5, 16, 49, 13, 631764, tzinfo=), 'id': 5}
The first argument to the form class is a dictionary that contains values that you want the form to validate.
Since you never pass these values in, the form cannot validate any input; which is why cleaned_data is none. Since .save() triggers form and model validation, the form validation fails.
You'll notice the form actually has no data:
af.data will be {} (empty dict)
af.is_bound will be False (as you haven't bound the form to any data)
Since there is no data, validation fails. The error is a bit misleading. If you pass in an empty dict:
af = ArticleForm({},instance=a)
af.save()
You'll get a more appropriate error:
ValueError: The Article could not be changed because the data didn't validate.

Django query — how to get list of dictionaries with M2M relation?

Let's say, I have this simple application with two models — Tag and SomeModel
class Tag(models.Model):
text = ...
class SomeModel(models.Model):
tags = models.ManyToManyField(Tag, related_name='tags')
And I want to get something like this from database:
[{'id': 1, 'tags': [1, 4, 8, 10]}, {'id': 6, 'tags': []}, {'id': 8, 'tags': [1, 2]}]
It is list of several SomeModel's dictionaries with SomeModel's id and ids of tags.
What should the Django query looks like? I tried this:
>>> SomeModel.objects.values('id', 'tags').filter(pk__in=[1,6,8])
[{'id': 1, 'tags': 1}, {'id': 1, 'tags': 4}, {'id': 1, 'tags': 8}, ...]
This is not what I want, so I tried something like this:
>>> SomeModel.objects.values_list('id', 'tags').filter(pk__in=[1,6,8])
[(1, 1), (1, 4), (1, 8), ...]
And my last try was:
>>> SomeModel.objects.values_list('id', 'tags', flat=True).filter(pk__in=[1,6,8])
...
TypeError: 'flat' is not valid when values_list is called with more than one field.
—
Maybe Django cannot do this, so the most similar result to what I want is:
[{'id': 1, 'tags': 1}, {'id': 1, 'tags': 4}, {'id': 1, 'tags': 8}, ...]
Is there any Python build-in method which transform it to this?
[{'id': 1, 'tags': [1, 4, 8, 10]}, {'id': 6, 'tags': []}, {'id': 8, 'tags': [1, 2]}]
— EDIT:
If I write method in SomeModel:
class SomeModel(models.Model):
tags = models.ManyToManyField(Tag, related_name='tags')
def get_tag_ids(self):
aid = []
for a in self.answers.all():
aid.append(a.id)
return aid
And then call:
>>> sm = SomeModel.objects.only('id', 'tags').filter(pk__in=[1,6,8])
# Hit database
>>> for s in sm:
... s.get_tag_ids()
...
>>> # Hit database 3 times.
This is not working, because it access to database 4 times. I need just one access.
As ArgsKwargs mentioned here in comments — I write my own code, which packs the list:
>>> sm = SomeModel.objects.values('id', 'tags').filter(pk__in=[1,6,8])
>>> a = {}
>>> for s in sm:
... if s['id'] not in a:
... a[s['id']] = [s['tags'],]
... else:
... a[s['id']].append(s['tags'])
...
The output of this code is exactly what I need, and it hit database only once. But it is not very elegant, I don't like this code :)
Btw. is better use pk or id in queries? .values('id', 'tags') or .values('pk', 'tags')?
What about a custom method on the model that returns a list of all tags
class Tag(models.Model):
text = ...
class SomeModel(models.Model):
tags = models.ManyToManyField(Tag, related_name='tags')
def all_tags(self):
return self.tags.values_list('pk',flat=True)
and then
SomeModel.objects.values('id', 'all_tags').filter(pk__in=[1,6,8])

Django formset unit test

I can't run a unit test with formset.
I try to do a test:
class NewClientTestCase(TestCase):
def setUp(self):
self.c = Client()
def test_0_create_individual_with_same_adress(self):
post_data = {
'ctype': User.CONTACT_INDIVIDUAL,
'username': 'dupond.f',
'email': 'new#gmail.com',
'password': 'pwd',
'password2': 'pwd',
'civility': User.CIVILITY_MISTER,
'first_name': 'François',
'last_name': 'DUPOND',
'phone': '+33 1 34 12 52 30',
'gsm': '+33 6 34 12 52 30',
'fax': '+33 1 34 12 52 30',
'form-0-address1': '33 avenue Gambetta',
'form-0-address2': 'apt 50',
'form-0-zip_code': '75020',
'form-0-city': 'Paris',
'form-0-country': 'FRA',
'same_for_billing': True,
}
response = self.c.post(reverse('client:full_account'), post_data, follow=True)
self.assertRedirects(response, '%s?created=1' % reverse('client:dashboard'))
and I have this error:
ValidationError: [u'ManagementForm data is missing or has been
tampered with']
My view :
def full_account(request, url_redirect=''):
from forms import NewUserFullForm, AddressForm, BaseArticleFormSet
fields_required = []
fields_notrequired = []
AddressFormSet = formset_factory(AddressForm, extra=2, formset=BaseArticleFormSet)
if request.method == 'POST':
form = NewUserFullForm(request.POST)
objforms = AddressFormSet(request.POST)
if objforms.is_valid() and form.is_valid():
user = form.save()
address = objforms.forms[0].save()
if url_redirect=='':
url_redirect = '%s?created=1' % reverse('client:dashboard')
logon(request, form.instance)
return HttpResponseRedirect(url_redirect)
else:
form = NewUserFullForm()
objforms = AddressFormSet()
return direct_to_template(request, 'clients/full_account.html', {
'form':form,
'formset': objforms,
'tld_fr':False,
})
and my form file :
class BaseArticleFormSet(BaseFormSet):
def clean(self):
msg_err = _('Ce champ est obligatoire.')
non_errors = True
if 'same_for_billing' in self.data and self.data['same_for_billing'] == 'on':
same_for_billing = True
else:
same_for_billing = False
for i in [0, 1]:
form = self.forms[i]
for field in form.fields:
name_field = 'form-%d-%s' % (i, field )
value_field = self.data[name_field].strip()
if i == 0 and self.forms[0].fields[field].required and value_field =='':
form.errors[field] = msg_err
non_errors = False
elif i == 1 and not same_for_billing and self.forms[1].fields[field].required and value_field =='':
form.errors[field] = msg_err
non_errors = False
return non_errors
class AddressForm(forms.ModelForm):
class Meta:
model = Address
address1 = forms.CharField()
address2 = forms.CharField(required=False)
zip_code = forms.CharField()
city = forms.CharField()
country = forms.ChoiceField(choices=CountryField.COUNTRIES, initial='FRA')
In particular, I've found that the ManagmentForm validator is looking for the following items to be POSTed:
form_data = {
'form-TOTAL_FORMS': 1,
'form-INITIAL_FORMS': 0
}
Every Django formset comes with a management form that needs to be included in the post. The official docs explain it pretty well. To use it within your unit test, you either need to write it out yourself. (The link I provided shows an example), or call formset.management_form which outputs the data.
It is in fact easy to reproduce whatever is in the formset by inspecting the context of the response.
Consider the code below (with self.client being a regular test client):
url = "some_url"
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
# data will receive all the forms field names
# key will be the field name (as "formx-fieldname"), value will be the string representation.
data = {}
# global information, some additional fields may go there
data['csrf_token'] = response.context['csrf_token']
# management form information, needed because of the formset
management_form = response.context['form'].management_form
for i in 'TOTAL_FORMS', 'INITIAL_FORMS', 'MIN_NUM_FORMS', 'MAX_NUM_FORMS':
data['%s-%s' % (management_form.prefix, i)] = management_form[i].value()
for i in range(response.context['form'].total_form_count()):
# get form index 'i'
current_form = response.context['form'].forms[i]
# retrieve all the fields
for field_name in current_form.fields:
value = current_form[field_name].value()
data['%s-%s' % (current_form.prefix, field_name)] = value if value is not None else ''
# flush out to stdout
print '#' * 30
for i in sorted(data.keys()):
print i, '\t:', data[i]
# post the request without any change
response = self.client.post(url, data)
Important note
If you modify data prior to calling the self.client.post, you are likely mutating the DB. As a consequence, subsequent call to self.client.get might not yield to the same data, in particular for the management form and the order of the forms in the formset (because they can be ordered differently, depending on the underlying queryset). This means that
if you modify data[form-3-somefield] and call self.client.get, this same field might appear in say data[form-8-somefield],
if you modify data prior to a self.client.post, you cannot call self.client.post again with the same data: you have to call a self.client.get and reconstruct data again.
Django formset unit test
You can add following test helper methods to your test class [Python 3 code]
def build_formset_form_data(self, form_number, **data):
form = {}
for key, value in data.items():
form_key = f"form-{form_number}-{key}"
form[form_key] = value
return form
def build_formset_data(self, forms, **common_data):
formset_dict = {
"form-TOTAL_FORMS": f"{len(forms)}",
"form-MAX_NUM_FORMS": "1000",
"form-INITIAL_FORMS": "1"
}
formset_dict.update(common_data)
for i, form_data in enumerate(forms):
form_dict = self.build_formset_form_data(form_number=i, **form_data)
formset_dict.update(form_dict)
return formset_dict
And use them in test
def test_django_formset_post(self):
forms = [{"key1": "value1", "key2": "value2"}, {"key100": "value100"}]
payload = self.build_formset_data(forms=forms, global_param=100)
print(payload)
# self.client.post(url=url, data=payload)
You will get correct payload which makes Django ManagementForm happy
{
"form-INITIAL_FORMS": "1",
"form-TOTAL_FORMS": "2",
"form-MAX_NUM_FORMS": "1000",
"global_param": 100,
"form-0-key1": "value1",
"form-0-key2": "value2",
"form-1-key100": "value100",
}
Profit
There are several very useful answers here, e.g. pymen's and Raffi's, that show how to construct properly formatted payload for a formset post using the test client.
However, all of them still require at least some hand-coding of prefixes, dealing with existing objects, etc., which is not ideal.
As an alternative, we could create the payload for a post() using the response obtained from a get() request:
def create_formset_post_data(response, new_form_data=None):
if new_form_data is None:
new_form_data = []
csrf_token = response.context['csrf_token']
formset = response.context['formset']
prefix_template = formset.empty_form.prefix # default is 'form-__prefix__'
# extract initial formset data
management_form_data = formset.management_form.initial
form_data_list = formset.initial # this is a list of dict objects
# add new form data and update management form data
form_data_list.extend(new_form_data)
management_form_data['TOTAL_FORMS'] = len(form_data_list)
# initialize the post data dict...
post_data = dict(csrf_token=csrf_token)
# add properly prefixed management form fields
for key, value in management_form_data.items():
prefix = prefix_template.replace('__prefix__', '')
post_data[prefix + key] = value
# add properly prefixed data form fields
for index, form_data in enumerate(form_data_list):
for key, value in form_data.items():
prefix = prefix_template.replace('__prefix__', f'{index}-')
post_data[prefix + key] = value
return post_data
The output (post_data) will also include form fields for any existing objects.
Here's how you might use this in a Django TestCase:
def test_post_formset_data(self):
url_path = '/my/post/url/'
user = User.objects.create()
self.client.force_login(user)
# first GET the form content
response = self.client.get(url_path)
self.assertEqual(HTTPStatus.OK, response.status_code)
# specify form data for test
test_data = [
dict(first_name='someone', email='someone#email.com', ...),
...
]
# convert test_data to properly formatted dict
post_data = create_formset_post_data(response, new_form_data=test_data)
# now POST the data
response = self.client.post(url_path, data=post_data, follow=True)
# some assertions here
...
Some notes:
Instead of using the 'TOTAL_FORMS' string literal, we could import TOTAL_FORM_COUNT from django.forms.formsets, but that does not seem to be public (at least in Django 2.2).
Also note that the formset adds a 'DELETE' field to each form if can_delete is True. To test deletion of existing items, you can do something like this in your test:
...
post_data = create_formset_post_data(response)
post_data['form-0-DELETE'] = True
# then POST, etc.
...
From the source, we can see that there is no need include MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT in our test data:
MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of the management form, but only for the convenience of client-side code. The POST value of them returned from the client is not checked.
This doesn't seem to be a formset at all. Formsets will always have some sort of prefix on every POSTed value, as well as the ManagementForm that Bartek mentions. It might have helped if you posted the code of the view you're trying to test, and the form/formset it uses.
My case may be an outlier, but some instances were actually missing a field set in the stock "contrib" admin form/template leading to the error
"ManagementForm data is missing or has been tampered with"
when saved.
The issue was with the unicode method (SomeModel: [Bad Unicode data]) which I found investigating the inlines that were missing.
The lesson learned is to not use the MS Character Map, I guess. My issue was with vulgar fractions (¼, ½, ¾), but I'd assume it could occur many different ways. For special characters, copying/pasting from the w3 utf-8 page fixed it.
postscript-utf-8