How does WagtailPageTests.assertCanCreate work with streamfield? - unit-testing

I'm trying to write unit tests for a page with a fairly complicated StreamField. I'm having issues with that so I've created a very pared down version to try to understand how WagtailPageTests works and build my way up.
My pared down model:
from wagtail.admin.edit_handlers import FieldPanel
from wagtail.core.fields import RichTextField
from wagtail.core.models import Page
class SEPage(Page):
banner_text = RichTextField(blank=True, features=["bold", "italic", "html"])
body = RichTextField(blank=True, features=["bold", "italic", "html"])
parent_page_types = ["LandingPage"]
content_panels = Page.content_panels + [
FieldPanel("banner_text"),
FieldPanel("body"),
]
The relevant bits of my test:
class SEPageTest(WagtailPageTests):
def test_can_create_se_page(self):
# Assert that a SEPage can be made here, with this POST data
form = nested_form_data({
'slug': 'test',
'title': 'title',
'banner_text': rich_text('About us'),
'body': rich_text('About us'),
})
print(f"form: {form}")
self.assertCanCreate(self.landing, SEPage, form)
That test passes as I would expect. However then I changed the body in the nested_form_data to
'body': streamfield([
('text', 'Lorem ipsum dolor sit amet'),
])
without changing the model and the test still passes.
I would expect that with the body in the model set up as a RichTextField and the test passing in a streamfield that the test would fail.
Can anyone explain why this test is passing?

In short: the streamfield helper will generate a bunch of data fields under names like body-count and body-0-value which will be ignored by the rich text field, because it's expecting a field simply named body. Since it doesn't find one, the resulting value for body is blank, which passes validation because the RichTextField is defined with blank=True.
The longer answer: The purpose of the form data helpers (nested_form_data, rich_text and streamfield) is to construct a dictionary of data in the same format that the page edit form in the Wagtail admin interface would submit as a POST request. For simple fields like title and slug, this works as you'd expect: a single item in the dictionary, mapping the field name to its value.
For more complex fields, the data format is an internal Wagtail detail that you wouldn't normally have to deal with (which is why Wagtail provides these helpers to do it for you). In the case of a rich text field, there's still a single entry in the dictionary under the field name (body here), but the value has to be sent in the Draftail editor's internal JSON format. In the case of a StreamField, though, the data consists of lots of individual form fields holding information about how many blocks there are, what type they are, what their values are, whether they've been deleted or reordered, and various other details. These come through in the form submission as a set of entries in the dictionary, all prefixed with body-.
So, when you have a rich text field on the page, the form is expecting to receive a submission like:
{
'slug': 'test',
'title': 'title',
'banner_text': '{"blocks": [{"key": "1", "text": "About us"}]}',
'body': '{"blocks": [{"key": "1", "text": "About us"}]}',
}
and indeed this is what you're providing when you use the rich_text helper. However, if you use the streamfield helper instead, it'll come out as something like:
{
'slug': 'test',
'title': 'title',
'banner_text': '{"blocks": [{"key": "1", "text": "About us"}]}',
'body-count': '1',
'body-0-type': 'text',
'body-0-value': 'Lorem ipsum dolor sit amet',
'body-0-deleted': '',
'body-0-order': '0',
}
What happens when you submit this incorrect dictionary of data? Wagtail will go through each field defined in SEPage in turn, picking out the entry or entries that are meaningful to that field. Since none of the fields defined in SEPage are looking for entries named body-count, body-0-type and so on, those will just get ignored. The body rich text field is expecting an entry simply named body, and doesn't find one, so the resulting value for that field is blank. However, the body field is defined with blank=True, and so a blank value is treated as valid. As a result, it's able to successfully create a page with an empty body field, and the test passes.

Related

Using django-import-export: How to customise which fields to export Excel files

I just starting out using Django. I'm using django-import-export package and I tried to customize my Django admin panel in this page in order to choose which fields to export to excel file.
Here is my admin model
class CompanyAdmin(ImportExportMixin, admin.ModelAdmin):
model = Company
resource_classes = [CompanyResource]
list_display = [
"name",
"uuid",
]
fields = [
"name",
"email",
]
def get_export_resource_kwargs(self, request, *args, **kwargs):
formats = self.get_export_formats()
form = CompanyExportForm(formats, request.POST or None)
form_fields = ("name", "email", )
return {"form_fields": form_fields}
Here is my model resource
class CompanyResource(resources.ModelResource):
class Meta:
model = Company
def __init__(self, form_fields=None):
super().__init__()
self.form_fields = form_fields
def get_export_fields(self):
return [self.fields[f] for f in self.form_fields]
Here is my export form
class CompanyExportForm(ExportForm):
# what should i write here? is this the place that i should write the custom code eg: checkboxes where user have the options to export the 'name` or 'email' fields ??
I try to use the same way as in this post in order to achieve the same result. However, i have been stuck forever.
Update:
For those who ended up here: Please take a look at this blog post : it is a different solution, although there are some improvement that can be done
If you want to define which fields should be exported, refer to this post. This means that only those fields will be exported, and the user cannot choose. This is relatively simple to achieve.
However it seems like you want the user to be able to choose the fields in the UI, then it is more complicated, and will involve more customisation. The answer you link to is the starting point.
There will have to be some UI element in which the user can choose the fields for export (e.g. some multi select widget). Then on POST, you'll have to read the ids of those fields and then feed that into the export() method.
If you are new to Django, it's going to take a bit of work to implement, and will be quite a steep learning curve. If you can find a "clean" way to implement it, such that future users would be able to implement, we would consider a PR.

Django Rest Framework JSON serializer field failing validation with required=False and no default?

How can I set required to be false for a django rest framework JSON serializer field? It seems to be enforcing validation regardless of the required flag:
serializer field
results = serializers.JSONField(required=False, label='Result')
model field
results = models.TextField(blank=True, default="")
But when I submit the form with a blank input, I get:
"results": [
"Value must be valid JSON."
],
I've also tried changing the model default to {} in both the model field and the serializer field, but have the same response.
UPDATE
Thanks to #Linovia for pointing out that "The required flag does mean that the serializer will not complain if that field isn't present"
After some digging, it looks like DRF is setting a default value of null on the input, which then is caught as invalid... How can I override this, as the serializer "default" attribute doesn't seem to have any effect.
"results": null,
The required flag does mean that the serializer will not complain if that field isn't present.
However, if it is present, it will follow the validation process. It doesn't mean at all that it will be discarded if it doesn't validate.
This appears to be an issue with DRF's empty class (used "to represent no data being provided for a given input or output value). The empty value for a JSONField is not json serializable, so you see
"results": [
"Value must be valid JSON."
],
To fix this, I overrode DRF's JSONField with my own NullableJSONField
from rest_framework.fields import empty
class NullableJSONField(serializers.JSONField):
def get_value(self, dictionary):
result = super().get_value(dictionary)
if result is empty:
return None
return result
and added allow_null=True to the field in the serializer
json_blob = NullableJSONField(required=False, allow_null=True)
The issue with this fix is that the json_blob field is then serialized with None in the response. (e.g. "json_blob": {})
I hope this resolves your issue, and I will post an update if I find a solution that complete removes this field from the response when it is not input.
For future reference, one way to successfully and quickly implement this is using the initial keyword on the serializer, which will save an empty dict to the model instance (ensure you have coupled this with a django JSONField in the model definition itself)
serializer:
results = serializers.JSONField(required=False, initial=dict)
model:
results = JSONField(default=dict)
The browsable api will render with {} as the initial value, which you may or may not choose to modify.
you can use syntax something like...
class DetailSerializer(serializers.ModelSerializer):
results = ResultSerializer(
allow_empty_file=True,required=False
)

Django way for dynamic forms with dependencies?

I'm looking for a django way to handle some complex forms with a lot of business logic. The issue is many of my forms have dependencies in them.
Some examples:
1. two "select" (choice) fields that are dependent on each other. For example consider two dropdowns one for Country and one for City.
2. A "required-if" rule, i.e set field required if something else in the form was selected. Say if the user select "Other" option in a select field, he need to add an explanation in a textarea.
3. Some way to handle date/datetime fields, i.e rules like max/min date?
What I'm doing now is implementing all of these in the form clean(), __init__(), and write some (tedious) client-side JS.
I wonder if there is a better approach? like defining these rules in a something similar to django Meta classes.
I'm going to necro this thread, because I don't see a good answer yet. If you are trying to validate a field and you want that field's validation to depend on another field in that same form, use the clean(self) method.
Here's an example: Say you have two fields, a "main_image" and "image_2". You want to make sure that if a user uploads a second image, that they also uploaded a main image as well. If they don't upload an image, the default image will be called 'default_ad.jpg'.
In forms.py:
class AdForm(forms.ModelForm):
class Meta:
model = Ad
fields = [
'title',
'main_image',
'image_2',
'item_or_model_names',
'category',
'buying_or_selling',
'condition',
'asking_price',
'location',
]
def clean(self):
# "Call the cleaned form"
cleaned_data = super().clean()
main_image = cleaned_data.get("main_image")
image_2 = cleaned_data.get("image_2")
if "default_ad" not in image_2:
# Check to see if image_2's name contains "default_ad"
if "default_ad" in main_image:
raise forms.ValidationError(
"Oops, you didn't upload a main image."
)
If you want more info, read: https://docs.djangoproject.com/en/2.2/ref/forms/validation/#cleaning-and-validating-fields-that-depend-on-each-other
Good luck!
1.This task is souly related the the html building of the form, not involving django/jinga.
2.Here, you go to dynamic forms. the best and most used way nowdays to do this, is through JS.
3.try building a DB with a "time" type field and then through "admin" watch how they handle it. all of special fields useage is covered here: https://docs.djangoproject.com/en/1.9/ref/forms/fields/

How to render django form differently based on what user selects?

I have a model and a form like this:
class MyModel(models.Model):
param = models.CharField()
param1 = models.CharField()
param2 = models.CharField()
class MyForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('param', 'param1', 'param2')
Then I have one drop down menu with different values and based on what value is selected I'm hiding and showing fields of MyForm. Now I have to take one step further and render param2 as a CheckboxInput widget if user selects a certain value from a drop down but in other cases it should be standard text field. So how would I do that?
I know this post is almost a year old, but it took me multiple hours to even find a post related to this topic (this is the only one I found, which came up as related when submitting my own question), so I felt the need to share my solution.
I wanted to have a form that would show and require a text field if an option from a dropdown menu matched a value stored in another model. I had a foreignKey relation between two models and I passed an instance of Model1 into the ModelForm for Model2. If a value chosen for a variable in Model2 matched a variable already set in Model1, I wanted to show and require a textfield. It was basically a "choose Other and then enter your own description" scenario.
I did not want the page to reload (I was trying to have this work in both mobile and desktop browsers with the least delay/reloads and using the same code for both), so I could not use the mentioned multiple forms loading in a view option. I started trying to do it with AJAX as suggested above when I realized I was over thinking the problem.
The answer was using JS and clean methods in the form. I added a non-required field (field1) that was not in Model2 to my Model2Form. I then hid this using jQuery and only displayed it (using jQuery) if the value of another field (field2) matched the value of the variable from Model1. To make that work, I did decide to have a hidden < span > in my template with the pk of the variable so I could easily grab it with jQuery. This jQuery worked perfectly for hiding and showing the field correctly so the user could choose the "other" value and then decided to choose a different one instead (and go back and forth endlessly).
I then used a clean method in my Model2Form for field1 that raised a ValidationError if no value was entered when the value in field2 matched my Model1 variable. I accessed that variable by using "self.other = Model1.variable" in my __ init __ method and then referencing that in the clean_field1 method.
I would have liked to have been able to accomplish this without having to hide and show a field with JS, but I think the only solutions for doing so with views or ajax caused delays/reloads that I did not want. Also, I liked the general simplicity of the method I used, rather than having to figure out how to pass partial forms back and forth through the HTTPRequest.
Update:
In my situation, I was creating entries for lost and found items and if the location where the item was found was not a provided option, then I wanted to show a textbox for the user to enter the location. I created a location object that was set as the "other" location and then displayed the textbox when that object was selected as the "found" location.
In forms.py, I added an extra CharField and use a clean method to check if the field is required and then throw a ValidationError if it wasn't filled in:
class Model2Form(forms.ModelForm):
def __init__(self, Model1, *args, **kwargs):
self.other = Model1.otherLocation
super(Model2Form, self).__init__(*args, **kwargs)
...
otherLocation = forms.CharField(
label="Location Description",
max_length=255,
required=False
)
def clean_otherLocation(self):
if self.cleaned_data['locationFound'] == self.other and not self.cleaned_data['otherLocation']:
raise ValidationError("Must describe the location.")
return self.cleaned_data['otherLocation']
Then in my JavaScript, I checked if the value of the "found" location was the "other" location (the value of which I had in a hidden span on my html page). I then used .show() and .hide() on the textbox's parent element as necessary:
$("#id_locationFound").change( function(){
if ($("#id_locationFound").val() == $("#otherLocation").attr("value")){ //if matches "other" location, display textbox; otherwise, hide textbox
$("#id_otherLocation").parent().show();
}else
$("#id_otherLocation").parent().hide();
});
Your best guess would be to trigger a "POST" request when you select something from your drop down menu.
The Value of that "POST" has to correspond your values you use to determine which field you would like to output.
Now you will actually need two forms:
class MyBaseForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ('param', 'param1', 'param2')
class MyDropDownForm(MyBaseForm):
class Meta:
widgets = {
'param2': Select(attrs={...}),
}
So as you can see the DropDownForm has been derived from MyBaseForm to make sure it will have all the same properties. But we have modified the widget of one of the fields.
Now you can update your view. Please note, this is untested Python + Pseudocode
views.py
def myFormView(request):
if request.method == 'POST': # If the form has been submitted...
form = MyBaseForm(request.POST)
#submit button has not been pressed, so the dropdown has triggered the submission.
#Hence we won't safe the form, but reload it
if 'my_real_submitbotton' not in form.data:
if 'param1' == "Dropdown":
form = MyDropDownForm(request.POST)
else:
#do your normal form saving procedure
else:
form = ContactForm() # An unbound form
return render(request, 'yourTemplate.html', {
'form': form,
})
This mechanism does the following:
When the form is submitted it checks if you have pressed the "submit" button or have used a dropdown onChange to trigger a submission. My solution doesn't contain the javascript code you need to trigger the submission with an onChange. I just like to provide a way to solve it.
To use the 'my_real_submitbutton' in form.data construct you will be required to name your submit button:
<input type="submit" name="my_real_submitbutton" value="Submit" />
Of course you can choose any string as Name. :-)
In case of a submit by your dropdown field you must check which value has been selected in this drop down menu. If this value satisfies the condition you want to return a Dropdown Menu you create an instance of DropDownForm(request.POST) otherwise you can leave everything as it is and rerender your template.
On the downside this will refresh your page.
On the upside it will keep all the already entered field values. So no harm done here.
If you would like to avoid the page refresh you can keep my proposed idea but you need to render the new form via AJAX.

How do I use error_messages on models in Django

I understand form the documentation http://docs.djangoproject.com/en/dev/ref/models/fields/ that you can add error_messages to a model field and supply your own dict of error messages. However, what are they keys of the dict you are supposed to pass?
class MyModel(models.Model):
some_field = models.CharField(max_length=55, error_messages={'required': "My custom error"})
If it is easier to do this on the modelform that is used that would also work, however. I would rather not have to create explicitly creating each field and their type again. This is what I was trying to avoid:
class MyModelForm(forms.ModelForm):
some_field = forms.CharField(error_messages={'required' : 'Required error'})
Update 2: Test code used in my project
My Model:
class MyTestModel(models.Model):
name = models.CharField(max_length=127,error_messages={'blank' : 'BLANK','required' : 'REQUIRED'})
My Form:
class EditTestModel(ModelForm):
class Meta:
model = MyTestModel
My View:
tf = EditTestModel({'name' : ''})
print tf.is_valid() # prints False
print tf.full_clean() # prints None
print tf # prints the form, with a <li> error list containg the error "This field is required"
<tr><th><label for="id_name">Name:</label></th><td><ul class="errorlist"><li>This field is required.</li></ul><input id="id_name" type="text" name="name" maxlength="127" /></td></tr>
You're right, those docs are not very useful. It's a recent addition after all!
My guess is that the normal usage of error_messages is for ModelForms, so I'd look here for a list of acceptable error keys per field: http://docs.djangoproject.com/en/dev/ref/forms/fields/#error-messages
But, if you want to be really safe and not assume anything...
The most reliable way for now is going to be looking at the source at django/db/models/fields/__init__.py where you'll see each of the default_error_messages that can be specified and the actual calls to self.error_messages['invalid']
# Field (base class)
default_error_messages = {
'invalid_choice': _(u'Value %r is not a valid choice.'),
'null': _(u'This field cannot be null.'),
'blank': _(u'This field cannot be blank.'),
}
# AutoField
default_error_messages = {
'invalid': _(u'This value must be an integer.'),
}
Here's the doc on model validation:
http://docs.djangoproject.com/en/dev/ref/models/instances/#validating-objects
Update:
Just tested this in a shell session and it appears to be working. Whats up?
I just defined a simple model:
class SubscriptionGroup(models.Model):
name = models.CharField(max_length=255, error_messages={'blank': 'INVALID!!11', 'null': 'NULL11!'})
# shell
>>> s = SubscriptionGroup()
>>> s.full_clean()
ValidationError: {'name': [u'INVALID!!11']}
(Bit late to this one, but I have been through the same issues myself, so using this as a note_to_self as much as anything.)
You can specify error_messages on both models and modelforms. The keys you should / can use are defined here for the form fields. The issue seems to be (to me) the inter-relationship between forms and the related model, and which error message appears, and when. The key to this is understanding that forms and models are actually very loosely-coupled, and that there really is no magic happening.
If you have a model field called 'quote', with a max_length of 140, and a modelform associated with this model, the error messages will work thus:
If you don't explicitly add a max_length attribute to the modelform, and then validate the form (calling is_valid() or errors), the error message that comes back will be from the model.
If you add a set of error_messages ('required','max_length') to the model, these will appear in the errors collection.
If you add a set of error_messages to the modelform, they will not appear, as it is the model that is failing validation, not the form.
If you then add a max_length attribute to the modelform, you will see the modelform errors surfacing (and overriding the model error_messages.)
So - fairly simple in summary - the error messages map to the object that is being validated - which can be either the model or the modelform.
I tried. It will not work if you defined it in models.
You must define error_messages in your forms like this
name = forms.CharField(error_messages={'required': 'this field is required'})