For me, a complete solution required several aspects:
- The ability for the user to arbitrarily add extra fields
- A limit of the number of fields if specified by the script
- A way to clone past jobs and auto-populate multiple selections
Adding extra fields and limiting the number of extra fields
To add extra attributes to a widget that are rendered in HTML is quite easy. You simply add whatever you wish to show up in the widget's attr dictionary as such:
field.widget.attrs.update({'data-attribute-here': 'data-value-here'})
This allows you to then do any work on the front-end with javascript, such as putting in checks for the maximum number of extra fields with code such as:
For adding extra fields, this isn't exactly what I want though. This approach will put the attribute on every single widget. To make this clear, this is the final implementation:
Now we can look for the data-wooey-multiple attribute, which corresponds to a div containing all inputs for a given field. This lets us easily count how many copies of the field the user has created to help enforce limit checks, as well as makes it so a single selector can be constructed to handle everything.
field.widget.attrs.update({'data-wooey-choice-limit': choice_limit})
For adding extra fields, this isn't exactly what I want though. This approach will put the attribute on every single widget. To make this clear, this is the final implementation:
You can see there are plus signs for two parameters that support multiple file inputs. If we put that extra attribute on the widget, we would create a + sign for every single widget of the parameter instead of a single button that is "add a new input for this field". To avoid this, we'd have to do a bunch of checks in javascript to see which widget is the last one, etc. That is too complicated to be correct.
To get around this, I chose to wrap the input field with a div. This is accomplished by patching the widget's render method to:
- Get the rendered content
- Wrap that content with an element
This is what it looks like:
WOOEY_MULTI_WIDGET_ATTR = 'data-wooey-multiple' WOOEY_MULTI_WIDGET_ANCHOR = 'wooey-multi-input' def mutli_render(render_func, appender_data_dict=None): def render(name, value, attrs=None): if not isinstance(values, (list, tuple)): values = [values] # The tag is a marker for our javascript to reshuffle the elements. This is because some widgets have complex rendering with multiple fields return mark_safe('<{tag} {multi_attr}>{widget}</{tag}>'.format(tag='div', multi_attr=WOOEY_MULTI_WIDGET_ATTR, widget=render_func(name, value, attrs))) return render # this is a dict we can put any attributes on the wrapping class with appender_data_dict = {} field.widget.render = mutli_render(field.widget.render, appender_data_dict=appender_data_dict)
Auto-populate multiple initial values
This aspect of this implementation was accomplished by extending my previous approach of overriding the render method. First, I made it so all my initial values would be passed as a list. Given this, it was simple to expand my above render method, into this:def mutli_render(render_func, appender_data_dict=None): def render(name, values, attrs=None): if not isinstance(values, (list, tuple)): values = [values] # The tag is a marker for our javascript to reshuffle the elements. This is because some widgets have complex rendering with multiple fields pieces = ['<{tag} {multi_attr}>{widget}</{tag}>'.format(tag='div', multi_attr=WOOEY_MULTI_WIDGET_ATTR, widget=render_func(name, value, attrs)) for value in values] # we add a final piece that is our button to click for adding. It's useful to have it here instead of the template so we don't # have to reverse-engineer who goes with what # build the attribute dict data_attrs = flatatt(appender_data_dict if appender_data_dict is not None else {}) pieces.append(format_html('', anchor=WOOEY_MULTI_WIDGET_ANCHOR ,data=data_attrs)) return mark_safe('\n'.join(pieces)) return render
Unfortunately, it wasn't that simple. By default, a field's clean method takes the first element of a list as its output, which clearly will fail here since we can have multiple items. Additionally, a widget's value_from_datadict method will usually return the first element in the list as well. To avoid subclassing every single widget to correct this behavior, I once again took my monkey-patch/decorator hybrid method to override the clean and value_from_datadict methods:
def multi_value_from_datadict(func): def value_from_datadict(data, files, name): return [func(QueryDict('{name}={value}'.format(name=name, value=i)), files, name) for i in data.getlist(name)] return value_from_datadict def multi_value_clean(func): def clean(*args, **kwargs): args = list(args) values = args[0] ret = [] for value in values: value_args = args value_args[0] = value ret.append(func(*value_args, **kwargs)) return ret return clean field.widget.value_from_datadict = multi_value_from_datadict(field.widget.value_from_datadict) field.clean = multi_value_clean(field.clean)
Summary
The above approach allows the user to add widgets to a given field at will, with the option to limit how many can be added. It also has the very useful attributes of:
- It will work on most field widgets (if they for some reason changed the render method's expected arguments, it may fail).
- It utilizes existing clean methods to perform validations.
- It will generate correct errors for each field without any extra work.
I'm not sure if my method of overriding the function is best called a monkey-patch or a decorator, it seems a bit of both to me. If anyone knows the correct term, let me know.
No comments:
Post a Comment