Skip to content

Commit

Permalink
Add option target_admin_access_action to ModelAutocompleteType (son…
Browse files Browse the repository at this point in the history
  • Loading branch information
pulzarraider authored and greg0ire committed Apr 24, 2017
1 parent a05559a commit a9f2c13
Show file tree
Hide file tree
Showing 5 changed files with 348 additions and 13 deletions.
19 changes: 11 additions & 8 deletions Controller/HelperController.php
Original file line number Diff line number Diff line change
Expand Up @@ -352,31 +352,34 @@ public function retrieveAutocompleteItemsAction(Request $request)
$itemsPerPage = $filterAutocomplete->getFieldOption('items_per_page', 10);
$reqParamPageNumber = $filterAutocomplete->getFieldOption('req_param_name_page_number', '_page');
$toStringCallback = $filterAutocomplete->getFieldOption('to_string_callback');
$targetAdminAccessAction = $filterAutocomplete->getFieldOption('target_admin_access_action');
} else {
// create/edit form
$fieldDescription = $this->retrieveFormFieldDescription($admin, $request->get('field'));
$formAutocomplete = $admin->getForm()->get($fieldDescription->getName());

if ($formAutocomplete->getConfig()->getAttribute('disabled')) {
$formAutocompleteConfig = $formAutocomplete->getConfig();
if ($formAutocompleteConfig->getAttribute('disabled')) {
throw new AccessDeniedException(
'Autocomplete list can`t be retrieved because the form element is disabled or read_only.'
);
}

$property = $formAutocomplete->getConfig()->getAttribute('property');
$callback = $formAutocomplete->getConfig()->getAttribute('callback');
$minimumInputLength = $formAutocomplete->getConfig()->getAttribute('minimum_input_length');
$itemsPerPage = $formAutocomplete->getConfig()->getAttribute('items_per_page');
$reqParamPageNumber = $formAutocomplete->getConfig()->getAttribute('req_param_name_page_number');
$toStringCallback = $formAutocomplete->getConfig()->getAttribute('to_string_callback');
$property = $formAutocompleteConfig->getAttribute('property');
$callback = $formAutocompleteConfig->getAttribute('callback');
$minimumInputLength = $formAutocompleteConfig->getAttribute('minimum_input_length');
$itemsPerPage = $formAutocompleteConfig->getAttribute('items_per_page');
$reqParamPageNumber = $formAutocompleteConfig->getAttribute('req_param_name_page_number');
$toStringCallback = $formAutocompleteConfig->getAttribute('to_string_callback');
$targetAdminAccessAction = $formAutocompleteConfig->getAttribute('target_admin_access_action');
}

$searchText = $request->get('q');

$targetAdmin = $fieldDescription->getAssociationAdmin();

// check user permission
$targetAdmin->checkAccess('list');
$targetAdmin->checkAccess($targetAdminAccessAction);

if (mb_strlen($searchText, 'UTF-8') < $minimumInputLength) {
return new JsonResponse(array('status' => 'KO', 'message' => 'Too short search string.'), 403);
Expand Down
4 changes: 4 additions & 0 deletions Form/Type/ModelAutocompleteType.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public function buildForm(FormBuilderInterface $builder, array $options)
|| (array_key_exists('read_only', $options) && $options['read_only'])
);
$builder->setAttribute('to_string_callback', $options['to_string_callback']);
$builder->setAttribute('target_admin_access_action', $options['target_admin_access_action']);

if ($options['multiple']) {
$resizeListener = new ResizeFormListener(
Expand Down Expand Up @@ -140,6 +141,9 @@ public function configureOptions(OptionsResolver $resolver)
'req_param_name_page_number' => '_page',
'req_param_name_items_per_page' => '_per_page',

// security
'target_admin_access_action' => 'list',

// CSS classes
'container_css_class' => '',
'dropdown_css_class' => '',
Expand Down
52 changes: 52 additions & 0 deletions Resources/doc/reference/form_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,58 @@ template
{# change the default selection format #}
{% block sonata_type_model_autocomplete_selection_format %}'<b>'+item.label+'</b>'{% endblock %}
target_admin_access_action
defaults to ``list``.
By default, the user needs the ``LIST`` role (mapped to ``list`` access action)
to get the autocomplete items from the target admin's datagrid.
If you can't give some users this role because they will then have access to the target
admin's datagrid, you have to grant them another role.

In the example below we changed the ``target_admin_access_action`` from ``list`` to ``autocomplete``,
which is mapped in the target admin to ``AUTOCOMPLETE`` role. Please make sure that all valid users
have the ``AUTOCOMPLETE`` role.

.. code-block:: php
<?php
// src/AppBundle/Admin/ArticleAdmin.php
class ArticleAdmin extends AbstractAdmin
{
protected function configureFormFields(FormMapper $formMapper)
{
// the dropdown autocomplete list will show only Category
// entities that contain specified text in "title" attribute
$formMapper
->add('category', 'sonata_type_model_autocomplete', array(
'property' => 'title',
'target_admin_access_action' => 'autocomplete'
))
;
}
}
.. code-block:: php
<?php
// src/AppBundle/Admin/CategoryAdmin.php
class CategoryAdmin extends AbstractAdmin
{
protected $accessMapping = array(
'autocomplete' => 'AUTOCOMPLETE',
);
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
// this text filter will be used to retrieve autocomplete fields
// only the users with role AUTOCOMPLETE will be able to get the items
$datagridMapper
->add('title')
;
}
}
sonata_choice_field_mask
^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
261 changes: 259 additions & 2 deletions Tests/Controller/HelperControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -575,8 +575,6 @@ public function testRetrieveAutocompleteItemsActionDisabledFormelememt()
->with('create')
->will($this->returnValue(true));

$entity = new Foo();

$fieldDescription = $this->createMock('Sonata\AdminBundle\Admin\FieldDescriptionInterface');

$fieldDescription->expects($this->once())
Expand Down Expand Up @@ -634,6 +632,265 @@ public function testRetrieveAutocompleteItemsActionDisabledFormelememt()
$this->controller->retrieveAutocompleteItemsAction($request);
}

public function testRetrieveAutocompleteItemsTooShortSearchString()
{
$this->admin->expects($this->once())
->method('hasAccess')
->with('create')
->will($this->returnValue(true));

$targetAdmin = $this->createMock('Sonata\AdminBundle\Admin\AbstractAdmin');
$targetAdmin->expects($this->once())
->method('checkAccess')
->with('list')
->will($this->returnValue(null));

$fieldDescription = $this->createMock('Sonata\AdminBundle\Admin\FieldDescriptionInterface');

$fieldDescription->expects($this->once())
->method('getTargetEntity')
->will($this->returnValue('Sonata\AdminBundle\Tests\Fixtures\Bundle\Entity\Foo'));

$fieldDescription->expects($this->once())
->method('getName')
->will($this->returnValue('barField'));

$fieldDescription->expects($this->once())
->method('getAssociationAdmin')
->will($this->returnValue($targetAdmin));

$this->admin->expects($this->once())
->method('getFormFieldDescriptions')
->will($this->returnValue(null));

$this->admin->expects($this->once())
->method('getFormFieldDescription')
->with('barField')
->will($this->returnValue($fieldDescription));

$form = $this->getMockBuilder('Symfony\Component\Form\Form')
->disableOriginalConstructor()
->getMock();

$this->admin->expects($this->once())
->method('getForm')
->will($this->returnValue($form));

$formType = $this->getMockBuilder('Symfony\Component\Form\Form')
->disableOriginalConstructor()
->getMock();

$form->expects($this->once())
->method('get')
->with('barField')
->will($this->returnValue($formType));

$formConfig = $this->getMockBuilder('Symfony\Component\Form\FormConfigInterface')
->disableOriginalConstructor()
->getMock();

$formType->expects($this->once())
->method('getConfig')
->will($this->returnValue($formConfig));

$formConfig->expects($this->any())
->method('getAttribute')
->will($this->returnCallback(function ($name, $default = null) {
switch ($name) {
case 'property':
return 'foo';
case 'callback':
return;
case 'minimum_input_length':
return 3;
case 'items_per_page':
return 10;
case 'req_param_name_page_number':
return '_page';
case 'to_string_callback':
return;
case 'disabled':
return false;
case 'target_admin_access_action':
return 'list';
default:
throw new \RuntimeException(sprintf('Unkown parameter "%s" called.', $name));
}
}));

$request = new Request(array(
'admin_code' => 'foo.admin',
'field' => 'barField',
'q' => 'so',
), array(), array(), array(), array(), array('REQUEST_METHOD' => 'GET', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'));

$response = $this->controller->retrieveAutocompleteItemsAction($request);
$this->isInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
$this->assertSame('application/json', $response->headers->get('Content-Type'));
$this->assertSame('{"status":"KO","message":"Too short search string."}', $response->getContent());
}

public function testRetrieveAutocompleteItems()
{
$entity = new Foo();
$this->admin->expects($this->once())
->method('hasAccess')
->with('create')
->will($this->returnValue(true));

$this->admin->expects($this->once())
->method('id')
->with($entity)
->will($this->returnValue(123));

$targetAdmin = $this->createMock('Sonata\AdminBundle\Admin\AbstractAdmin');
$targetAdmin->expects($this->once())
->method('checkAccess')
->with('list')
->will($this->returnValue(null));

$targetAdmin->expects($this->once())
->method('setPersistFilters')
->with(false)
->will($this->returnValue(null));

$datagrid = $this->createMock('Sonata\AdminBundle\Datagrid\DatagridInterface');
$targetAdmin->expects($this->once())
->method('getDatagrid')
->with()
->will($this->returnValue($datagrid));

$metadata = $this->createMock('Sonata\CoreBundle\Model\Metadata');
$metadata->expects($this->once())
->method('getTitle')
->with()
->will($this->returnValue('FOO'));

$targetAdmin->expects($this->once())
->method('getObjectMetadata')
->with($entity)
->will($this->returnValue($metadata));

$datagrid->expects($this->once())
->method('hasFilter')
->with('foo')
->will($this->returnValue(true));

$datagrid->expects($this->exactly(3))
->method('setValue')
->withConsecutive(
array($this->equalTo('foo'), $this->equalTo(null), $this->equalTo('sonata')),
array($this->equalTo('_per_page'), $this->equalTo(null), $this->equalTo(10)),
array($this->equalTo('_page'), $this->equalTo(null), $this->equalTo(1))
)
->will($this->returnValue(null));

$datagrid->expects($this->once())
->method('buildPager')
->with()
->will($this->returnValue(null));

$pager = $this->createMock('Sonata\AdminBundle\Datagrid\Pager');
$datagrid->expects($this->once())
->method('getPager')
->with()
->will($this->returnValue($pager));

$pager->expects($this->once())
->method('getResults')
->with()
->will($this->returnValue(array($entity)));

$pager->expects($this->once())
->method('isLastPage')
->with()
->will($this->returnValue(true));

$fieldDescription = $this->createMock('Sonata\AdminBundle\Admin\FieldDescriptionInterface');

$fieldDescription->expects($this->once())
->method('getTargetEntity')
->will($this->returnValue('Sonata\AdminBundle\Tests\Fixtures\Bundle\Entity\Foo'));

$fieldDescription->expects($this->once())
->method('getName')
->will($this->returnValue('barField'));

$fieldDescription->expects($this->once())
->method('getAssociationAdmin')
->will($this->returnValue($targetAdmin));

$this->admin->expects($this->once())
->method('getFormFieldDescriptions')
->will($this->returnValue(null));

$this->admin->expects($this->once())
->method('getFormFieldDescription')
->with('barField')
->will($this->returnValue($fieldDescription));

$form = $this->getMockBuilder('Symfony\Component\Form\Form')
->disableOriginalConstructor()
->getMock();

$this->admin->expects($this->once())
->method('getForm')
->will($this->returnValue($form));

$formType = $this->getMockBuilder('Symfony\Component\Form\Form')
->disableOriginalConstructor()
->getMock();

$form->expects($this->once())
->method('get')
->with('barField')
->will($this->returnValue($formType));

$formConfig = $this->getMockBuilder('Symfony\Component\Form\FormConfigInterface')
->disableOriginalConstructor()
->getMock();

$formType->expects($this->once())
->method('getConfig')
->will($this->returnValue($formConfig));

$formConfig->expects($this->any())
->method('getAttribute')
->will($this->returnCallback(function ($name, $default = null) {
switch ($name) {
case 'property':
return 'foo';
case 'callback':
return;
case 'minimum_input_length':
return 3;
case 'items_per_page':
return 10;
case 'req_param_name_page_number':
return '_page';
case 'to_string_callback':
return;
case 'disabled':
return false;
case 'target_admin_access_action':
return 'list';
default:
throw new \RuntimeException(sprintf('Unkown parameter "%s" called.', $name));
}
}));

$request = new Request(array(
'admin_code' => 'foo.admin',
'field' => 'barField',
'q' => 'sonata',
), array(), array(), array(), array(), array('REQUEST_METHOD' => 'GET', 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'));

$response = $this->controller->retrieveAutocompleteItemsAction($request);
$this->isInstanceOf('Symfony\Component\HttpFoundation\Response', $response);
$this->assertSame('application/json', $response->headers->get('Content-Type'));
$this->assertSame('{"status":"OK","more":false,"items":[{"id":123,"label":"FOO"}]}', $response->getContent());
}

/**
* Symfony Validator has 2 API version (2.4 and 2.5)
* This data provider ensure tests pass on each one.
Expand Down
Loading

0 comments on commit a9f2c13

Please sign in to comment.