diff --git a/Action/Action.php b/Action/Action.php index e89ec8aed..09b6d87e7 100644 --- a/Action/Action.php +++ b/Action/Action.php @@ -3,6 +3,7 @@ namespace LAG\AdminBundle\Action; use LAG\AdminBundle\Action\Configuration\ActionConfiguration; +use LAG\AdminBundle\Admin\AdminInterface; use LAG\AdminBundle\Admin\Filter; use LAG\AdminBundle\Field\Field; @@ -151,4 +152,15 @@ public function getConfiguration() { return $this->configuration; } + + /** + * Return true if the pagination is required for this action. Only action with a "multiple" load strategy require + * pagination. + * + * @return bool + */ + public function isPaginationRequired() + { + return $this->configuration->getParameter('load_strategy') === AdminInterface::LOAD_STRATEGY_MULTIPLE; + } } diff --git a/Action/ActionInterface.php b/Action/ActionInterface.php index 61757b89c..6fd0ec764 100644 --- a/Action/ActionInterface.php +++ b/Action/ActionInterface.php @@ -65,4 +65,11 @@ public function setFields($fields); * @param Field $field */ public function addField(Field $field); + + /** + * Return true if the Action requires pagination. + * + * @return bool + */ + public function isPaginationRequired(); } diff --git a/Action/Configuration/ActionConfiguration.php b/Action/Configuration/ActionConfiguration.php index 0160571d6..31d1e5607 100644 --- a/Action/Configuration/ActionConfiguration.php +++ b/Action/Configuration/ActionConfiguration.php @@ -100,7 +100,8 @@ public function configureOptions(OptionsResolver $resolver) 'xls' ]); - // entity will be retrived with this order. It should be an array of field/order mapping + // retrieved entities are sorted according to this option it should be an array indexed by the field name + // with its order as a value $resolver ->setDefault('order', []) ->setAllowedTypes('order', 'array'); diff --git a/Admin/Admin.php b/Admin/Admin.php index c9f1767fb..374ac16ff 100644 --- a/Admin/Admin.php +++ b/Admin/Admin.php @@ -196,6 +196,11 @@ public function create() ->entities ->add($entity); + // inform the user that the entity is created + $this + ->messageHandler + ->handleSuccess($this->generateMessageTranslationKey('created')); + return $entity; } @@ -212,16 +217,16 @@ public function save() ->dataProvider ->save($entity); } - // inform user everything went fine + // inform the user that the entity is saved $this ->messageHandler - ->handleSuccess('lag.admin.'.$this->name.'.saved'); + ->handleSuccess($this->generateMessageTranslationKey('saved')); $success = true; } catch (Exception $e) { $this ->messageHandler ->handleError( - 'lag.admin.saved_errors', + $this->generateMessageTranslationKey('lag.admin.saved_errors'), "An error has occurred while saving an entity : {$e->getMessage()}, stackTrace: {$e->getTraceAsString()}" ); $success = false; @@ -242,16 +247,16 @@ public function remove() ->dataProvider ->remove($entity); } - // inform user everything went fine + // inform the user that the entity is removed $this ->messageHandler - ->handleSuccess('lag.admin.'.$this->name.'.deleted'); + ->handleSuccess($this->generateMessageTranslationKey('deleted')); $success = true; } catch (Exception $e) { $this ->messageHandler ->handleError( - 'lag.admin.deleted_errors', + $this->generateMessageTranslationKey('lag.admin.deleted_errors'), "An error has occurred while deleting an entity : {$e->getMessage()}, stackTrace: {$e->getTraceAsString()} " ); $success = false; @@ -272,10 +277,10 @@ public function generateRouteName($actionName) { if (!array_key_exists($actionName, $this->getConfiguration()->getParameter('actions'))) { throw new Exception( - sprintf('Invalid action name %s for admin %s (available action are: %s)', - $actionName, - $this->getName(), - implode(', ', $this->getActionNames())) + sprintf('Invalid action name %s for admin %s (available action are: %s)', + $actionName, + $this->getName(), + implode(', ', $this->getActionNames())) ); } // get routing name pattern @@ -298,13 +303,16 @@ public function generateRouteName($actionName) */ public function load(array $criteria, $orderBy = [], $limit = 25, $offset = 1) { - $pager = $this + $actionConfiguration = $this + ->getCurrentAction() + ->getConfiguration(); + $pager = $actionConfiguration->getParameter('pager'); + $requirePagination = $this ->getCurrentAction() - ->getConfiguration() - ->getParameter('pager'); + ->isPaginationRequired(); - if ($pager == 'pagerfanta') { - // adapter to pager fanta + if ($pager == 'pagerfanta' && $requirePagination) { + // adapter to pagerfanta $adapter = new PagerFantaAdminAdapter($this->dataProvider, $criteria, $orderBy); // create pager $this->pager = new Pagerfanta($adapter); @@ -315,6 +323,10 @@ public function load(array $criteria, $orderBy = [], $limit = 25, $offset = 1) ->pager ->getCurrentPageResults(); } else { + // if the current action should retrieve only one entity, the offset should be zero + if ($actionConfiguration->getParameter('load_strategy') !== AdminInterface::LOAD_STRATEGY_MULTIPLE) { + $offset = 0; + } $entities = $this ->dataProvider ->findBy($criteria, $orderBy, $limit, $offset); @@ -480,7 +492,7 @@ public function isCurrentActionDefined() } /** - * Return admin configuration object + * Return admin configuration object. * * @return AdminConfiguration */ @@ -488,4 +500,19 @@ public function getConfiguration() { return $this->configuration; } + + /** + * Return a translation key for a message according to the Admin's translation pattern. + * + * @param string $message + * @return string + */ + protected function generateMessageTranslationKey($message) + { + return $this->getTranslationKey( + $this->configuration->getParameter('translation_pattern'), + $message, + $this->name + ); + } } diff --git a/Admin/Behaviors/AdminTrait.php b/Admin/Behaviors/AdminTrait.php index 0aa3c102c..17f6fe72f 100644 --- a/Admin/Behaviors/AdminTrait.php +++ b/Admin/Behaviors/AdminTrait.php @@ -9,6 +9,7 @@ trait AdminTrait use EntityLabelTrait { getEntityLabel as parentEntityLabel; } + use TranslationKeyTrait; /** * @var Pagerfanta diff --git a/Application/Configuration/ApplicationConfiguration.php b/Application/Configuration/ApplicationConfiguration.php index 233cf6a3f..463805bd6 100644 --- a/Application/Configuration/ApplicationConfiguration.php +++ b/Application/Configuration/ApplicationConfiguration.php @@ -40,7 +40,7 @@ public function configureOptions(OptionsResolver $resolver) // main base template // as bundles are not loaded when reading the configuration, the kernel locateResources will always failed. - // So we must not check resource existance here. + // So we must not check resource existence here. $resolver->setDefault('base_template', 'LAGAdminBundle::admin.layout.html.twig'); $resolver->setAllowedTypes('base_template', 'string'); @@ -64,13 +64,38 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('string_length_truncate', 'string'); // routing configuration (route name pattern and url name pattern) + $this->setRoutingOptions($resolver); + + // translation configuration + $this->setTranslationOptions($resolver); + + // maximum number of elements displayed + $resolver->setDefault('max_per_page', 25); + $resolver->setAllowedTypes('max_per_page', 'integer'); + + // admin field type mapping + $this->setFieldsOptions($resolver); + } + + /** + * @param OptionsResolver $resolver + */ + protected function setRoutingOptions(OptionsResolver $resolver) + { $resolver->setDefault('routing', [ 'url_pattern' => '/{admin}/{action}', - 'name_pattern' => 'lag.admin.{admin}', + 'name_pattern' => 'lag.admin.{admin}.{action}', ]); $resolver->setAllowedTypes('routing', 'array'); $resolver->setNormalizer('routing', function(Options $options, $value) { + if (!array_key_exists('url_pattern', $value)) { + $value['url_pattern'] = '/{admin}/{action}'; + } + if (!array_key_exists('name_pattern', $value)) { + $value['name_pattern'] = 'lag.admin.{admin}.{action}'; + } + // url pattern should contain {admin} and {action} token $urlPattern = $value['url_pattern']; @@ -87,11 +112,19 @@ public function configureOptions(OptionsResolver $resolver) if (strstr($namePattern, '{admin}') === false) { throw new InvalidOptionsException('Admin routing configuration pattern name should contains the {admin} placeholder'); } + if (strstr($namePattern, '{action}') === false) { + throw new InvalidOptionsException('Admin routing configuration pattern name should contains the {action} placeholder'); + } return $value; }); + } - // translation configuration + /** + * @param OptionsResolver $resolver + */ + protected function setTranslationOptions(OptionsResolver $resolver) + { $resolver->setDefault('translation', [ 'enabled' => true, 'pattern' => 'lag.admin.{key}' @@ -99,6 +132,10 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setAllowedTypes('translation', 'array'); $resolver->setNormalizer('translation', function(Options $options, $value) { + if (!array_key_exists('enabled', $value)) { + throw new InvalidOptionsException('Admin translation enabled parameter should be defined'); + } + if (!is_bool($value['enabled'])) { throw new InvalidOptionsException('Admin translation enabled parameter should be a boolean'); } @@ -113,12 +150,13 @@ public function configureOptions(OptionsResolver $resolver) return $value; }); + } - // maximum number of elements displayed - $resolver->setDefault('max_per_page', 25); - $resolver->setAllowedTypes('max_per_page', 'integer'); - - // admin field type mapping + /** + * @param OptionsResolver $resolver + */ + protected function setFieldsOptions(OptionsResolver $resolver) + { $defaultMapping = [ Field::TYPE_STRING => StringField::class, Field::TYPE_ARRAY => Field\ArrayField::class, diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 69c55829e..8bfb13623 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -37,6 +37,9 @@ public function getAdminsConfigurationNode() $node = $builder->root('admins'); $node + // useAttributeAsKey() method will preserve keys when multiple configurations files are used and then avoid + // admin not found by configuration override + ->useAttributeAsKey('name') ->prototype('array') ->children() ->scalarNode('entity')->end() diff --git a/Form/Type/DateTimePickerType.php b/Form/Type/DateTimePickerType.php index d20427a65..6c26843f4 100644 --- a/Form/Type/DateTimePickerType.php +++ b/Form/Type/DateTimePickerType.php @@ -4,6 +4,7 @@ use LAG\AdminBundle\Application\Configuration\ApplicationConfiguration; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\DateTimeType; use Symfony\Component\OptionsResolver\OptionsResolver; class DateTimePickerType extends AbstractType @@ -24,9 +25,12 @@ public function configureOptions(OptionsResolver $resolver) ]); } + /** + * @return string + */ public function getParent() { - return 'datetime'; + return DateTimeType::class; } public function getName() diff --git a/README.md b/README.md index 439a9bac8..a650d5cc1 100644 --- a/README.md +++ b/README.md @@ -1,183 +1,211 @@ -# AdminBundle +[![Build Status](https://travis-ci.org/larriereguichet/AdminBundle.svg?branch=master)](https://travis-ci.org/larriereguichet/AdminBundle) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/larriereguichet/AdminBundle/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/larriereguichet/AdminBundle/?branch=master) +[![SensioLabsInsight](https://insight.sensiolabs.com/projects/c8e28654-44c7-46f3-9450-497e37bda3d0/mini.png)](https://insight.sensiolabs.com/projects/c8e28654-44c7-46f3-9450-497e37bda3d0) + -AdminBundle allows you to create flexible and robust management application, using a simple configuration, -for your Symfony application. +# AdminBundle -Development and Documentation are in progress +The AdminBundle let you creates a **flexible** and **robust backoffice** on any Symfony application, with simple `yml` configuration. -Testing : -branch dev [![Build Status](https://travis-ci.org/larriereguichet/AdminBundle.svg?branch=dev)](https://travis-ci.org/larriereguichet/AdminBundle) -coverage [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/larriereguichet/AdminBundle/badges/quality-score.png?b=dev)](https://scrutinizer-ci.com/g/larriereguichet/AdminBundle/?branch=dev) +It eases the handling of CRUD views by configuring `Admin` objects on top of your Doctrine entities. Each `Admin` has one or many `Action`. +By default, the four actions of a classical CRUD are available (`create`, `edit`, `delete` and `list`) and the creation of new actions is painless. -## Features +If you require *more flexibility*, you can easily override any part of the process (repositories, controllers, views...). +The purpose of the bundle is to provide an Admin interface with default configuration, and allows the user to add his +specific need where he wants, and allow to implements any specific needs without any hassles. -Current version: 0.2 -* Provides dynamics CRUD for your entities (not generated) -* Simple configuration in yml (look alike symfony1 generators.yml files) -* Basic permissions -* Built-in pager (using PagerFanta) -* List export in multiple formats : html, pdf, xls, csv, json (using EE/DataExporter) -* Fully customizable (use your own controllers, managers or templates) +## Features +Version 0.4 : +* Dynamic CRUD for your entities (no code generation) +* Simple configuration in yml (look alike symfony1 generators.yml syntax) +* List with pagination, sorting and batch remove (filters are coming) +* Full translated +* Main and left menu integration +* Fully customizable (use your own controllers, data providers or templates) +* Bootstrap integration (can be disabled or override) -# Installation +## Installation ``` -php composer.phar require lag/adminbundle + composer require lag/adminbundle ``` -# Getting started +AdminBundle rely on KnpMenuBundle to handle menus and on WhiteOctoberPagerfantaBundle to handle list pagination. If you +want to use those features, both bundles should be enabled in addition to AdminBundle. + +```php -## Configuring your application + class AppKernel extends Kernel { + ... -AdminBundle configuration is made in config.yml file. However, if you require a lot of configuration, it is maybe better -to store it in a separate file. + new LAG\AdminBundle\LAGAdminBundle(), + new Knp\Bundle\MenuBundle\KnpMenuBundle(), + new WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(), -__1) Create app/config/admins.yml file__ + ... +``` -To manage easily your admin configuration, you can put it in a separate configuration file (for example, app/config/admins.yml) -This is optional, you can write your configuration directly in your config.yml. If you choose to have a separate file, you have to -import it in your config.yml (@see [Configuration Organization](http://symfony.com/doc/current/cookbook/configuration/configuration_organization.html#advanced-techniques)). +## Configuration -__2) Add your admins.yml file into app/config/config.yml__ +As the configuration can be huge depending on your needs, we recommend to put the AdminBundle's configuration in a +separate file. ```yml - # app/config/config.yml - imports: - - { resource: admins.yml } - ... + app/config/config.yml + imports: + - { resource: admin.yml } ``` -__3) Configure your application parameters__ - -AdminBundle comes with a built-in main layout to handle page parameters (like title...). You have to configure at least your -application title, which is the content title markup by default for all your pages. - ```yml - # config.yml - blue_bear_admin: - application: - title: MyLittleTauntaun Admin - description: My little tauntaun is so nice !!! - + app/config/admin.yml + + lag_admin: + application: + title: My Little TaunTaun application + knp_menu: + twig: + template: 'LAGAdminBundle:Menu:bootstrap_menu.html.twig' ``` -__4) Configuring your CRUD views__ - -Now you have to configure the entities you want to have an admin. To achieve this, you have to add an entry in the admins -configuration. -"my_entity" is just a key. It has to be unique for each entities you have configured. -"MyBundle\Entity\MyEntity" is the namespace path to the entity -"my_form_type" is the form you want to be displayed on edition and creation for your entity. In future version, you will -be allow to set this parameter to null to let AdminBundle generates a form for you. For now you should expose your form -type as a service, and "my_form_type" is the service id. -To expose form type as service, rendez-vous [here](http://symfony.com/doc/current/cookbook/form/create_custom_field_type.html#creating-your-field-type-as-a-service) +## Admin configuration +An `Admin` is based on an Doctrine entity and a Symfony form class (for create and edit actions). Both should be provided +to enable an `Admin`. + ```yml - blue_bear_admin: + app/config/admin.yml + + lag_admin: admins: - my_entity: - entity: MyBundle\Entity\MyEntity - form: my_form_type - + planet: + entity: UniverseBundle\Entity\Planet + form: UniverseBundle\Form\Type\PlanetType ``` -__5) Import AdminBundle routing__ - -You have to import AdminBundle routing (or using yours) in your app/config/routing.yml file. +AdminBundle use a data provider to retrieve and save entities. If you do not provide one, the default one will be used. +It assumes that you have Doctrine repository implementing the `LAG\AdminBundle\Repository\RepositoryInterface`. It will +add the save and delete method to your repository. -```yml - # app/config/routing.yml - # LAG AdminBundle - blue_bear_admin: - resource: . - type: extra - # optional prefix - prefix: /admin - -``` +Fortunately, the AdminBundle provide the `LAG\AdminBundle\Repository\DoctrineRepository` abstract repository class which +implements those methods for you. All you have to do is to extend this class with your repository + +```php + + namespace UniverseBundle\Repository; -That's it. You should have admin application and you should be able to manage your entities. + use LAG\AdminBundle\Repository\DoctrineRepository; + class PlanetRepository extends DoctrineRepository { + + ... +``` + +Your admin is now ready! +> As new routes will be generated, you may need to clear the Symfony's cache. -__Configuration Reference__ +## Configuration Reference ```yml -blue_bear_admin: - application: - # application configuration - title: MyLittleTaunTaun Admin - description: My little tauntaun is so nice !!! - # main layout - layout: MyBundle::layout.html.twig - # form block fields template - block_template: MyBundle:Form:fields.html.twig - # number of entities per page before displaying a pager (for all admin by default) - max_per_page: 25 - # use default bootstrap integration (add bootstrap css and js to the layout) - bootstrap: true - # routing options - routing: - # admin routing name pattern - name_pattern: 'lag.admin.{admin}.{action}' - # admin url pattern (admin is the unique key that you have configured in admins configuration section) - url_pattern: '{admin}/{action}' - # list of managed admins - admins: - # entities (pencil is an example) - tauntaun: - # orm entity class - entity: MyBundle\Entity\TaunTaun - # associated form type - form: tauntaun_type - # if you want full customization, use your controller to handle actions - controller: MyBundle:MyController - # entity manager call for CRUD actions - manager: - # service id of your custom manager - name: my_bundle.manager.tauntaun - # only save action is supported right now. More are coming. myCustomSaveMethod should be a method of - # your manager @my_bundle.manager.tauntaun - save: myCustomSaveMethod - # number of entities per page before displaying a pager (just for current admin) + lag_admin: + application: + title: My Little TaunTaun application + description: My Little TaunTaun application using Admin Bundle + locale: en + # Use the css framework Bootstrap integration (default: true) + bootstrap: true + # Your base template (default: LAGAdminBundle::admin.layout.html.twig) + base_template: 'MyLittleTaunTaunBundle::layout.html.twig' + # Form block template + block_template: 'MyLittleTaunTaunBundle:Form:fields.html.twig' + # Admins routing configuration + routing: + name_pattern: 'tauntaun.{admin}.{action}' + url_pattern: 'tauntaun/{admin}/{action}' + # Use extra configuration helper (default: true) + enable_extra_configuration: true + # Global date format (can be override for each admin, or field) + date_format: 'd/M/Y' + # In list view, strings will be truncated after 200 characters and will be suffixed by ... + string_length: 200 + string_length_truncate: '...' + # Translation configuration + translation: + # Default: true + enabled: true + pattern: app.{key} + # In list view, only 25 items per page max_per_page: 25 - # available actions (for current admin). Actions should always be list, create, edit and delete. More - # customizations will be available in future versions - actions: - list: - # title of the list page - title: Pencils list - # roles allowed to the list action - permissions: [ROLE_ADMIN, ROLE_USER] - # available export for list action - export: [html, pdf, xls, csv, json] - # this fields will be displayed in the list table - fields: - # your entity should have those fields accessible (public property, getters...) - # for example, getId(). @see Symfony\Component\PropertyAccess\PropertyAccessor - id: ~ - name: ~ - label: ~ - # displayed only 50 first characters in list action for this field (more options are coming to - # customize field display - description: {length: 50} - # in this example, riders is related object to current entity (via ManyToOne, a rider can have - # many tauntaun in this case, your entity rider should have an accessible property label) - rider: ~ - # default configuration for those actions + fields_mapping: + # You can override or create new field (it should be declared in services.yml, see the dedicated chapter) + my_custom_field: MyLittleTaunTaunBundle\Fields\MyCustomField + my_custom_string: MyLittleTaunTaunBundle\Fields\MyString + admins: + planet: + # Generic action create: ~ edit: ~ delete: ~ - # short configuration for this entity - rider: - entity: MyOtherBundle\ORM\Rider - form: rider_type - + list: + fields: + id: ~ + name: + type: link + options: + length: 40 + # According to global routing pattern + route: tauntaun.planet.edit + parameters: {id: ~} + category: ~ + galaxy: ~ + publicationStatus: ~ + publicationDate: {type: date, options: {format: d/m/Y}} + updatedAt: {type: date, options: {format: '\L\e d/m/Y à h:i:s'}} + # Your Doctrine entity (required) + entity: MyLittleTaunTaunBundle\Entity\Planet + # Your Symfony form type (required; used in create and edit action) + form: MyLittleTaunTaunBundle\Form\PlanetType + actions: + # Custom actions + death_star: + title: Destroy a planet + fields: + permissions: [ROLE_DARK_SITH] + # Planets will be retrieved sorted by size and by population + order: + size: getSize + population: ~ + route: app.planets.destroy + route_parameters: {id: ~} + icon: fa fa-planet + load_strategy: unique + # Allowed options are pagerfanta and false + pager: false + criteria: {id: ~} + menu: + top: + items: + destroy_another: + title: Destroy an other planet + route: destroy.again + # Used batch action in list view + batch: true + # Global routing override + routing_url_pattern: custom/planet/{admin}/{action} + routing_name_pattern: tauntaun.{admin}.{action} + # Your custom controller (can extends CRUDController to ease Admin management) + controller: MyLittleTaunTaunBundle:MyController + max_per_page: 5 + # Should implements DataProviderInterface + data_provider: 'my.custom.data_provider.service' + # Translations pattern override + translation_pattern: {key} + # Short configuration reference + tauntaun: + entity: MyLittleTaunTaunBundle\Entity\TaunTaun + form: MyLittleTaunTaunBundle\Entity\TaunTaunType + actions: ~ ``` - -## Documentation - -Full documentation can be found [here](https://github.com/larriereguichet/AdminBundle/tree/master/Resources/docs/summary.md) diff --git a/Resources/config/services.yml b/Resources/config/services.yml index a7469e310..b628814f3 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -46,6 +46,7 @@ services: - '@router' - '@translator' - '@lag.admin.configuration_factory' + - '@twig' # handler lag.admin.message_handler: diff --git a/Resources/views/CRUD/edit.html.twig b/Resources/views/CRUD/edit.html.twig index 3f14f7f21..3f3c39bd4 100644 --- a/Resources/views/CRUD/edit.html.twig +++ b/Resources/views/CRUD/edit.html.twig @@ -45,7 +45,3 @@ {% endblock %} - -{% block javascripts %} - {{ tinymce_init() }} -{% endblock %} diff --git a/Resources/views/admin.layout.html.twig b/Resources/views/admin.layout.html.twig index aa6e9820c..fb82e9775 100644 --- a/Resources/views/admin.layout.html.twig +++ b/Resources/views/admin.layout.html.twig @@ -17,58 +17,63 @@ {% block stylesheets %}{% endblock %} -{% import "LAGAdminBundle:CRUD:macros.html.twig" as AdminBundle %} -{% if admin is not defined %} - {% set admin = null %} -{% endif %} +{% block body %} + {% import "LAGAdminBundle:CRUD:macros.html.twig" as AdminBundle %} -
- {% if app.user %} - + {% endif %} - {% block container %} -
- {# flash messages #} - {% include "LAGAdminBundle:Layout:flash_messages.html.twig" %} + {% block container %} +
+ {# flash messages #} + {% include "LAGAdminBundle:Layout:flash_messages.html.twig" %} -
-
- {% block content %}{% endblock %} +
+
+ {% block content %}{% endblock %} +
-
- {% endblock %} -
- -
- {% include "LAGAdminBundle:Layout:footer.html.twig" %} + {% endblock %} +
+ + {% block javascripts %}{% endblock %} + + +{% endblock %} + diff --git a/Tests/AdminBundle/Admin/AdminTest.php b/Tests/AdminBundle/Admin/AdminTest.php index ae18b9a06..2183a56b5 100644 --- a/Tests/AdminBundle/Admin/AdminTest.php +++ b/Tests/AdminBundle/Admin/AdminTest.php @@ -11,6 +11,7 @@ use stdClass; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\User\User; class AdminTest extends AdminTestBase @@ -82,6 +83,15 @@ public function testHandleRequest() ]); $admin->addAction(new Action('custom_list', $actionConfiguration)); + $this->assertTrue($admin->hasAction('custom_list')); + $this->assertTrue($admin->isActionGranted('custom_list', [ + 'ROLE_ADMIN', + new Role('ROLE_ADMIN') + ])); + $this->assertFalse($admin->isActionGranted('custom_list', [ + 'WRONG_ROLE', + 'IS_AUTHENTICATED_ANONYMOUSLY' + ])); $request = new Request([], [], [ @@ -116,7 +126,7 @@ public function testHandleRequest() } /** - * checkPermissions method SHOULd throw an exception if the permissions are invalid. + * checkPermissions method SHOULD throw an exception if the permissions are invalid. */ public function testCheckPermissions() { @@ -152,7 +162,8 @@ public function testCheckPermissions() ] ]); $user = new User('JohnKrovitch', 'john1234', [ - 'ROLE_USER' + 'ROLE_USER', + new Role('ROLE_USER') ]); $admin->handleRequest($request); $admin->checkPermissions($user); @@ -491,6 +502,8 @@ public function testLoad() $admin->handleRequest($request); $admin->load([]); + + // if an array is returned from the data provider, it SHOULD wrapped into an array collection $this->assertEquals(new ArrayCollection($testEntities), $admin->getEntities()); @@ -519,6 +532,33 @@ public function testLoad() $admin->handleRequest($request); $admin->load([]); + + + // test pagerfanta with multiple load strategy + $admin = new Admin( + 'test', + $dataProvider, + $adminConfiguration, + $this->mockMessageHandler() + ); + $request = new Request([], [], [ + '_route_params' => [ + '_action' => 'custom_list' + ] + ]); + $admin->addAction($this->createAction('custom_list', $admin, [ + 'load_strategy' => AdminInterface::LOAD_STRATEGY_MULTIPLE, + 'route' => '', + 'export' => '', + 'order' => [], + 'icon' => '', + 'pager' => 'pagerfanta', + 'criteria' => [], + ])); + $admin->handleRequest($request); + $admin->load([]); + + // test exception $dataProvider = $this->mockDataProvider(new stdClass()); @@ -548,6 +588,80 @@ public function testLoad() }); } + /** + * load method SHOULD work without a pager. + */ + public function testLoadWithoutPager() + { + $testEntities = [ + new stdClass(), + new stdClass(), + ]; + $dataProvider = $this->mockDataProvider($testEntities); + + $applicationConfiguration = $this->createApplicationConfiguration(); + $adminConfiguration = $this->createAdminConfiguration($applicationConfiguration, $this->getFakeAdminsConfiguration()['full_entity']); + + $admin = new Admin( + 'test', + $dataProvider, + $adminConfiguration, + $this->mockMessageHandler() + ); + $request = new Request([], [], [ + '_route_params' => [ + '_action' => 'custom_list' + ] + ]); + $admin->addAction($this->createAction('custom_list', $admin, [ + 'load_strategy' => AdminInterface::LOAD_STRATEGY_MULTIPLE, + 'route' => '', + 'export' => '', + 'order' => [], + 'icon' => '', + 'pager' => false, + 'criteria' => [], + ])); + $admin->handleRequest($request); + $admin->load([]); + } + + /** + * getCurrentAction method SHOULD throw an exception if no pager is configured. + */ + public function testGetCurrentActionException() + { + $testEntities = [ + new stdClass(), + new stdClass(), + ]; + $dataProvider = $this->mockDataProvider($testEntities); + + $applicationConfiguration = $this->createApplicationConfiguration(); + $adminConfiguration = $this->createAdminConfiguration($applicationConfiguration, $this->getFakeAdminsConfiguration()['full_entity']); + + $admin = new Admin( + 'test', + $dataProvider, + $adminConfiguration, + $this->mockMessageHandler() + ); + $admin->addAction($this->createAction('custom_list', $admin, [ + 'load_strategy' => AdminInterface::LOAD_STRATEGY_MULTIPLE, + 'route' => '', + 'export' => '', + 'order' => [], + 'icon' => '', + 'pager' => false, + 'criteria' => [], + ])); + + $this->assertExceptionRaised(Exception::class, function () use ($admin) { + $admin->getCurrentAction(); + }); + $this->assertFalse($admin->isCurrentActionDefined()); + } + protected function doTestAdmin(AdminInterface $admin, array $configuration, $adminName) { $this->assertEquals($admin->getName(), $adminName); diff --git a/Tests/AdminBundle/Application/Configuration/ApplicationConfigurationTest.php b/Tests/AdminBundle/Application/Configuration/ApplicationConfigurationTest.php index aaaac3899..35b411ff3 100644 --- a/Tests/AdminBundle/Application/Configuration/ApplicationConfigurationTest.php +++ b/Tests/AdminBundle/Application/Configuration/ApplicationConfigurationTest.php @@ -6,6 +6,7 @@ use LAG\AdminBundle\Field\Field; use LAG\AdminBundle\Tests\AdminTestBase; use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; class ApplicationConfigurationTest extends AdminTestBase { @@ -31,7 +32,7 @@ public function testConfigureOptions() 'string_length_truncate' => '....', 'routing' => [ 'url_pattern' => '/{admin}/{action}', - 'name_pattern' => 'lag.admin.{admin}', + 'name_pattern' => 'lag.admin.{admin}.{action}', ], 'translation' => [ 'enabled' => true, @@ -54,7 +55,7 @@ public function testConfigureOptions() $this->assertEquals('d/m/YYYY', $applicationConfiguration->getParameter('date_format')); $this->assertEquals(100, $applicationConfiguration->getParameter('string_length')); $this->assertEquals('....', $applicationConfiguration->getParameter('string_length_truncate')); - $this->assertEquals('lag.admin.{admin}', $applicationConfiguration->getParameter('routing')['name_pattern']); + $this->assertEquals('lag.admin.{admin}.{action}', $applicationConfiguration->getParameter('routing')['name_pattern']); $this->assertEquals('/{admin}/{action}', $applicationConfiguration->getParameter('routing')['url_pattern']); $this->assertEquals('lag.admin.{key}', $applicationConfiguration->getParameter('translation')['pattern']); $this->assertEquals(25, $applicationConfiguration->getParameter('max_per_page')); @@ -69,5 +70,61 @@ public function testConfigureOptions() Field::TYPE_COLLECTION => 'LAG\AdminBundle\Field\Field\Collection', Field::TYPE_BOOLEAN => 'LAG\AdminBundle\Field\Field\Boolean', ], $applicationConfiguration->getParameter('fields_mapping')); + + // test exception raising + $this->assertExceptionRaised(InvalidOptionsException::class, function() use ($resolver) { + $resolver->resolve([ + 'routing' => [ + 'url_pattern' => '/wrong/{action}', + ], + ]); + }); + $this->assertExceptionRaised(InvalidOptionsException::class, function() use ($resolver) { + $resolver->resolve([ + 'routing' => [ + 'url_pattern' => '/{admin}/wrong', + ], + ]); + }); + $this->assertExceptionRaised(InvalidOptionsException::class, function() use ($resolver) { + $resolver->resolve([ + 'routing' => [ + 'name_pattern' => 'wrong.{action}', + ], + ]); + }); + $this->assertExceptionRaised(InvalidOptionsException::class, function() use ($resolver) { + $resolver->resolve([ + 'routing' => [ + 'name_pattern' => '{admin}.wrong', + ], + ]); + }); + $this->assertExceptionRaised(InvalidOptionsException::class, function() use ($resolver) { + $resolver->resolve([ + 'translation' => [ + 'enabled' => 'true', + ], + ]); + }); + $this->assertExceptionRaised(InvalidOptionsException::class, function() use ($resolver) { + $resolver->resolve([ + 'translation' => [ + ], + ]); + }); + $resolver->resolve([ + 'translation' => [ + 'enabled' => true + ], + ]); + $this->assertExceptionRaised(InvalidOptionsException::class, function() use ($resolver) { + $resolver->resolve([ + 'translation' => [ + 'enabled' => true, + 'pattern' => 'wrong_pattern' + ], + ]); + }); } } diff --git a/Twig/AdminExtension.php b/Twig/AdminExtension.php index 35a64a0e3..4721e94f3 100644 --- a/Twig/AdminExtension.php +++ b/Twig/AdminExtension.php @@ -12,6 +12,7 @@ use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\PropertyAccess\PropertyAccess; use Symfony\Component\Translation\TranslatorInterface; +use Twig_Environment; use Twig_Extension; use Twig_SimpleFilter; use Twig_SimpleFunction; @@ -42,21 +43,29 @@ class AdminExtension extends Twig_Extension */ protected $translator; + /** + * @var Twig_Environment + */ + protected $twig; + /** * AdminExtension constructor. * * @param RouterInterface $router * @param TranslatorInterface $translator * @param ConfigurationFactory $configurationFactory + * @param Twig_Environment $twig */ public function __construct( RouterInterface $router, TranslatorInterface $translator, - ConfigurationFactory $configurationFactory + ConfigurationFactory $configurationFactory, + Twig_Environment $twig ) { $this->router = $router; $this->translator = $translator; $this->configuration = $configurationFactory->getApplicationConfiguration(); + $this->twig = $twig; } /** @@ -70,6 +79,7 @@ public function getFunctions() new Twig_SimpleFunction('field', [$this, 'field']), new Twig_SimpleFunction('field_title', [$this, 'fieldTitle']), new Twig_SimpleFunction('route_parameters', [$this, 'routeParameters']), + new Twig_SimpleFunction('function_exists', [$this, 'functionExists']), ]; } @@ -116,6 +126,7 @@ public function getSortColumnUrl(Request $request, $fieldName) * * @param ParameterBagInterface $parameters * @param $fieldName + * * @return array */ public function getOrderQueryString(ParameterBagInterface $parameters, $fieldName) @@ -137,6 +148,7 @@ public function getOrderQueryString(ParameterBagInterface $parameters, $fieldNam * @param null $order * @param $fieldName * @param $sort + * * @return string */ public function getSortColumnIconClass($order = null, $fieldName, $sort) @@ -161,7 +173,7 @@ public function getSortColumnIconClass($order = null, $fieldName, $sort) * @param FieldInterface $field * @param $entity * - * @return mixed + * @return string */ public function field(FieldInterface $field, $entity) { @@ -169,6 +181,7 @@ public function field(FieldInterface $field, $entity) ->enableMagicCall() ->getPropertyAccessor(); $value = null; + // if name starts with a underscore, it is a custom field, not mapped to the entity if (substr($field->getName(), 0, 1) != '_') { // get raw value from object @@ -187,6 +200,7 @@ public function field(FieldInterface $field, $entity) * * @param $fieldName * @param null $adminName + * * @return string */ public function fieldTitle($fieldName, $adminName = null) @@ -205,6 +219,7 @@ public function fieldTitle($fieldName, $adminName = null) /** * @param array $parameters * @param $entity + * * @return array */ public function routeParameters(array $parameters, $entity) @@ -226,6 +241,7 @@ public function routeParameters(array $parameters, $entity) * Camelize a string (using Container camelize method) * * @param $string + * * @return string */ public function camelize($string) @@ -242,4 +258,18 @@ public function getName() { return 'lag.admin'; } + + /** + * Return true if the method exists in twig. + * + * @param string $functionName + * + * @return bool + */ + public function functionExists($functionName) + { + return false !== $this + ->twig + ->getFunction($functionName); + } }