Knowledge does hurt. Today I had my sweet cup of WTF. We are developing a medical database based on Plone 3.1. It uses very advanced AJAX framework called KSS – basically you can avoid the pain of writing pure Javascript by crafting special CSS like stylesheets which bind server-side Python code to any Javascript event. This makes AJAX programming a joy. You can easily combine server-side logic with user interface events, like field validation.
Well… then there was an error. KSS validation was not working for the text fields on a certain pages…. or it did sometimes. We were not sure. This is so called Heisenbug. I armed myself with sleepy eyes, Firebug and a lot of energy drinks.
I saw a KSS error in the Firebug log window and failed HTTP POST in the server logs.
Invalid request.
The parameter, value, was omitted from the request.
Looks like the field value was not properly posted for the field validation.
The first thing was locate the error and get function traceback for the messy situation. Unfortunately Firefox Javascript engine or Firebug cannot show tracebacks properly… the grass is so much greener on the other side of the fence. So I had to manually search through the codebase by manually plotting console.log() calls here and there.
Finally I thought I pinpointed the cause of the failure. By shaking finger (excitement, tireness and all that extra caffeinen from energy drinks), I opened the Javascript file just to realize why Javascript is utterly utterly shitty and why no sane person wants to do low level Javascript development. If ECMA standard committee had been clever and had been able to enforce anything long time ago, the following piece could be replaced with one function call.
fo.getValueOfFormElement = function(element) { // Returns the value of the form element / or null // First: update the field in case an editor is lurking // in the background this.fieldUpdateRegistry.doUpdate(element); if (element.disabled) { return null; } // Collect the data if (element.selectedIndex != undefined) { // handle single selects first if(!element.multiple) { if (element.selectedIndex < 0) { value=""; } else { var option = element.options[element.selectedIndex]; // on FF and safari, option.value has the value // on IE, option.text needs to be used value = option.value || option.text; } // Now process selects with the multiple option set } else { var value = []; for(i=0; i<element.options.length; i++) { var option = element.options[i]; if(option.selected) { // on FF and safari, option.value has the value // on IE, option.text needs to be used value.push(option.value || option.text); } } } // Safari 3.0.3 no longer has "item", instead it works // with direct array access []. Although other browsers // seem to support this as well, we provide checking // in both ways. (No idea if item is still needed.) } else if (typeof element.length != 'undefined' && ((typeof element[0] != 'undefined' && element[0].type == "radio") || (typeof element.item(0) != 'undefined' && element.item(0).type == "radio"))) { // element really contains a list of input nodes, // in this case. var radioList = element; value = null; for (var i=0; i < radioList.length; i++) { var radio = radioList[i] || radioList.item(i); if (radio.checked) { value = radio.value; } } } else if (element.type == "radio" || element.type == "checkbox") { if (element.checked) { value = element.value; } else { value = null; } } else if ((element.tagName.toLowerCase() == 'textarea') || (element.tagName.toLowerCase() == 'input' && element.type != 'submit' && element.type != 'reset') ) { value = element.value; } else { value = null; } return value; };
It turned out that the element in this case was an empty list of radio buttons. When you are tab keying through a radio button group without any value selected, like in the case a content object is just created, KSS validation is triggered even though there is no value in any of the radio buttons. This makes KSS think the value is null and it does not properly handle the situation. This does not cause any user visible effects unless you have Javascript debugging on (Firebug + debugging mode in Plone’s Javascript registry).
But this was not the bug I was looking for. It was just masking the original bug, because I had an empty radio button group next to the text field whose validation was not correctly done. More server side debugging…
I inserted some funky debug prints to Archetypes.Field.validate_validators():
validate_validators()
Calling validators:(('isEmptyNoError', V_SUFFICIENT), ('validDecRange03', V_REQUIRED))
We can see that not triggered validator, validDecRange03, is still with us. Then I add more debug prints to see where things go wrong, this time to to Products.validation.chain.__call__.
Calling validators:(('isEmptyNoError', V_SUFFICIENT), ('validDecRange03', V_REQUIRED)) Name:isEmptyNoError value:234234234234234234234234234232342342342344534232342344234534554 result:True
Ok – we have a case here. isEmptyNoError validator is executed before our custom validator. Since this validator is flagged as “sufficient” other validators are not evaluated. I think this has not been the case before and our validator have worked properly… maybe there was API change in Plone 3.1 which broke the things?
After digging and digging and digging I found this 4 years old bug. Let’s open the famous isEmptyNoError source code in Products.validation.validators.EmptyValidator:
class EmptyValidator: __implements__ = IValidator def __init__(self, name, title='', description='', showError=True): self.name = name self.title = title or name self.description = description self.showError = showError def __call__(self, value, *args, **kwargs): isEmpty = kwargs.get('isEmpty', False) instance = kwargs.get('instance', None) field = kwargs.get('field', None) # XXX: This is a temporary fix. Need to be fixed right for AT 2.0 # content_edit / BaseObject.processForm() calls # widget.process_form a second time! if instance and field: widget = field.widget request = getattr(instance, 'REQUEST', None) if request and request.form: form = request.form result = widget.process_form(instance, field, form, empty_marker=_marker, emptyReturnsMarker=True) if result is _marker or result is None: isEmpty = True if isEmpty: return True elif value == '' or value is None: return True else: if getattr(self, 'showError', False): return ("Validation failed(%(name)s): '%(value)s' is not empty." % { 'name' : self.name, 'value': value}) else: return False
There is my WTF. Or XXX – thanks for the kisses. My guess is that because KSS validation is executed in special context, the magical REQUEST might not be there. The “is sufficient” validator fails because of the some sort of god forgotten magic and thus all custom validators fail in KSS when the field is not required.
The workaround: I add my own greetings to the code:
# XXX: This is a temporary fix. Need to be fixed right for AT 2.0 # content_edit / BaseObject.processForm() calls # widget.process_form a second time! if instance and field: widget = field.widget request = getattr(instance, 'REQUEST', None) # XXX: Whatever this all does, it does not work for KSS validation # requests and we should ignore this if "fieldname" in request: return False if request and request.form: form = request.form result = widget.process_form(instance, field, form, empty_marker=_marker, emptyReturnsMarker=True) if result is _marker or result is None: isEmpty = True
If Zope 3 drives you smoking Plone 3 drives me drinking. No wonder newbies steer away from Plone – if you hadn’t been armed with 8 years of web development experience you would never have figured out what’s going on with such a simple thing as adding a custom validator. A comment added to the bug tracker.
Many thanks for tracking this down!
We are aware that the cooperation of Archetypes and KSS has several problems – the main reason for this is that AT was not originally designed for the KSS / Ajax uses cases. We are applying a bunch of hacks and workarounds to make it work (like, in the case of field validation) but we always keep on encountering edge cases like the current one, that make it fail.
Obviously, the number of these problems will be greatly reduced once AT is replaced by an alternative, but till then, the workarounds will stay.
If this issue is solved it would be great to
create a Selenium test for it to assure that it
stays fixed – for more info please visit:
http://kssproject.org/docs/tutorial/kss-testing-how-to-for-plone-3.1
Best wishes,
“if you hadn’t been armed with 8 years of web development experience…”
… you would’ve reported a bug or asked for help.
Don’t pretend like computer systems don’t have bugs, and don’t pretend like bugs aren’t sometimes hard to find, regardless of which platform you happen to use.
Martin
this for validation i want dissallowed to string or characters and i only allowed numbers can you sallow this problem
package com.stl.kps.web.validator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
import com.stl.kps.web.data.ManageFieldForm;
public class ManageFieldValidator implements Validator {
protected final Log logger = LogFactory.getLog(getClass());
@SuppressWarnings(“unchecked”)
public boolean supports(Class clazz) {
return clazz.equals(ManageFieldForm.class);
}
public void validate(Object obj, Errors errors)
{
ManageFieldForm pc = (ManageFieldForm) obj;
if (pc ==null)
{
errors.rejectValue(“number”, “error.not-specified”, null, “Value required.”);
}
else
{
logger.info(“Validating with ” + pc);
if (pc.getNumber() == null || pc.getNumber().length() =5)
{
errors.rejectValue(“number”, “error.number”, “value is maximum 5 characters”);
}
}
}
}
In the Archetype Field.py have noticed the comment like this:
# insert isEmpty validator at position 0 if first validator
# is not isEmpty
Then the idea just came out in my mind. I changed the validators in schema like this:
validators=((‘isEmpty’, V_REQUIRED),(‘yourCustomValidator’,V_SUFFICIENT),),
and now it calls the custom validator
I have tried this on save of form. haven’t tried on kss validator. But it should work.
Same problem in both case
Tareq