add custom DynamicArrayField to better handle arrays
This commit is contained in:
parent
56d872af15
commit
6dcdf7bcce
|
@ -3,6 +3,11 @@
|
||||||
{% load i18n %}
|
{% load i18n %}
|
||||||
{% load utils %}
|
{% load utils %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ block.super }}
|
||||||
|
{{ form.media.css }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{% block above_form %}
|
{% block above_form %}
|
||||||
|
@ -16,3 +21,8 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{{ block.super }}
|
||||||
|
{{ form.media.js }}
|
||||||
|
{% endblock %}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
from passbook.core.models import DummyFactor, PasswordFactor
|
from passbook.core.models import DummyFactor, PasswordFactor
|
||||||
|
from passbook.lib.fields import DynamicArrayField
|
||||||
|
|
||||||
GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled']
|
GENERAL_FIELDS = ['name', 'slug', 'order', 'policies', 'enabled']
|
||||||
|
|
||||||
|
@ -16,6 +17,9 @@ class PasswordFactorForm(forms.ModelForm):
|
||||||
'name': forms.TextInput(),
|
'name': forms.TextInput(),
|
||||||
'order': forms.NumberInput(),
|
'order': forms.NumberInput(),
|
||||||
}
|
}
|
||||||
|
field_classes = {
|
||||||
|
'backends': DynamicArrayField
|
||||||
|
}
|
||||||
|
|
||||||
class DummyFactorForm(forms.ModelForm):
|
class DummyFactorForm(forms.ModelForm):
|
||||||
"""Form to create/edit Dummy Factor"""
|
"""Form to create/edit Dummy Factor"""
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
.dynamic-array-widget .array-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-array-widget .remove_sign {
|
||||||
|
width: 10px;
|
||||||
|
height: 2px;
|
||||||
|
background: #a41515;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-array-widget .remove {
|
||||||
|
height: 15px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dynamic-array-widget .remove:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
|
@ -16,3 +16,33 @@ const typeHandler = function (e) {
|
||||||
|
|
||||||
$source.on('input', typeHandler) // register for oninput
|
$source.on('input', typeHandler) // register for oninput
|
||||||
$source.on('propertychange', typeHandler) // for IE8
|
$source.on('propertychange', typeHandler) // for IE8
|
||||||
|
|
||||||
|
window.addEventListener('load', function () {
|
||||||
|
|
||||||
|
function addRemoveEventListener(widgetElement) {
|
||||||
|
widgetElement.querySelectorAll('.array-remove').forEach(function (element) {
|
||||||
|
element.addEventListener('click', function () {
|
||||||
|
this.parentNode.parentNode.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.dynamic-array-widget').forEach(function (widgetElement) {
|
||||||
|
|
||||||
|
addRemoveEventListener(widgetElement);
|
||||||
|
|
||||||
|
widgetElement.querySelector('.add-array-item').addEventListener('click', function () {
|
||||||
|
var first = widgetElement.querySelector('.array-item');
|
||||||
|
var newElement = first.cloneNode(true);
|
||||||
|
var id_parts = newElement.querySelector('input').getAttribute('id').split('_');
|
||||||
|
var id = id_parts.slice(0, -1).join('_') + '_' + String(parseInt(id_parts.slice(-1)[0]) + 1);
|
||||||
|
newElement.querySelector('input').setAttribute('id', id);
|
||||||
|
newElement.querySelector('input').value = '';
|
||||||
|
|
||||||
|
addRemoveEventListener(newElement);
|
||||||
|
first.parentElement.insertBefore(newElement, first.parentNode.lastChild);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
<link rel="shortcut icon" type="image/png" href="{% static 'img/logo.png' %}">
|
<link rel="shortcut icon" type="image/png" href="{% static 'img/logo.png' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly.min.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly.min.css' %}">
|
||||||
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly-additions.min.css' %}">
|
<link rel="stylesheet" type="text/css" href="{% static 'css/patternfly-additions.min.css' %}">
|
||||||
|
<link rel="stylesheet" type="text/css" href="{% static 'css/passbook.css' %}">
|
||||||
<style>
|
<style>
|
||||||
.login-pf {
|
.login-pf {
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
"""passbook lib fields"""
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from django import forms
|
||||||
|
from django.contrib.postgres.utils import prefix_validation_error
|
||||||
|
|
||||||
|
from passbook.lib.widgets import DynamicArrayWidget
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicArrayField(forms.Field):
|
||||||
|
"""Show array field as a dynamic amount of textboxes"""
|
||||||
|
|
||||||
|
default_error_messages = {"item_invalid": "Item %(nth)s in the array did not validate: "}
|
||||||
|
|
||||||
|
def __init__(self, base_field, **kwargs):
|
||||||
|
self.base_field = base_field
|
||||||
|
self.max_length = kwargs.pop("max_length", None)
|
||||||
|
kwargs.setdefault("widget", DynamicArrayWidget)
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
cleaned_data = []
|
||||||
|
errors = []
|
||||||
|
value = [x for x in value if x]
|
||||||
|
for index, item in enumerate(value):
|
||||||
|
try:
|
||||||
|
cleaned_data.append(self.base_field.clean(item))
|
||||||
|
except forms.ValidationError as error:
|
||||||
|
errors.append(
|
||||||
|
prefix_validation_error(
|
||||||
|
error, self.error_messages["item_invalid"],
|
||||||
|
code="item_invalid", params={"nth": index}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if errors:
|
||||||
|
raise forms.ValidationError(list(chain.from_iterable(errors)))
|
||||||
|
if not cleaned_data and self.required:
|
||||||
|
raise forms.ValidationError(self.error_messages["required"])
|
||||||
|
return cleaned_data
|
||||||
|
|
||||||
|
def has_changed(self, initial, data):
|
||||||
|
if not data and not initial:
|
||||||
|
return False
|
||||||
|
return super().has_changed(initial, data)
|
|
@ -0,0 +1,17 @@
|
||||||
|
{% load utils %}
|
||||||
|
|
||||||
|
{% spaceless %}
|
||||||
|
<div class="dynamic-array-widget">
|
||||||
|
{% for widget in widget.subwidgets %}
|
||||||
|
<div class="array-item input-group">
|
||||||
|
{% include widget.template_name %}
|
||||||
|
<div class="input-group-btn">
|
||||||
|
<button class="array-remove btn btn-danger" type="button">
|
||||||
|
<span class="pficon-delete"></span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div><button type="button" class="add-array-item btn btn-default">Add another</button></div>
|
||||||
|
</div>
|
||||||
|
{% endspaceless %}
|
|
@ -0,0 +1,36 @@
|
||||||
|
"""Dynamic array widget"""
|
||||||
|
from django import forms
|
||||||
|
|
||||||
|
|
||||||
|
class DynamicArrayWidget(forms.TextInput):
|
||||||
|
"""Dynamic array widget"""
|
||||||
|
|
||||||
|
template_name = "lib/arrayfield.html"
|
||||||
|
|
||||||
|
def get_context(self, name, value, attrs):
|
||||||
|
value = value or [""]
|
||||||
|
context = super().get_context(name, value, attrs)
|
||||||
|
final_attrs = context["widget"]["attrs"]
|
||||||
|
id_ = context["widget"]["attrs"].get("id")
|
||||||
|
|
||||||
|
subwidgets = []
|
||||||
|
for index, item in enumerate(context["widget"]["value"]):
|
||||||
|
widget_attrs = final_attrs.copy()
|
||||||
|
if id_:
|
||||||
|
widget_attrs["id"] = "{id_}_{index}".format(id_=id_, index=index)
|
||||||
|
widget = forms.TextInput()
|
||||||
|
widget.is_required = self.is_required
|
||||||
|
subwidgets.append(widget.get_context(name, item, widget_attrs)["widget"])
|
||||||
|
|
||||||
|
context["widget"]["subwidgets"] = subwidgets
|
||||||
|
return context
|
||||||
|
|
||||||
|
def value_from_datadict(self, data, files, name):
|
||||||
|
try:
|
||||||
|
getter = data.getlist
|
||||||
|
return [value for value in getter(name) if value]
|
||||||
|
except AttributeError:
|
||||||
|
return data.get(name)
|
||||||
|
|
||||||
|
def format_value(self, value):
|
||||||
|
return value or []
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
|
|
||||||
|
from passbook.lib.fields import DynamicArrayField
|
||||||
from passbook.saml_idp.models import (SAMLPropertyMapping, SAMLProvider,
|
from passbook.saml_idp.models import (SAMLPropertyMapping, SAMLProvider,
|
||||||
get_provider_choices)
|
get_provider_choices)
|
||||||
from passbook.saml_idp.utils import CertificateBuilder
|
from passbook.saml_idp.utils import CertificateBuilder
|
||||||
|
@ -46,3 +47,6 @@ class SAMLPropertyMappingForm(forms.ModelForm):
|
||||||
'saml_name': forms.TextInput(),
|
'saml_name': forms.TextInput(),
|
||||||
'friendly_name': forms.TextInput(),
|
'friendly_name': forms.TextInput(),
|
||||||
}
|
}
|
||||||
|
field_classes = {
|
||||||
|
'values': DynamicArrayField
|
||||||
|
}
|
||||||
|
|
Reference in New Issue