FormDataFilter: new implementation for assignments

First prototype
This commit is contained in:
Thomas Gelf 2016-10-13 09:44:32 +00:00
parent a735df89a0
commit 9349ba0f2a
6 changed files with 732 additions and 53 deletions

View File

@ -7,7 +7,6 @@ use Icinga\Module\Director\Web\Form\QuickSubForm;
class AssignListSubForm extends QuickSubForm
{
protected $object;
public function setObject($object)
@ -16,20 +15,15 @@ class AssignListSubForm extends QuickSubForm
return $this;
}
public function setValue($value)
{
var_dump($value);
}
public function setup()
{
$idx = -1;
if ($this->object && $this->object->supportsAssignments()) {
// $this->setElementValue('assignlist', $object->assignments()->getFormValues());
foreach ($this->object->assignments()->getFormValues() as $values) {
foreach ($this->object->assignments()->getValues() as $values) {
$idx++;
$sub = new AssignmentSubForm();
$sub = $this->loadForm('assignmentSub');
$sub->setObject($this->object);
$sub->setup();
$sub->populate($values);
@ -38,7 +32,7 @@ class AssignListSubForm extends QuickSubForm
}
$idx++;
$sub = new AssignmentSubForm();
$sub = $this->loadForm('assignmentSub');
$sub->setObject($this->object);
$sub->setup();
$this->addSubForm($sub, $idx);
@ -48,6 +42,7 @@ class AssignListSubForm extends QuickSubForm
'ignore' => true,
));
$this->getElement('addmore')->setDecorators(array('ViewHelper'));
}
public function loadDefaultDecorators()
@ -56,7 +51,7 @@ class AssignListSubForm extends QuickSubForm
'FormElements',
array('HtmlTag', array(
'tag' => 'ul',
'class' => 'assign-rule'
'class' => 'assign-rule required'
)),
array('Fieldset', array(
'legend' => 'Assignment rules',

View File

@ -24,44 +24,13 @@ class AssignmentSubForm extends QuickSubForm
'class' => 'assign-type',
'value' => 'assign'
));
$this->addElement('select', 'property', array(
'label' => $this->translate('Property'),
'class' => 'assign-property autosubmit',
'multiOptions' => $this->optionalEnum(IcingaHost::enumProperties($this->object->getConnection(), 'host.'))
));
$this->addElement('select', 'operator', array(
'label' => $this->translate('Operator'),
'multiOptions' => array(
'=' => '=',
'!=' => '!=',
'>' => '>',
'>=' => '>=',
'<=' => '<=',
'<' => '<',
),
'required' => $this->valueIsEmpty($this->getValue('property')),
'value' => '=',
'class' => 'assign-operator',
$this->addElement('dataFilter', 'filter_string', array(
'columns' => IcingaHost::enumProperties($this->db)
));
$this->addElement('text', 'expression', array(
'label' => $this->translate('Expression'),
'placeholder' => $this->translate('Expression'),
'class' => 'assign-expression',
'required' => !$this->valueIsEmpty($this->getValue('property'))
));
/*
$this->addElement('submit', 'remove', array(
'label' => '-',
'ignore' => true
));
$this->addElement('submit', 'add', array(
'label' => '+',
'ignore' => true
));
*/
foreach ($this->getElements() as $el) {
$el->setDecorators(array('ViewHelper'));
$el->setDecorators(array('ViewHelper', 'Errors'));
}
}

View File

@ -161,12 +161,12 @@ class IcingaServiceForm extends DirectorObjectForm
return $this;
}
$sub = new AssignListSubForm();
$sub = $this->loadForm('assignListSub');
$sub->setObject($this->getObject());
$sub->setup();
$sub->setOrder(30);
$this->addSubForm($sub, 'assignlist');
$this->addSubForm($sub, 'assignments');
return $this;
}

View File

@ -0,0 +1,479 @@
<?php
use Icinga\Data\Filter\Filter;
use Icinga\Data\Filter\FilterChain;
use Icinga\Data\Filter\FilterExpression;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Director\Objects\IcingaObject;
use Icinga\Module\Director\Objects\IcingaObjectGroup;
use Icinga\Module\Director\Web\Form\IconHelper;
/**
* View helper for extensible sets
*
* Avoid complaints about class names:
* @codingStandardsIgnoreStart
*/
class Zend_View_Helper_FormDataFilter extends Zend_View_Helper_FormElement
{
private $currentId;
private $fieldName;
private $addTo;
private $cachedColumnSelect;
private $query;
/**
* Generates an 'extensible set' element.
*
* @codingStandardsIgnoreEnd
*
* @param string|array $name If a string, the element name. If an
* array, all other parameters are ignored, and the array elements
* are used in place of added parameters.
*
* @param mixed $value The element value.
*
* @param array $attribs Attributes for the element tag.
*
* @return string The element XHTML.
*/
public function formDataFilter($name, $value = null, $attribs = null)
{
$info = $this->_getInfo($name, $value, $attribs);
extract($info); // id, name, value, attribs, options, listsep, disable
if (array_key_exists('columns', $attribs)) {
$this->setColumns($attribs['columns']);
unset($attribs['columns']);
}
if (array_key_exists('addTo', $attribs)) {
$this->addTo = $attribs['addTo'];
unset($attribs['addTo']);
}
// TODO: check for columns in attribs, preserve & remove them from the
// array use attribs? class etc? disabled?
// override _getInfo?
$this->fieldName = $name;
// $this->fieldName = $id;
if ($value === null) {
$value = Filter::matchAll();
} elseif (is_string($value)) {
$value = Filter::fromQueryString($value);
}
return $this->renderFilter($value);
}
protected function renderFilter(Filter $filter, $level = 0)
{
if ($level === 0 && $filter->isChain() && $filter->isEmpty()) {
return '<ul class="filter-expression filter-root"><li class="active">'
. $this->renderNewFilter()
. '</li></ul>';
}
if ($filter instanceof FilterChain) {
return $this->renderFilterChain($filter, $level);
} elseif ($filter instanceof FilterExpression) {
return $this->renderFilterExpression($filter, $level);
} else {
throw new ProgrammingError('Got a Filter being neither expression nor chain');
}
}
protected function emptyExpression()
{
return Filter::expression('', '=', '');
}
protected function renderFilterChain(FilterChain $filter, $level)
{
$parts = array();
foreach ($filter->filters() as $f) {
$parts[] = '<li>'
. $this->renderFilter($f, $level + 1)
. '</li>';
}
if ($this->addTo && $this->addTo == $filter->getId()) {
$parts[] = '<li style="background: #ffb">'
. $this->renderNewFilter()
// . $this->cancelLink()
. '</li>';
}
return $this->beginChain($filter) . implode('', $parts) . $this->endChain($filter);
}
protected function beginChain(FilterChain $filter)
{
$root = $filter->isRootNode() === 0 ? ' class="filter-root"' : '';
$list = $filter->isEmpty() ? '' : '<ul' . $root . '>' . "\n";
return '<div class="filter-chain'
. '"><span class="handle"> </span>'
. $this->selectOperator($filter)
. $this->removeLink($filter)
. ($filter->count() === 1 ? $this->stripLink($filter) : '')
. $this->addLink($filter)
. $list;
}
protected function endChain(FilterChain $filter)
{
$list = $filter->isEmpty() ? '' : "</ul>\n";
return $list . "</div>\n";
}
protected function beginExpression(FilterExpression $filter)
{
$root = $filter->isRootNode() === 0 ? ' filter-root' : '';
return '<div class="filter-expression' . $root . '">' . "\n";
}
protected function endExpression(FilterExpression $filter)
{
return "</div>\n";
}
protected function eventuallyAddTo($html, Filter $filter, $level)
{
if ($this->addTo && $this->addTo === $filter->getId()) {
// $filter->replaceWith(Filter::matchAll(clone($filter)));
// $and = $filt = Filter::matchAll(clone($filter));
// return $this->renderFilterChain($filter, $level);
$html .= preg_replace(
//'/ class="autosubmit"/',
'/ class="/',
' class="autofocus',
$this->selectOperator()
) . '<ul><li>'
. $html
. '</li><li class="active">'
. $this->renderNewFilter() /*.$this->cancelLink()*/
. '</li></ul>';
}
return $html;
}
protected function filterExpressionHtml(FilterExpression $filter, $level)
{
return $this->selectColumn($filter)
. $this->selectSign($filter)
. $this->element($filter)
. $this->removeLink($filter)
. $this->addLink($filter);
}
protected function renderFilterExpression(FilterExpression $filter, $level)
{
return $this->beginExpression($filter)
. $this->eventuallyAddTo(
$this->filterExpressionHtml($filter, $level),
$filter,
$level
) . $this->endExpression($filter);
}
protected function element(Filter $filter = null)
{
if ($filter) {
// TODO: Make this configurable
$type = 'host';
$dummy = IcingaObject::createByType($type);
$col = $filter->getColumn();
if ($dummy->hasProperty($col)) {
if ($dummy->propertyIsBoolean($col)) {
return $this->boolean($filter);
}
}
if ($col === 'groups' && $dummy->supportsGroups()) {
return $this->selectGroup($type, $filter);
}
}
return $this->text($filter);
}
protected function selectGroup($type, Filter $filter)
{
$available = IcingaObjectGroup::enumForType($type);
return $this->select(
$this->elementId('value', $filter),
$this->optionalEnum($available),
$filter->getExpression()
);
}
protected function boolean(Filter $filter = null)
{
$value = $filter === null ? '' : $filter->getExpression();
$el = new Icinga\Module\Director\Web\Form\Element\Boolean(
$this->elementId('value', $filter),
array(
'value' => $value,
'decorators' => array('ViewHelper'),
)
);
return $el;
}
protected function text(Filter $filter = null)
{
$value = $filter === null ? '' : $filter->getExpression();
if (is_array($value)) {
return $this->view->formExtensibleSet(
$this->elementId('value', $filter),
$value
);
$value = '(' . implode('|', $value) . ')';
}
return $this->view->formText(
$this->elementId('value', $filter),
$value
);
}
protected function renderNewFilter()
{
return $this->selectColumn()
. $this->selectSign()
. $this->element();
}
protected function arrayForSelect($array, $flip = false)
{
$res = array();
foreach ($array as $k => $v) {
if (is_int($k)) {
$res[$v] = ucwords(str_replace('_', ' ', $v));
} elseif ($flip) {
$res[$v] = $k;
} else {
$res[$k] = $v;
}
}
// sort($res);
return $res;
}
protected function elementId($field, Filter $filter = null)
{
$prefix = $this->fieldName . '[id_';
$suffix = '][' . $field . ']';
if ($filter === null) {
return $prefix . 'new_' . ($this->addTo ?: '0') . $suffix;
} else {
return $prefix . $filter->getId() . $suffix;
}
}
protected function selectOperator(Filter $filter = null)
{
$ops = array(
'AND' => 'AND',
'OR' => 'OR',
'NOT' => 'NOT'
);
return $this->view->formSelect(
$this->elementId('operator', $filter),
$filter === null ? null : $filter->getOperatorName(),
array(
'class' => 'operator autosubmit',
),
$ops
);
return $this->select(
$this->elementId('operator', $filter),
$ops,
$filter === null ? null : $filter->getOperatorName(),
array('class' => 'operator autosubmit')
);
}
protected function selectSign(Filter $filter = null)
{
$signs = array(
'=' => '=',
'!=' => '!=',
'>' => '>',
'<' => '<',
'>=' => '>=',
'<=' => '<=',
'in' => 'in',
'true' => 'is true (or set)',
);
if ($filter === null) {
$sign = null;
} else {
if ($filter->getExpression() === true) {
$sign = 'true';
} elseif (is_array($filter->getExpression())) {
$sign = 'in';
} else {
$sign = $filter->getSign();
}
}
$class = 'sign autosubmit';
if (strlen($sign) > 3) {
$class .= ' wide';
}
return $this->select(
$this->elementId('sign', $filter),
$signs,
$sign,
array('class' => $class)
);
}
public function setColumns(array $columns = null)
{
$this->cachedColumnSelect = $columns ? $this->arrayForSelect($columns) : null;
return $this;
}
protected function selectColumn(Filter $filter = null)
{
$active = $filter === null ? null : $filter->getColumn();
if (! $this->hasColumnList()) {
return $this->view->formText(
$this->elementId('column', $filter),
$active
);
}
$cols = $this->getColumnList();
if ($active && !isset($cols[$active])) {
$cols[$active] = str_replace(
'_',
' ',
ucfirst(ltrim($active, '_'))
); // ??
}
$cols = $this->optionalEnum($cols);
return $this->select(
$this->elementId('column', $filter),
$cols,
$active,
array('class' => 'autosubmit')
);
}
protected function optionalEnum($enum)
{
return array_merge(
array(null => $this->view->translate('- please choose -')),
$enum
);
}
protected function hasColumnList()
{
return $this->cachedColumnSelect !== null || $this->query !== null;
}
protected function getColumnList()
{
if ($this->cachedColumnSelect === null) {
$this->fetchColumnList();
}
return $this->cachedColumnSelect;
}
protected function fetchColumnList()
{
if ($this->query instanceof FilterColumns) {
$this->cachedColumnSelect = $this->arrayForSelect(
$this->query->getFilterColumns(),
true
);
asort($this->cachedColumnSelect);
} elseif ($this->cachedColumnSelect === null) {
throw new ProgrammingError('No columns set nor does the query provide any');
}
}
protected function select($name, $list, $selected, $attributes = null)
{
return $this->view->formSelect($name, $selected, $attributes, $list);
}
protected function removeLink(Filter $filter)
{
return $this->filterActionButton(
$filter,
'cancel',
t('Remove this part of your filter')
);
}
protected function addLink(Filter $filter)
{
return $this->filterActionButton(
$filter,
'plus',
t('Add another filter')
);
}
protected function stripLink(Filter $filter)
{
return $this->filterActionButton(
$filter,
'minus',
t('Strip this operator, preserve child nodes')
);
}
protected function filterActionButton(Filter $filter, $action, $title)
{
return $this->iconButton(
$this->getActionButtonName($filter),
$action,
$title
);
}
protected function getActionButtonName(Filter $filter)
{
return sprintf(
'%s[id_%s][action]',
$this->fieldName,
$filter->getId()
);
}
protected function iconButton($name, $icon, $title)
{
return $this->view->formSubmit(
$name,
IconHelper::instance()->iconCharacter($icon),
array('class' => 'icon-button', 'title' => $title)
);
}
}

View File

@ -3,6 +3,7 @@
namespace Icinga\Module\Director\Objects;
use Icinga\Data\Filter\Filter;
use Icinga\Exception\IcingaException;
use Icinga\Exception\ProgrammingError;
use Icinga\Module\Director\IcingaConfig\AssignRenderer;
use Icinga\Module\Director\IcingaConfig\IcingaConfigHelper as c;
@ -18,9 +19,15 @@ class IcingaObjectAssignments
public function __construct(IcingaObject $object)
{
if (! $object->supportsAssignments()) {
if ($object->hasProperty('object_type')) {
$type = $object->object_type;
} else {
$type = get_class($object);
}
throw new ProgrammingError(
'I can only assign for applied objects, got %s',
$object->object_type
$type
);
}
@ -43,13 +50,14 @@ class IcingaObjectAssignments
return $this->setValues(array($values));
}
$this->current = array();
if (is_object($values)) {
$values = (array) $values;
}
$this->current = array();
ksort($values);
foreach ((array) $values as $type => $value) {
foreach ($values as $type => $value) {
if (is_numeric($type)) {
$this->addRule($value);
} else {
@ -107,6 +115,16 @@ class IcingaObjectAssignments
$rows[$val['assign_type']] = array();
}
if (array_key_exists('filter_string', $val)) {
$filter = $val['filter_string'];
if (! $filter->isEmpty()) {
$rows[$val['assign_type']][] = $filter;
}
continue;
}
if (empty($val['property'])) {
continue;
}
@ -132,10 +150,17 @@ class IcingaObjectAssignments
protected function addRule($string, $type = 'assign')
{
if (is_array($string) && array_key_exists('assign_type', $string)) {
$type = $string['assign_type'];
$string = $string['filter_string'];
}
// TODO: validate
//echo "ADD RULE\n";
//var_dump($string);
//echo "ADD RULE END\n";
$this->current[] = array(
'assign_type' => $type,
'filter_string' => $this->rerenderFilter($string)
'filter_string' => $string instanceof Filter ? $this->renderFilter($string) : $string
);
return $this;
@ -227,9 +252,14 @@ class IcingaObjectAssignments
return $this->stored;
}
protected function renderFilter(Filter $filter)
{
return rawurldecode($filter->toQueryString());
}
protected function rerenderFilter($string)
{
return rawurldecode(Filter::fromQueryString($string)->toQueryString());
return $this->renderFilterFilter::fromQueryString($string);
}
protected function createPlain($dbRows)

View File

@ -0,0 +1,206 @@
<?php
namespace Icinga\Module\Director\Web\Form\Element;
use Icinga\Data\Filter\Filter;
use Icinga\Module\Director\Web\Form\IconHelper;
use Exception;
/**
* Input control for extensible sets
*/
class DataFilter extends FormElement
{
/**
* Default form view helper to use for rendering
* @var string
*/
public $helper = 'formDataFilter';
/**
* @codingStandardsIgnoreStart
*/
protected function _filterValue(&$value, &$key)
{
// @codingStandardsIgnoreEnd
try {
if ($value instanceof Filter) {
// OK
} elseif (is_string($value)) {
$value = Filter::fromQueryString($value);
} else {
$value = $this->arrayToFilter($value);
}
if ($value->isEmpty()) {
$value = Filter::matchAll(Filter::expression('', '=', ''));
}
} catch (Exception $e) {
$value = null;
// TODO: getFile, getLine
// Hint: cannot addMessage at it would loop through getValue
$this->addErrorMessage($e->getMessage());
$this->_isErrorForced = true;
}
}
protected function arrayToFilter($array)
{
if ($array === null) {
return Filter::matchAll();
}
$firstKey = key($array);
if (! in_array($firstKey, array('id_1', 'id_new_0'))) {
die('FCK: ' . key($array));
}
$entry = array_shift($array);
$filter = $this->entryToFilter($entry);
if ($firstKey === 'id_new_0') {
$this->setAttrib('addTo', '0');
}
$remove = $strip = null;
// TODO: This is for the first entry, duplicates code and has debug info
$filterId = $this->idToFilterId($firstKey);
switch ($this->entryAction($entry)) {
case 'cancel':
$remove = $filterId;
echo "cancel";
break;
case 'minus':
$strip = $filterId;
echo "minus";
break;
case 'plus':
$this->setAttrib('addTo', $filterId);
echo "plus";
break;
}
foreach ($array as $id => $entry) {
// TODO: addTo from FilterEditor
$sub = $this->entryToFilter($entry);
$filterId = $this->idToFilterId($id);
switch ($this->entryAction($entry)) {
case 'cancel':
$remove = $filterId;
break;
case 'minus':
$strip = $filterId;
break;
case 'plus':
$this->setAttrib('addTo', $filterId);
break;
}
$parentId = $this->parentIdFor($filterId);
$filter->getById($parentId)->addFilter($sub);
}
if ($remove) {
if ($filter->getById($remove)->isRootNode()) {
$filter = Filter::matchAll();
} else {
$filter->removeId($remove);
}
}
if ($strip) {
$subId = $strip . '-1';
if ($filter->getId() === $strip) {
$filter = $filter->getById($strip . '-1');
} else {
$filter->replaceById($strip, $filter->getById($strip . '-1'));
}
}
return $filter;
}
protected function parentIdFor($id)
{
if (false === ($pos = strrpos($id, '-'))) {
return '0';
} else {
return substr($id, 0, $pos);
}
}
protected function idToFilterId($id)
{
if (! preg_match('/^id_(new_)?(\d+(?:-\d+)*)$/', $id, $m)) {
die('nono' . $id);
}
return $m[2];
}
protected function entryToFilter($entry)
{
if (array_key_exists('operator', $entry)) {
return Filter::chain($entry['operator']);
} else {
if ($entry['sign'] === 'true') {
return Filter::expression(
$entry['column'],
'=',
true
);
} elseif ($entry['sign'] === 'in') {
if (array_key_exists('value', $entry)) {
if (is_array($entry['value'])) {
$value = array_filter($entry['value'], 'strlen');
} elseif (empty($entry['value'])) {
$value = array();
} else {
$value = array($entry['value']);
}
} else {
$value = array();
}
return Filter::expression(
$entry['column'],
'=',
$value
);
} else {
return Filter::expression(
$entry['column'],
$entry['sign'],
array_key_exists('value', $entry) ? $entry['value'] : null
);
}
}
}
protected function entryAction($entry)
{
if (array_key_exists('action', $entry)) {
return IconHelper::instance()->characterIconName($entry['action']);
}
return null;
}
public function isValid($value, $context = null)
{
if (! $value instanceof Filter) {
// TODO: try, return false on E
$filter = $this->arrayToFilter($value);
}
$this->setValue($filter);
return true;
}
}