diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b1af21fb0..eaa439de9 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,19 +16,11 @@ stages: - build-doc - deploy-doc - - -test:php-7.1: - stage: test - <<: *test_definition - image: chill/ci-image:php-7.1 - script: vendor/bin/phpunit - test:php-7.2: stage: test <<: *test_definition image: chill/ci-image:php-7.2 - script: vendor/bin/phpunit + script: APP_ENV=test vendor/bin/phpunit # deploy documentation api-doc-build: diff --git a/ChillMainBundle.php b/ChillMainBundle.php index 579908ea4..7e6620522 100644 --- a/ChillMainBundle.php +++ b/ChillMainBundle.php @@ -10,6 +10,8 @@ use Chill\MainBundle\DependencyInjection\TimelineCompilerClass; use Chill\MainBundle\DependencyInjection\RoleProvidersCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\ExportsCompilerPass; use Chill\MainBundle\DependencyInjection\CompilerPass\WidgetsCompilerPass; +use Chill\MainBundle\DependencyInjection\CompilerPass\NotificationCounterCompilerPass; +use Chill\MainBundle\DependencyInjection\CompilerPass\MenuCompilerPass; class ChillMainBundle extends Bundle @@ -23,5 +25,7 @@ class ChillMainBundle extends Bundle $container->addCompilerPass(new RoleProvidersCompilerPass()); $container->addCompilerPass(new ExportsCompilerPass()); $container->addCompilerPass(new WidgetsCompilerPass()); + $container->addCompilerPass(new NotificationCounterCompilerPass()); + $container->addCompilerPass(new MenuCompilerPass()); } } diff --git a/Controller/ExportController.php b/Controller/ExportController.php index c25a25e03..6d9e79199 100644 --- a/Controller/ExportController.php +++ b/Controller/ExportController.php @@ -412,9 +412,29 @@ class ExportController extends Controller } public function downloadResultAction(Request $request, $alias) - { - return $this->render("ChillMainBundle:Export:download.html.twig", [ + { + /* @var $exportManager \Chill\MainBundle\Export\ExportManager */ + $exportManager = $this->get('chill.main.export_manager'); + $formCenters = $this->createCreateFormExport($alias, 'generate_centers'); + $formCenters->handleRequest($request); + $dataCenters = $formCenters->getData(); + + $formExport = $this->createCreateFormExport($alias, 'generate_export', $dataCenters); + $formExport->handleRequest($request); + $dataExport = $formExport->getData(); + + $formatterAlias = $exportManager->getFormatterAlias($dataExport['export']); + $formater = $exportManager->getFormatter($formatterAlias); + + $viewVariables = [ 'alias' => $alias - ]); + ]; + + if ($formater instanceof \Chill\MainBundle\Export\Formatter\CSVListFormatter) { + // due to a bug in php, we add the mime type in the download view + $viewVariables['mime_type'] = 'text/csv'; + } + + return $this->render("ChillMainBundle:Export:download.html.twig", $viewVariables); } } diff --git a/Controller/LoginController.php b/Controller/LoginController.php index 72f8a28fb..046b78678 100644 --- a/Controller/LoginController.php +++ b/Controller/LoginController.php @@ -5,41 +5,34 @@ namespace Chill\MainBundle\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\Security\Core\SecurityContextInterface; +use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; class LoginController extends Controller { /** + * + * @var AuthenticationUtils + */ + protected $helper; + + public function __construct(AuthenticationUtils $helper) + { + $this->helper = $helper; + } + + /** + * Show a login form * - * @todo Improve this with http://symfony.com/blog/new-in-symfony-2-6-security-component-improvements#added-a-security-error-helper * @param Request $request * @return Response */ public function loginAction(Request $request) { - - $session = $request->getSession(); - - if ($request->attributes->has(SecurityContextInterface::AUTHENTICATION_ERROR)) { - $error = $request->attributes->get( - SecurityContextInterface::AUTHENTICATION_ERROR - ); - } elseif (null !== $session && $session->has(SecurityContextInterface::AUTHENTICATION_ERROR)) { - $error = $session->get(SecurityContextInterface::AUTHENTICATION_ERROR); - $session->remove(SecurityContextInterface::AUTHENTICATION_ERROR); - } else { - $error = ''; - } - - $lastUsername = (null === $session) ? - '' : $session->get(SecurityContextInterface::LAST_USERNAME); - return $this->render('ChillMainBundle:Login:login.html.twig', array( - 'last_username' => $lastUsername, - 'error' => (empty($error)) ? $error : $error->getMessage() + 'last_username' => $this->helper->getLastUsername(), + 'error' => $this->helper->getLastAuthenticationError() )); - } public function LoginCheckAction(Request $request) diff --git a/Controller/PermissionsGroupController.php b/Controller/PermissionsGroupController.php index 911fff79b..336cab5d5 100644 --- a/Controller/PermissionsGroupController.php +++ b/Controller/PermissionsGroupController.php @@ -11,6 +11,7 @@ use Chill\MainBundle\Form\PermissionsGroupType; use Symfony\Component\Security\Core\Role\Role; use Symfony\Component\Security\Core\Role\RoleInterface; use Chill\MainBundle\Entity\Scope; +use Chill\MainBundle\Form\Type\ComposedRoleScopeType; /** * PermissionsGroup controller. @@ -491,7 +492,7 @@ class PermissionsGroupController extends Controller ->setAction($this->generateUrl('admin_permissionsgroup_add_role_scope', array('id' => $permissionsGroup->getId()))) ->setMethod('PUT') - ->add('composed_role_scope', 'composed_role_scope') + ->add('composed_role_scope', ComposedRoleScopeType::class) ->add('submit', SubmitType::class, array('label' => 'Add permission')) ->getForm() ; diff --git a/Controller/PostalCodeController.php b/Controller/PostalCodeController.php new file mode 100644 index 000000000..cd7dd9ace --- /dev/null +++ b/Controller/PostalCodeController.php @@ -0,0 +1,89 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\Request; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use Chill\MainBundle\Entity\PostalCode; +use Symfony\Component\HttpFoundation\JsonResponse; +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Doctrine\ORM\Query; + +/** + * + * + * @author Julien Fastré + */ +class PostalCodeController extends Controller +{ + /** + * + * @var TranslatableStringHelper + */ + protected $translatableStringHelper; + + public function __construct(TranslatableStringHelper $translatableStringHelper) + { + $this->translatableStringHelper = $translatableStringHelper; + } + + + /** + * + * @Route( + * "{_locale}/postalcode/search" + * ) + * @param Request $request + * @return JsonResponse + */ + public function searchAction(Request $request) + { + $pattern = $request->query->getAlnum('q', ''); + + if (empty($pattern)) { + return new JsonResponse(["results" => [], "pagination" => [ "more" => false]]); + } + + $query = $this->getDoctrine()->getManager() + ->createQuery(sprintf( + "SELECT p.id AS id, p.name AS name, p.code AS code, " + . "country.name AS country_name, " + . "country.countryCode AS country_code " + . "FROM %s p " + . "JOIN p.country country " + . "WHERE LOWER(p.name) LIKE LOWER(:pattern) OR LOWER(p.code) LIKE LOWER(:pattern) " + . "ORDER BY code" + , PostalCode::class) + ) + ->setParameter('pattern', '%'.$pattern.'%') + ->setMaxResults(30) + ; + + $results = \array_map(function($row) { + $row['country_name'] = $this->translatableStringHelper->localize($row['country_name']); + $row['text'] = $row['code']." ".$row["name"]." (".$row['country_name'].")"; + + return $row; + }, $query->getResult(Query::HYDRATE_ARRAY)); + + return new JsonResponse([ 'results' => $results, "pagination" => [ "more" => false ] ]); + } +} diff --git a/Controller/UIController.php b/Controller/UIController.php new file mode 100644 index 000000000..07b3694f8 --- /dev/null +++ b/Controller/UIController.php @@ -0,0 +1,39 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Chill\MainBundle\Templating\UI\CountNotificationUser; + +/** + * + * + * @author Julien Fastré + */ +class UIController extends Controller +{ + public function showNotificationUserCounterAction( + CountNotificationUser $counter + ) { + $nb = $counter->getSumNotification($this->getUser()); + + return $this->render('ChillMainBundle:UI:notification_user_counter.html.twig', [ + 'nb' => $nb + ]); + } +} diff --git a/DependencyInjection/ChillMainExtension.php b/DependencyInjection/ChillMainExtension.php index c2f63761f..df0686f34 100644 --- a/DependencyInjection/ChillMainExtension.php +++ b/DependencyInjection/ChillMainExtension.php @@ -29,6 +29,7 @@ use Chill\MainBundle\DependencyInjection\Configuration; use Chill\MainBundle\Doctrine\DQL\GetJsonFieldByKey; use Chill\MainBundle\Doctrine\DQL\Unaccent; use Chill\MainBundle\Doctrine\DQL\JsonAggregate; +use Chill\MainBundle\Doctrine\DQL\JsonbExistsInArray; /** * This class load config for chillMainExtension. @@ -92,7 +93,13 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, $loader->load('services/pagination.yml'); $loader->load('services/export.yml'); $loader->load('services/form.yml'); - + $loader->load('services/validator.yml'); + $loader->load('services/widget.yml'); + $loader->load('services/controller.yml'); + $loader->load('services/routing.yml'); + $loader->load('services/fixtures.yml'); + $loader->load('services/menu.yml'); + $loader->load('services/security.yml'); } public function getConfiguration(array $config, ContainerBuilder $container) @@ -136,11 +143,23 @@ class ChillMainExtension extends Extension implements PrependExtensionInterface, 'unaccent' => Unaccent::class, 'GET_JSON_FIELD_BY_KEY' => GetJsonFieldByKey::class, 'AGGREGATE' => JsonAggregate::class - ) + ), + 'numeric_functions' => [ + 'JSONB_EXISTS_IN_ARRAY' => JsonbExistsInArray::class + ] ) ) )); + //add dbal types (default entity_manager) + $container->prependExtensionConfig('doctrine', array( + 'dbal' => [ + 'types' => [ + 'dateinterval' => \Chill\MainBundle\Doctrine\Type\NativeDateIntervalType::class + ] + ] + )); + //add current route to chill main $container->prependExtensionConfig('chill_main', array( 'routing' => array( diff --git a/DependencyInjection/CompilerPass/ExportsCompilerPass.php b/DependencyInjection/CompilerPass/ExportsCompilerPass.php index dc4d1805d..3b4b2069e 100644 --- a/DependencyInjection/CompilerPass/ExportsCompilerPass.php +++ b/DependencyInjection/CompilerPass/ExportsCompilerPass.php @@ -31,6 +31,7 @@ use Symfony\Component\DependencyInjection\Definition; * - chill.export_formatter * - chill.export_aggregator * - chill.export_filter + * - chill.export_elements_provider * * * @author Julien Fastré @@ -53,6 +54,7 @@ class ExportsCompilerPass implements CompilerPassInterface $this->compileFilters($chillManagerDefinition, $container); $this->compileAggregators($chillManagerDefinition, $container); $this->compileFormatters($chillManagerDefinition, $container); + $this->compileExportElementsProvider($chillManagerDefinition, $container); } private function compileExports(Definition $chillManagerDefinition, @@ -174,5 +176,35 @@ class ExportsCompilerPass implements CompilerPassInterface } } } + + private function compileExportElementsProvider(Definition $chillManagerDefinition, + ContainerBuilder $container) + { + $taggedServices = $container->findTaggedServiceIds( + 'chill.export_elements_provider' + ); + + $knownAliases = array(); + + foreach ($taggedServices as $id => $tagAttributes) { + foreach ($tagAttributes as $attributes) { + if (!isset($attributes["prefix"])) { + throw new \LogicException("the 'prefix' attribute is missing in your ". + "service '$id' definition"); + } + + if (array_search($attributes["prefix"], $knownAliases)) { + throw new \LogicException("There is already a chill.export_elements_provider service with prefix " + .$attributes["prefix"].". Choose another prefix."); + } + $knownAliases[] = $attributes["prefix"]; + + $chillManagerDefinition->addMethodCall( + 'addExportElementsProvider', + array(new Reference($id), $attributes["prefix"]) + ); + } + } + } } diff --git a/DependencyInjection/CompilerPass/MenuCompilerPass.php b/DependencyInjection/CompilerPass/MenuCompilerPass.php new file mode 100644 index 000000000..b6b66a755 --- /dev/null +++ b/DependencyInjection/CompilerPass/MenuCompilerPass.php @@ -0,0 +1,49 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\DependencyInjection\CompilerPass; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Reference; +use Chill\MainBundle\Routing\MenuComposer; + +/** + * + * + * @author Julien Fastré + */ +class MenuCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('chill.main.menu_composer')) { + throw new \LogicException(sprintf("The service %s does not exists in " + . "container.", MenuComposer::class)); + } + + $menuComposerDefinition = $container->getDefinition('chill.main.menu_composer'); + + foreach ($container->findTaggedServiceIds('chill.menu_builder') as $id => $tags) { + $class = $container->getDefinition($id)->getClass(); + foreach ($class::getMenuIds() as $menuId) { + $menuComposerDefinition + ->addMethodCall('addLocalMenuBuilder', [new Reference($id), $menuId]); + } + } + } +} diff --git a/DependencyInjection/CompilerPass/NotificationCounterCompilerPass.php b/DependencyInjection/CompilerPass/NotificationCounterCompilerPass.php new file mode 100644 index 000000000..941aeeb7a --- /dev/null +++ b/DependencyInjection/CompilerPass/NotificationCounterCompilerPass.php @@ -0,0 +1,46 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\DependencyInjection\CompilerPass; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Chill\MainBundle\Templating\UI\CountNotificationUser; +use Symfony\Component\DependencyInjection\Reference; + +/** + * + * + * @author Julien Fastré + */ +class NotificationCounterCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition(CountNotificationUser::class)) { + throw new \LogicException("The service ".CountNotificationUser::class." " + . "should be defined"); + } + + $notificationCounterDefinition = $container->getDefinition(CountNotificationUser::class); + + foreach ($container->findTaggedServiceIds('chill.count_notification.user') as $id => $tags) { + $notificationCounterDefinition + ->addMethodCall('addNotificationCounter', [new Reference($id)]); + } + } +} diff --git a/Doctrine/DQL/JsonbExistsInArray.php b/Doctrine/DQL/JsonbExistsInArray.php new file mode 100644 index 000000000..6c4eaa5c3 --- /dev/null +++ b/Doctrine/DQL/JsonbExistsInArray.php @@ -0,0 +1,41 @@ + + */ +class JsonbExistsInArray extends FunctionNode +{ + private $expr1; + private $expr2; + + public function getSql(SqlWalker $sqlWalker): string + { + return sprintf( + 'jsonb_exists(%s, %s)', + $this->expr1->dispatch($sqlWalker), + $sqlWalker->walkInputParameter($this->expr2) + ); + } + + public function parse(Parser $parser): void + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + $this->expr1 = $parser->StringPrimary(); + $parser->match(Lexer::T_COMMA); + $this->expr2 = $parser->InputParameter(); + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} diff --git a/Doctrine/Type/NativeDateIntervalType.php b/Doctrine/Type/NativeDateIntervalType.php new file mode 100644 index 000000000..bc4938e97 --- /dev/null +++ b/Doctrine/Type/NativeDateIntervalType.php @@ -0,0 +1,111 @@ + + */ +class NativeDateIntervalType extends DateIntervalType +{ + const FORMAT = '%rP%YY%MM%DDT%HH%IM%SS'; + + public function getName(): string + { + return \Doctrine\DBAL\Types\Type::DATEINTERVAL; + } + + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string + { + return 'INTERVAL'; + } + + /** + * {@inheritdoc} + */ + public function convertToDatabaseValue($value, AbstractPlatform $platform) + { + if (null === $value) { + return null; + } + + if ($value instanceof \DateInterval) { + return $value->format(self::FORMAT); + } + + throw ConversionException::conversionFailedInvalidType($value, $this->getName(), ['null', 'DateInterval']); + } + + public function convertToPHPValue($value, AbstractPlatform $platform) + { + if ($value === null || $value instanceof \DateInterval) { + return $value; + } + + try { + $strings = explode(' ', $value); + + if (count($strings) === 0) { + return null; + } + $intervalSpec = 'P'; + \reset($strings); + + do { + $intervalSpec .= $this->convertEntry($strings); + } while (next($strings) !== FALSE); + + return new \DateInterval($intervalSpec); + } catch (\Exception $exception) { + throw $this->createConversionException($value, $exception); + } + } + + private function convertEntry(&$strings) + { + $current = \current($strings); + + if (is_numeric($current)) { + $next = \next($strings); + switch($next) { + case 'year': + case 'years': + $unit = 'Y'; + break; + case 'mon': + case 'mons': + $unit = 'M'; + break; + case 'day': + case 'days': + $unit = 'D'; + break; + default: + throw $this->createConversionException(implode('', $strings)); + } + + return $current.$unit; + + } elseif (\preg_match('/([0-9]{2}\:[0-9]{2}:[0-9]{2})/', $v) === 1) { + $tExploded = explode(':', $v); + $intervalSpec = 'T'; + $intervalSpec.= $tExploded[0].'H'; + $intervalSpec.= $tExploded[1].'M'; + $intervalSpec.= $tExploded[2].'S'; + + return $intervalSpec; + } + } + + protected function createConversionException($value, $exception = null) + { + return ConversionException::conversionFailedFormat($value, $this->getName(), 'xx year xx mons xx days 01:02:03', $exception); + } + +} diff --git a/Entity/Address.php b/Entity/Address.php index 792535a17..49951e48f 100644 --- a/Entity/Address.php +++ b/Entity/Address.php @@ -141,6 +141,15 @@ class Address return $this; } + public static function createFromAddress(Address $original) : Address + { + return (new Address()) + ->setPostcode($original->getPostcode()) + ->setStreetAddress1($original->getStreetAddress1()) + ->setStreetAddress2($original->getStreetAddress2()) + ->setValidFrom($original->getValidFrom()) + ; + } } diff --git a/Entity/User.php b/Entity/User.php index 13f49b59e..c814d7a01 100644 --- a/Entity/User.php +++ b/Entity/User.php @@ -22,6 +22,24 @@ class User implements AdvancedUserInterface { */ private $username; + /** + * + * @var string + */ + private $usernameCanonical; + + /** + * + * @var string + */ + private $email; + + /** + * + * @var string + */ + private $emailCanonical; + /** * * @var string @@ -115,9 +133,47 @@ class User implements AdvancedUserInterface { return $this->username; } + public function getUsernameCanonical() + { + return $this->usernameCanonical; + } + + public function getEmail() + { + return $this->email; + } + + public function getEmailCanonical() + { + return $this->emailCanonical; + } + + public function setUsernameCanonical($usernameCanonical) + { + $this->usernameCanonical = $usernameCanonical; + + return $this; + } + + public function setEmail($email) + { + $this->email = $email; + + return $this; + } + + public function setEmailCanonical($emailCanonical) + { + $this->emailCanonical = $emailCanonical; + + return $this; + } + + function setPassword($password) { $this->password = $password; + return $this; } diff --git a/Export/ExportElementsProviderInterface.php b/Export/ExportElementsProviderInterface.php new file mode 100644 index 000000000..077272c9b --- /dev/null +++ b/Export/ExportElementsProviderInterface.php @@ -0,0 +1,35 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Export; + +/** + * Interface to provide export elements dynamically. + * + * The typical use case is providing exports or aggregators depending on + * dynamic data. Example: providing exports for reports, reports depending + * on data stored in database. + * + * @author Julien Fastré + */ +interface ExportElementsProviderInterface +{ + /** + * @return ExportElementInterface[] + */ + public function getExportElements(); +} diff --git a/Export/ExportManager.php b/Export/ExportManager.php index 85c2b4280..a46054352 100644 --- a/Export/ExportManager.php +++ b/Export/ExportManager.php @@ -169,6 +169,26 @@ class ExportManager $this->formatters[$alias] = $formatter; } + public function addExportElementsProvider(ExportElementsProviderInterface $provider, $prefix) + { + foreach ($provider->getExportElements() as $suffix => $element) { + $alias = $prefix.'_'.$suffix; + + if ($element instanceof ExportInterface) { + $this->addExport($element, $alias); + } elseif ($element instanceof FilterInterface) { + $this->addFilter($element, $alias); + } elseif ($element instanceof AggregatorInterface) { + $this->addAggregator($element, $alias); + } elseif ($element instanceof FormatterInterface) { + $this->addFormatter($element, $alias); + } else { + throw new \LogicException("This element ".\get_class($element)." " + . "is not an instance of export element"); + } + } + } + /** * * @return string[] the existing type for known exports @@ -437,16 +457,16 @@ class ExportManager $result = $export->getResult($query, $data[ExportType::EXPORT_KEY]); - if (!is_array($result)) { + if (!is_iterable($result)) { throw new \UnexpectedValueException( sprintf( - 'The result of the export should be an array, %s given', + 'The result of the export should be an iterable, %s given', gettype($result) ) ); } - /* @var $formatter Formatter\CSVFormatter */ + /* @var $formatter FormatterInterface */ $formatter = $this->getFormatter($this->getFormatterAlias($data)); $filtersData = array(); $aggregatorsData = array(); diff --git a/Form/ChoiceLoader/PostalCodeChoiceLoader.php b/Form/ChoiceLoader/PostalCodeChoiceLoader.php new file mode 100644 index 000000000..71ebbd919 --- /dev/null +++ b/Form/ChoiceLoader/PostalCodeChoiceLoader.php @@ -0,0 +1,85 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Form\ChoiceLoader; + +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Chill\MainBundle\Repository\PostalCodeRepository; +use Symfony\Component\Form\ChoiceList\LazyChoiceList; +use Chill\MainBundle\Entity\PostalCode; + +/** + * + * + * @author Julien Fastré + */ +class PostalCodeChoiceLoader implements ChoiceLoaderInterface +{ + /** + * + * @var PostalCodeRepository + */ + protected $postalCodeRepository; + + protected $lazyLoadedPostalCodes = []; + + public function __construct(PostalCodeRepository $postalCodeRepository) + { + $this->postalCodeRepository = $postalCodeRepository; + } + + public function loadChoiceList($value = null): ChoiceListInterface + { + $list = new \Symfony\Component\Form\ChoiceList\ArrayChoiceList( + $this->lazyLoadedPostalCodes, + function(PostalCode $pc) use ($value) { + return \call_user_func($value, $pc); + }); + + return $list; + } + + public function loadChoicesForValues(array $values, $value = null) + { + $choices = []; + + foreach($values as $value) { + $choices[] = $this->postalCodeRepository->find($value); + } + + return $choices; + } + + public function loadValuesForChoices(array $choices, $value = null) + { + $values = []; + + foreach ($choices as $choice) { + if (NULL === $choice) { + $values[] = null; + continue; + } + + $id = \call_user_func($value, $choice); + $values[] = $id; + $this->lazyLoadedPostalCodes[$id] = $choice; + } + + return $values; + } +} diff --git a/Form/Type/AddressType.php b/Form/Type/AddressType.php index 173bd9b70..ee15e3d20 100644 --- a/Form/Type/AddressType.php +++ b/Form/Type/AddressType.php @@ -23,6 +23,9 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Form\Type\PostalCodeType; /** * A type to create/update Address entity @@ -46,12 +49,17 @@ class AddressType extends AbstractType 'placeholder' => 'Choose a postal code', 'required' => true )) - ->add('validFrom', 'date', array( - 'required' => true, - 'widget' => 'single_text', + ->add('validFrom', DateType::class, array( + 'required' => true, + 'widget' => 'single_text', 'format' => 'dd-MM-yyyy' ) ) ; } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefault('data_class', Address::class); + } } diff --git a/Form/Type/ChillCollectionType.php b/Form/Type/ChillCollectionType.php new file mode 100644 index 000000000..1068522c9 --- /dev/null +++ b/Form/Type/ChillCollectionType.php @@ -0,0 +1,61 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Form\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormView; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Form\FormInterface; + +/** + * Available options : + * + * - `button_add_label` + * - `button_remove_label` + * - `identifier`: an identifier to identify the kind of collecton. Useful if some + * javascript should be launched associated to `add_entry`, `remove_entry` events. + * + * + * @author Julien Fastré + */ +class ChillCollectionType extends AbstractType +{ + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefaults([ + 'button_add_label' => 'Add an entry', + 'button_remove_label' => 'Remove entry', + 'identifier' => '' + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['button_add_label'] = $options['button_add_label']; + $view->vars['button_remove_label'] = $options['button_remove_label']; + $view->vars['allow_delete'] = (int) $options['allow_delete']; + $view->vars['allow_add'] = (int) $options['allow_add']; + $view->vars['identifier'] = $options['identifier']; + } + + public function getParent() + { + return \Symfony\Component\Form\Extension\Core\Type\CollectionType::class; + } +} diff --git a/Form/Type/DataTransformer/DateIntervalTransformer.php b/Form/Type/DataTransformer/DateIntervalTransformer.php new file mode 100644 index 000000000..29a81447c --- /dev/null +++ b/Form/Type/DataTransformer/DateIntervalTransformer.php @@ -0,0 +1,92 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Form\Type\DataTransformer; + +use Symfony\Component\Form\DataTransformerInterface; +use Symfony\Component\Form\Exception\TransformationFailedException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; + +/** + * + * + * @author Julien Fastré + */ +class DateIntervalTransformer implements DataTransformerInterface +{ + /** + * + * @param \DateInterval $value + * @throws UnexpectedTypeException + */ + public function transform($value) + { + if (empty($value)) { + return null; + } + + if (!$value instanceof \DateInterval) { + throw new UnexpectedTypeException($value, \DateInterval::class); + } + + if ($value->d > 0) { + // we check for weeks (weeks are converted to 7 days) + if ($value->d % 7 === 0) { + return [ + 'n' => $value->d / 7, + 'unit' => 'W' + ]; + } else { + return [ + 'n' => $value->d, + 'unit' => 'D' + ]; + } + } elseif ($value->m > 0) { + return [ + 'n' => $value->m, + 'unit' => 'M' + ]; + } elseif ($value->y > 0) { + return [ + 'n' => $value->y, + 'unit' => 'Y' + ]; + } + + throw new TransformationFailedException('the date interval does not ' + . 'contains any days, months or years'); + } + + public function reverseTransform($value) + { + if (empty($value) or empty($value['n'])) { + return null; + } + + $string = 'P'.$value['n'].$value['unit']; + + try { + return new \DateInterval($string); + } catch (\Exception $e) { + throw new TransformationFailedException("Could not transform value " + . "into DateInterval", 1542, $e); + } + + + } +} diff --git a/Form/Type/DateIntervalType.php b/Form/Type/DateIntervalType.php new file mode 100644 index 000000000..499c61f93 --- /dev/null +++ b/Form/Type/DateIntervalType.php @@ -0,0 +1,57 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Form\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Chill\MainBundle\Form\Type\DataTransformer\DateIntervalTransformer; +use Symfony\Component\Validator\Constraints\GreaterThan; + +/** + * + * + */ +class DateIntervalType extends AbstractType +{ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('n', IntegerType::class, [ + 'scale' => 0, + 'constraints' => [ + new GreaterThan([ + 'value' => 0 + ]) + ] + ]) + ->add('unit', ChoiceType::class, [ + 'choices' => [ + 'Days' => 'D', + 'Weeks' => 'W', + 'Months' => 'M', + 'Years' => 'Y' + ], + 'choices_as_values' => true + ]) + ; + + $builder->addModelTransformer(new DateIntervalTransformer()); + } +} diff --git a/Form/Type/Export/ExportType.php b/Form/Type/Export/ExportType.php index 36a72d0c2..c8410f231 100644 --- a/Form/Type/Export/ExportType.php +++ b/Form/Type/Export/ExportType.php @@ -79,8 +79,6 @@ class ExportType extends AbstractType $filters = $this->exportManager->getFiltersApplyingOn($export, $options['picked_centers']); $filterBuilder = $builder->create(self::FILTER_KEY, FormType::class, array('compound' => true)); - dump($this->exportManager); - foreach($filters as $alias => $filter) { $filterBuilder->add($alias, FilterType::class, array( 'filter_alias' => $alias, diff --git a/Form/Type/Export/PickFormatterType.php b/Form/Type/Export/PickFormatterType.php index 8ac039c34..88a0b7ba8 100644 --- a/Form/Type/Export/PickFormatterType.php +++ b/Form/Type/Export/PickFormatterType.php @@ -54,7 +54,7 @@ class PickFormatterType extends AbstractType } $builder->add('alias', ChoiceType::class, array( - 'choices' => array_combine(array_values($choices),array_keys($choices)), + 'choices' => $choices, 'choices_as_values' => true, 'multiple' => false )); diff --git a/Form/Type/PostalCodeType.php b/Form/Type/PostalCodeType.php index bdaae6bc9..1cbbc549d 100644 --- a/Form/Type/PostalCodeType.php +++ b/Form/Type/PostalCodeType.php @@ -21,10 +21,14 @@ namespace Chill\MainBundle\Form\Type; use Symfony\Component\Form\AbstractType; use Symfony\Component\OptionsResolver\OptionsResolver; -use Symfony\Component\Form\FormBuilderInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Entity\PostalCode; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\Form\FormView; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Chill\MainBundle\Form\ChoiceLoader\PostalCodeChoiceLoader; +use Symfony\Component\Translation\TranslatorInterface; /** * A form to pick between PostalCode @@ -39,10 +43,35 @@ class PostalCodeType extends AbstractType * @var TranslatableStringHelper */ protected $translatableStringHelper; + + /** + * + * @var UrlGeneratorInterface + */ + protected $urlGenerator; + + /** + * + * @var PostalCodeChoiceLoader + */ + protected $choiceLoader; + + /** + * + * @var TranslatorInterface + */ + protected $translator; - public function __construct(TranslatableStringHelper $helper) - { + public function __construct( + TranslatableStringHelper $helper, + UrlGeneratorInterface $urlGenerator, + PostalCodeChoiceLoader $choiceLoader, + TranslatorInterface $translator + ) { $this->translatableStringHelper = $helper; + $this->urlGenerator = $urlGenerator; + $this->choiceLoader = $choiceLoader; + $this->translator = $translator; } @@ -55,11 +84,26 @@ class PostalCodeType extends AbstractType { // create a local copy for usage in Closure $helper = $this->translatableStringHelper; - $resolver->setDefault('class', PostalCode::class) + $resolver + ->setDefault('class', PostalCode::class) ->setDefault('choice_label', function(PostalCode $code) use ($helper) { return $code->getCode().' '.$code->getName().' ['. $helper->localize($code->getCountry()->getName()).']'; - } - ); + }) + ->setDefault('choice_loader', $this->choiceLoader) + ->setDefault('placeholder', 'Select a postal code') + ; } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['attr']['data-postal-code'] = 'data-postal-code'; + $view->vars['attr']['data-search-url'] = $this->urlGenerator + ->generate('chill_main_postal_code_search'); + $view->vars['attr']['data-placeholder'] = $this->translator->trans($options['placeholder']); + $view->vars['attr']['data-no-results-label'] = $this->translator->trans('select2.no_results'); + $view->vars['attr']['data-error-load-label'] = $this->translator->trans('select2.error_loading'); + $view->vars['attr']['data-searching-label'] = $this->translator->trans('select2.searching'); + } + } diff --git a/Form/Type/ScopePickerType.php b/Form/Type/ScopePickerType.php index 963cbb7b9..dbbc28255 100644 --- a/Form/Type/ScopePickerType.php +++ b/Form/Type/ScopePickerType.php @@ -26,6 +26,8 @@ use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Chill\MainBundle\Entity\Scope; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Chill\MainBundle\Templating\TranslatableStringHelper; +use Chill\MainBundle\Entity\Center; +use Symfony\Component\Security\Core\Role\Role; /** * Allow to pick amongst available scope for the current @@ -82,10 +84,10 @@ class ScopePickerType extends AbstractType $resolver // create `center` option ->setRequired('center') - ->setAllowedTypes('center', [\Chill\MainBundle\Entity\Center::class ]) + ->setAllowedTypes('center', [Center::class ]) // create ``role` option ->setRequired('role') - ->setAllowedTypes('role', ['string', \Symfony\Component\Security\Core\Role\Role::class ]) + ->setAllowedTypes('role', ['string', Role::class ]) ; $resolver @@ -95,25 +97,7 @@ class ScopePickerType extends AbstractType return $this->translatableStringHelper->localize($c->getName()); }) ->setNormalizer('query_builder', function(Options $options) { - $qb = $this->scopeRepository->createQueryBuilder('s'); - $qb - // jointure to center - ->join('s.roleScopes', 'rs') - ->join('rs.permissionsGroups', 'pg') - ->join('pg.groupCenters', 'gc') - //->join('gc.users', 'user') - // add center constraint - ->where($qb->expr()->eq('IDENTITY(gc.center)', ':center')) - ->setParameter('center', $options['center']->getId()) - // role constraints - ->andWhere($qb->expr()->eq('rs.role', ':role')) - ->setParameter('role', $options['role']) - // user contraint - ->andWhere(':user MEMBER OF gc.users') - ->setParameter('user', $this->tokenStorage->getToken()->getUser()) - ; - - return $qb; + return $this->buildAccessibleScopeQuery($options['center'], $options['role']); }) ; } @@ -122,4 +106,38 @@ class ScopePickerType extends AbstractType { return EntityType::class; } + + /** + * + * @return \Doctrine\ORM\QueryBuilder + */ + protected function buildAccessibleScopeQuery(Center $center, Role $role) + { + $roles = $this->authorizationHelper->getParentRoles($role); + $roles[] = $role; + + $qb = $this->scopeRepository->createQueryBuilder('s'); + $qb + // jointure to center + ->join('s.roleScopes', 'rs') + ->join('rs.permissionsGroups', 'pg') + ->join('pg.groupCenters', 'gc') + // add center constraint + ->where($qb->expr()->eq('IDENTITY(gc.center)', ':center')) + ->setParameter('center', $center->getId()) + // role constraints + ->andWhere($qb->expr()->in('rs.role', ':roles')) + ->setParameter('roles', \array_map( + function(Role $role) { + return $role->getRole(); + }, + $roles + )) + // user contraint + ->andWhere(':user MEMBER OF gc.users') + ->setParameter('user', $this->tokenStorage->getToken()->getUser()) + ; + + return $qb; + } } diff --git a/Form/Type/UserPickerType.php b/Form/Type/UserPickerType.php index 9cade1ad2..0d0a44fdd 100644 --- a/Form/Type/UserPickerType.php +++ b/Form/Type/UserPickerType.php @@ -25,6 +25,8 @@ use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Chill\MainBundle\Entity\User; +use Symfony\Component\Security\Core\Role\RoleHierarchyInterface; +use Symfony\Component\Security\Core\Role\Role; /** @@ -81,29 +83,14 @@ class UserPickerType extends AbstractType $resolver ->setDefault('class', User::class) - ->setDefault('empty_data', $this->tokenStorage->getToken()->getUser()) + ->setDefault('placeholder', 'Choose an user') ->setDefault('choice_label', function(User $u) { return $u->getUsername(); }) - ->setNormalizer('query_builder', function(Options $options) { - $qb = $this->userRepository->createQueryBuilder('u'); - $qb - // add center constraint - ->join('u.groupCenters', 'ug') - ->where($qb->expr()->eq('ug.center', ':center')) - ->setParameter('center', $options['center']) - // link to permission groups - ->join('ug.permissionsGroup', 'pg') - // role constraints - ->join('pg.roleScopes', 'roleScope') - ->andWhere($qb->expr()->eq('roleScope.role', ':role')) - ->setParameter('role', $options['role']) - // add active constraint - ->andWhere('u.enabled = :enabled') - ->setParameter('enabled', true) - ; - - return $qb; + ->setNormalizer('choices', function(Options $options) { + + return $this->authorizationHelper + ->findUsersReaching($options['role'], $options['center']); }) ; } diff --git a/Form/UserType.php b/Form/UserType.php index 404461423..cbb027c25 100644 --- a/Form/UserType.php +++ b/Form/UserType.php @@ -19,9 +19,10 @@ class UserType extends AbstractType { $builder ->add('username') + ->add('email') ; if ($options['is_creation']) { - $builder->add('plainPassword', new UserPasswordType(), array( + $builder->add('plainPassword', UserPasswordType::class, array( 'mapped' => false )); diff --git a/Repository/PostalCodeRepository.php b/Repository/PostalCodeRepository.php new file mode 100644 index 000000000..5b8ab8423 --- /dev/null +++ b/Repository/PostalCodeRepository.php @@ -0,0 +1,28 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\MainBundle\Repository; + +/** + * + * + * @author Julien Fastré + */ +class PostalCodeRepository extends \Doctrine\ORM\EntityRepository +{ + +} diff --git a/Resources/config/doctrine/PostalCode.orm.yml b/Resources/config/doctrine/PostalCode.orm.yml index 09381d465..59d74ddf3 100644 --- a/Resources/config/doctrine/PostalCode.orm.yml +++ b/Resources/config/doctrine/PostalCode.orm.yml @@ -1,6 +1,9 @@ Chill\MainBundle\Entity\PostalCode: type: entity table: chill_main_postal_code + repositoryClass: Chill\MainBundle\Repository\PostalCodeRepository + indexes: + - { name: search_name_code, columns: [ "code", "label" ] } id: id: type: integer diff --git a/Resources/config/doctrine/User.orm.yml b/Resources/config/doctrine/User.orm.yml index 05b434260..08d8064d3 100644 --- a/Resources/config/doctrine/User.orm.yml +++ b/Resources/config/doctrine/User.orm.yml @@ -14,6 +14,21 @@ Chill\MainBundle\Entity\User: username: type: string length: 80 + usernameCanonical: + name: username_canonical + type: string + length: 80 + unique: true + email: + type: string + length: 150 + nullable: true + emailCanonical: + name: email_canonical + type: string + length: 150 + nullable: true + unique: true password: type: string length: 255 diff --git a/Resources/config/routing.yml b/Resources/config/routing.yml index 6321e9876..5a9414fdd 100644 --- a/Resources/config/routing.yml +++ b/Resources/config/routing.yml @@ -17,6 +17,10 @@ chill_main_admin: chill_main_exports: resource: "@ChillMainBundle/Resources/config/routing/exports.yml" prefix: "{_locale}/exports" + +chill_postal_code: + resource: "@ChillMainBundle/Resources/config/routing/postal-code.yml" + prefix: "{_locale}/postal-code" root: path: / @@ -32,12 +36,6 @@ chill_main_homepage_without_locale: chill_main_homepage: path: /{_locale}/homepage defaults: { _controller: ChillMainBundle:Default:index } - options: - menus: - section: - order: 10 - label: Homepage - icons: [home] chill_main_admin_central: @@ -80,13 +78,7 @@ login_check: logout: path: /logout - options: - menus: - user: - order: 10 - label: Logout - icon: power-off password: path: /password - defaults: { _controller: ChillMainBundle:Password:userPassword } + defaults: { _controller: ChillMainBundle:Password:userPassword } \ No newline at end of file diff --git a/Resources/config/routing/exports.yml b/Resources/config/routing/exports.yml index 82eb981c3..966902952 100644 --- a/Resources/config/routing/exports.yml +++ b/Resources/config/routing/exports.yml @@ -1,12 +1,6 @@ chill_main_export_index: path: / defaults: { _controller: ChillMainBundle:Export:index } - options: - menus: - section: - order: 20 - label: Export Menu - icons: [upload] chill_main_export_new: path: /new/{alias} diff --git a/Resources/config/routing/postal-code.yml b/Resources/config/routing/postal-code.yml new file mode 100644 index 000000000..d8892c098 --- /dev/null +++ b/Resources/config/routing/postal-code.yml @@ -0,0 +1,4 @@ +chill_main_postal_code_search: + path: /search + defaults: { _controller: ChillMainBundle:PostalCode:search } + \ No newline at end of file diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 33b64df34..05807258b 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -2,27 +2,6 @@ parameters: # cl_chill_main.example.class: Chill\MainBundle\Example services: - chill.main.routes_loader: - class: Chill\MainBundle\Routing\Loader\ChillRoutesLoader - arguments: - - "%chill_main.routing.resources%" - tags: - - { name: routing.loader } - - chill.main.menu_composer: - class: Chill\MainBundle\Routing\MenuComposer - #must be set in function to avoid circular reference with chill.main.twig.chill_menu - calls: - - [setContainer, ["@service_container"]] - - chill.main.twig.chill_menu: - class: Chill\MainBundle\Routing\MenuTwig - arguments: - - "@chill.main.menu_composer" - calls: - - [setContainer, ["@service_container"]] - tags: - - { name: twig.extension } twig_intl: class: Twig_Extensions_Extension_Intl @@ -41,6 +20,7 @@ services: arguments: - "@request_stack" - "@translator.default" + Chill\MainBundle\Templating\TranslatableStringHelper: '@chill.main.helper.translatable_string' chill.main.twig.translatable_string: class: Chill\MainBundle\Templating\TranslatableStringTwig @@ -75,15 +55,7 @@ services: - "@doctrine.orm.entity_manager" calls: - [ setContainer, ["@service_container"]] - - chill.main.security.authorization.helper: - class: Chill\MainBundle\Security\Authorization\AuthorizationHelper - arguments: - - "@security.role_hierarchy" - - chill.main.role_provider: - class: Chill\MainBundle\Security\RoleProvider - + chill.main.validator.role_scope_scope_presence: class: Chill\MainBundle\Validation\Validator\RoleScopeScopePresence arguments: diff --git a/Resources/config/services/controller.yml b/Resources/config/services/controller.yml new file mode 100644 index 000000000..7ead910ec --- /dev/null +++ b/Resources/config/services/controller.yml @@ -0,0 +1,6 @@ +services: + + Chill\MainBundle\Controller\: + autowire: true + resource: '../../../Controller' + tags: ['controller.service_arguments'] diff --git a/Resources/config/services/fixtures.yml b/Resources/config/services/fixtures.yml new file mode 100644 index 000000000..b3ef847e4 --- /dev/null +++ b/Resources/config/services/fixtures.yml @@ -0,0 +1,4 @@ +services: + Chill\MainBundle\DataFixtures\ORM\: + resource: ../../../DataFixtures/ORM + tags: [ 'doctrine.fixture.orm' ] diff --git a/Resources/config/services/form.yml b/Resources/config/services/form.yml index 30502a399..78bfa0280 100644 --- a/Resources/config/services/form.yml +++ b/Resources/config/services/form.yml @@ -54,9 +54,17 @@ services: class: Chill\MainBundle\Form\Type\PostalCodeType arguments: - "@chill.main.helper.translatable_string" + - '@Symfony\Component\Routing\Generator\UrlGeneratorInterface' + - '@chill.main.form.choice_loader.postal_code' + - '@Symfony\Component\Translation\TranslatorInterface' tags: - { name: form.type } + chill.main.form.choice_loader.postal_code: + class: Chill\MainBundle\Form\ChoiceLoader\PostalCodeChoiceLoader + arguments: + - '@Chill\MainBundle\Repository\PostalCodeRepository' + chill.main.form.type.export: class: Chill\MainBundle\Form\Type\Export\ExportType arguments: diff --git a/Resources/config/services/menu.yml b/Resources/config/services/menu.yml new file mode 100644 index 000000000..0de492e3e --- /dev/null +++ b/Resources/config/services/menu.yml @@ -0,0 +1,10 @@ +services: + Chill\MainBundle\Routing\MenuBuilder\UserMenuBuilder: + tags: + - { name: 'chill.menu_builder' } + + Chill\MainBundle\Routing\MenuBuilder\SectionMenuBuilder: + arguments: + $authorizationChecker: '@Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface' + tags: + - { name: 'chill.menu_builder' } diff --git a/Resources/config/services/pagination.yml b/Resources/config/services/pagination.yml index 848599f42..ca9b50e4f 100644 --- a/Resources/config/services/pagination.yml +++ b/Resources/config/services/pagination.yml @@ -5,6 +5,7 @@ services: - "@request_stack" - "@router" - "%chill_main.pagination.item_per_page%" + Chill\MainBundle\Pagination\PaginatorFactory: '@chill_main.paginator_factory' chill_main.paginator.twig_extensions: class: Chill\MainBundle\Pagination\ChillPaginationTwig diff --git a/Resources/config/services/repositories.yml b/Resources/config/services/repositories.yml index 3f40e82dc..56ea84656 100644 --- a/Resources/config/services/repositories.yml +++ b/Resources/config/services/repositories.yml @@ -15,4 +15,13 @@ services: class: Doctrine\ORM\EntityRepository factory: ["@doctrine.orm.entity_manager", getRepository] arguments: - - "Chill\\MainBundle\\Entity\\Scope" \ No newline at end of file + - "Chill\\MainBundle\\Entity\\Scope" + + chill.main.postalcode_repository: + class: Doctrine\ORM\EntityRepository + factory: ["@doctrine.orm.entity_manager", getRepository] + arguments: + - "Chill\\MainBundle\\Entity\\PostalCode" + + Chill\MainBundle\Repository\PostalCodeRepository: '@chill.main.postalcode_repository' + \ No newline at end of file diff --git a/Resources/config/services/routing.yml b/Resources/config/services/routing.yml new file mode 100644 index 000000000..c935f1a4d --- /dev/null +++ b/Resources/config/services/routing.yml @@ -0,0 +1,24 @@ +services: + chill.main.menu_composer: + class: Chill\MainBundle\Routing\MenuComposer + arguments: + - '@Symfony\Component\Routing\RouterInterface' + - '@Knp\Menu\FactoryInterface' + - '@Symfony\Component\Translation\TranslatorInterface' + Chill\MainBundle\Routing\MenuComposer: '@chill.main.menu_composer' + + chill.main.routes_loader: + class: Chill\MainBundle\Routing\Loader\ChillRoutesLoader + arguments: + - "%chill_main.routing.resources%" + tags: + - { name: routing.loader } + + chill.main.twig.chill_menu: + class: Chill\MainBundle\Routing\MenuTwig + arguments: + - "@chill.main.menu_composer" + calls: + - [setContainer, ["@service_container"]] + tags: + - { name: twig.extension } diff --git a/Resources/config/services/security.yml b/Resources/config/services/security.yml new file mode 100644 index 000000000..a5c5471f5 --- /dev/null +++ b/Resources/config/services/security.yml @@ -0,0 +1,23 @@ +services: + chill.main.security.authorization.helper: + class: Chill\MainBundle\Security\Authorization\AuthorizationHelper + arguments: + $roleHierarchy: "@security.role_hierarchy" + $hierarchy: "%security.role_hierarchy.roles%" + $em: '@Doctrine\ORM\EntityManagerInterface' + Chill\MainBundle\Security\Authorization\AuthorizationHelper: '@chill.main.security.authorization.helper' + + chill.main.role_provider: + class: Chill\MainBundle\Security\RoleProvider + + chill.main.user_provider: + class: Chill\MainBundle\Security\UserProvider\UserProvider + arguments: + $em: '@Doctrine\ORM\EntityManagerInterface' + + Chill\MainBundle\Security\Authorization\ChillExportVoter: + arguments: + $authorizationHelper: '@Chill\MainBundle\Security\Authorization\AuthorizationHelper' + tags: + - { name: security.voter } + \ No newline at end of file diff --git a/Resources/config/services/validator.yml b/Resources/config/services/validator.yml new file mode 100644 index 000000000..c15b2181e --- /dev/null +++ b/Resources/config/services/validator.yml @@ -0,0 +1,13 @@ +services: + chill_main.validator_user_circle_consistency: + class: Chill\MainBundle\Validator\Constraints\Entity\UserCircleConsistencyValidator + arguments: + - "@chill.main.security.authorization.helper" + tags: + - { name: "validator.constraint_validator" } + + Chill\MainBundle\Validation\Validator\UserUniqueEmailAndUsername: + arguments: + $em: '@Doctrine\ORM\EntityManagerInterface' + tags: + - { name: "validator.constraint_validator" } diff --git a/Resources/config/services/widget.yml b/Resources/config/services/widget.yml new file mode 100644 index 000000000..53f29da5b --- /dev/null +++ b/Resources/config/services/widget.yml @@ -0,0 +1,2 @@ +services: + Chill\MainBundle\Templating\UI\CountNotificationUser: ~ diff --git a/Resources/config/validation.yml b/Resources/config/validation.yml index 87f696f43..141d489fc 100644 --- a/Resources/config/validation.yml +++ b/Resources/config/validation.yml @@ -1,27 +1,32 @@ Chill\MainBundle\Entity\PermissionsGroup: properties: - name: + name: - NotBlank: ~ - Length: max: 50 roleScopes: - Valid: ~ constraints: - - Callback: [isRoleScopePresentOnce] - + - Callback: + callback: isRoleScopePresentOnce + Chill\MainBundle\Entity\User: properties: username: - - Length: + - Length: max: 70 min: 3 + email: + - Email: ~ constraints: - - Callback: [isGroupCenterPresentOnce] + - Callback: + callback: isGroupCenterPresentOnce + - \Chill\MainBundle\Validation\Constraint\UserUniqueEmailAndUsernameConstraint: ~ Chill\MainBundle\Entity\RoleScope: constraints: - \Chill\MainBundle\Validation\Constraint\RoleScopeScopePresenceConstraint: ~ - + Chill\MainBundle\Entity\Center: properties: name: @@ -41,11 +46,11 @@ Chill\MainBundle\Entity\Address: validFrom: - NotNull: ~ - Date: ~ - + Chill\MainBundle\Entity\PostalCode: properties: name: - - Length: + - Length: max: 250 min: 2 code: @@ -53,4 +58,4 @@ Chill\MainBundle\Entity\PostalCode: min: 2 max: 100 country: - - NotNull: ~ \ No newline at end of file + - NotNull: ~ diff --git a/Resources/migrations/Version20180703191509.php b/Resources/migrations/Version20180703191509.php new file mode 100644 index 000000000..82117de9d --- /dev/null +++ b/Resources/migrations/Version20180703191509.php @@ -0,0 +1,33 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + try { + $this->addSql('CREATE EXTENSION IF NOT EXISTS pg_trgm'); + $this->addSql('CREATE INDEX search_name_code ON chill_main_postal_code USING GIN (LOWER(code) gin_trgm_ops, LOWER(label) gin_trgm_ops)'); + } catch (\Exception $e) { + $this->skipIf(true, "Could not create extension pg_trgm"); + } + + } + + public function down(Schema $schema) : void + { + $this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('DROP INDEX search_name_code'); + + } +} diff --git a/Resources/migrations/Version20180709181423.php b/Resources/migrations/Version20180709181423.php new file mode 100644 index 000000000..93ca04e12 --- /dev/null +++ b/Resources/migrations/Version20180709181423.php @@ -0,0 +1,87 @@ +abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('ALTER TABLE users ADD usernameCanonical VARCHAR(80) DEFAULT NULL'); + $this->addSql('UPDATE users SET usernameCanonical=LOWER(UNACCENT(username))'); + $this->addSql('ALTER TABLE users ALTER usernameCanonical DROP NOT NULL'); + $this->addSql('ALTER TABLE users ALTER usernameCanonical SET DEFAULT NULL'); + $this->addSql('ALTER TABLE users ADD email VARCHAR(150) DEFAULT NULL'); + $this->addSql('ALTER TABLE users ADD emailCanonical VARCHAR(150) DEFAULT NULL'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9F5A5DC32 ON users (usernameCanonical)'); + $this->addSql('CREATE UNIQUE INDEX UNIQ_1483A5E9885281E ON users (emailCanonical)'); + + $this->addSql(<<<'SQL' + CREATE OR REPLACE FUNCTION canonicalize_user_on_update() RETURNS TRIGGER AS + $BODY$ + BEGIN + IF NEW.username <> OLD.username OR NEW.email <> OLD.email OR OLD.emailcanonical IS NULL OR OLD.usernamecanonical IS NULL THEN + UPDATE users SET usernamecanonical=LOWER(UNACCENT(NEW.username)), emailcanonical=LOWER(UNACCENT(NEW.email)) WHERE id=NEW.id; + END IF; + + RETURN NEW; + END; + $BODY$ LANGUAGE PLPGSQL +SQL + ); + + $this->addSql(<<addSql(<<<'SQL' + CREATE OR REPLACE FUNCTION canonicalize_user_on_insert() RETURNS TRIGGER AS + $BODY$ + BEGIN + UPDATE users SET usernamecanonical=LOWER(UNACCENT(NEW.username)), emailcanonical=LOWER(UNACCENT(NEW.email)) WHERE id=NEW.id; + + RETURN NEW; + END; + $BODY$ LANGUAGE PLPGSQL; +SQL + ); + + $this->addSql(<<abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.'); + + $this->addSql('DROP INDEX UNIQ_1483A5E9F5A5DC32'); + $this->addSql('DROP INDEX UNIQ_1483A5E9885281E'); + $this->addSql('ALTER TABLE users DROP usernameCanonical'); + $this->addSql('ALTER TABLE users DROP email'); + $this->addSql('ALTER TABLE users DROP emailCanonical'); + $this->addSql('DROP TRIGGER canonicalize_user_on_insert ON users'); + $this->addSql('DROP FUNCTION canonicalize_user_on_insert()'); + $this->addSql('DROP TRIGGER canonicalize_user_on_update ON users'); + $this->addSql('DROP FUNCTION canonicalize_user_on_update()'); + + } +} diff --git a/Resources/public/css/scratch.css b/Resources/public/css/scratch.css index a66373576..31d1f0ab1 100644 --- a/Resources/public/css/scratch.css +++ b/Resources/public/css/scratch.css @@ -4585,7 +4585,6 @@ header { right: 0; top: 0; z-index: -1; - background-image: url("/bundles/chillmain/img/background/desert.jpg"); background-attachment: fixed; background-repeat: no-repeat; background-size: cover; diff --git a/Resources/public/fonts/OpenSans/OpenSans.css b/Resources/public/fonts/OpenSans/OpenSans.scss similarity index 100% rename from Resources/public/fonts/OpenSans/OpenSans.css rename to Resources/public/fonts/OpenSans/OpenSans.scss diff --git a/Resources/public/js/chill.js b/Resources/public/js/chill.js index a7d661679..54d2ceb80 100644 --- a/Resources/public/js/chill.js +++ b/Resources/public/js/chill.js @@ -93,7 +93,7 @@ var chill = function() { * * @param{string} form_id An identification string of the form * @param{string} alert_message The alert message to display - * @param{boolean} check_unsaved_data If true display the alert message only when the form + * @param{boolean} check_unsaved_data If true display the alert message only when the form * contains some modified fields otherwise always display the alert when leaving * @return nothing */ @@ -123,12 +123,12 @@ var chill = function() { }); } - /** - * Mark the choices "not specified" as check by default. - * - * This function apply to `custom field choices` when the `required` + /** + * Mark the choices "not specified" as check by default. + * + * This function apply to `custom field choices` when the `required` * option is false and `expanded` is true (checkboxes or radio buttons). - * + * * @param{string} choice_name the name of the input */ function checkNullValuesInChoices(choice_name) { @@ -184,21 +184,21 @@ var chill = function() { * child) of a given form : each parent option has a category, the * child select only display options that have the same category of the * parent optionn - * - * The parent must have the class "chill-category-link-parent". - * + * + * The parent must have the class "chill-category-link-parent". + * * The children must have the class "chill-category-link-child". Each option * of the parent must have the attribute `data-link-category`, with the value of * the connected option in parent. - * + * * Example : - * + * * ```html * - * + * * * ``` - * + * * TODO ECRIRE LA DOC METTRE LES TESTS DANS git : * tester que init est ok : - quand vide @@ -224,7 +224,7 @@ var chill = function() { form.old_category = null; form.link_parent = $(form).find('.chill-category-link-parent'); form.link_child = $(form).find('.chill-category-link-child'); - + // check if the parent allow multiple or single results parent_multiple = $(form).find('.chill-category-link-parent').get(0).multiple; // if we use select2, parent_multiple will be `undefined` @@ -233,10 +233,10 @@ var chill = function() { // we suppose that multiple is false (old behaviour) parent_multiple = false } - + $(form.link_parent).addClass('select2'); $(form.link_parant).select2({allowClear: true}); // it is weird: when I fix the typo here, the whole stuff does not work anymore... - + if (parent_multiple == false) { form.old_category = null; @@ -279,9 +279,9 @@ var chill = function() { } }); } else { - var i=0, + var i=0, selected_items = $(form.link_parent).find(':selected'); - + form.old_categories = []; for (i=0;i < selected_items.length; i++) { form.old_categories.push(selected_items[i].value); @@ -314,13 +314,13 @@ var chill = function() { }); form.link_parent.change(function() { - var new_categories = [], + var new_categories = [], selected_items = $(form.link_parent).find(':selected'), visible; for (i=0;i < selected_items.length; i++) { new_categories.push(selected_items[i].value); } - + if(new_categories != form.old_categories) { $(form.link_child).find('option') .each(function(i,e) { @@ -352,16 +352,16 @@ var chill = function() { form.old_categories = new_categories; } }); - } + } }); } - + function _displayHideTargetWithCheckbox(checkbox) { var target = checkbox.dataset.displayTarget, hideableElements; - + hideableElements = document.querySelectorAll('[data-display-show-hide="' + target + '"]'); - + if (checkbox.checked) { for (let i=0; i < hideableElements.length; i = i+1) { hideableElements[i].style.display = "unset"; @@ -371,36 +371,36 @@ var chill = function() { hideableElements[i].style.display = "none"; } } - + } - + /** - * create an interaction between a checkbox and element to show if the + * create an interaction between a checkbox and element to show if the * checkbox is checked, or hide if the checkbox is not checked. - * - * The checkbox must have the data `data-display-target` with an id, + * + * The checkbox must have the data `data-display-target` with an id, * and the parts to show/hide must have the data `data-display-show-hide` * with the same value. - * - * Example : - * + * + * Example : + * * ``` * - * + * *
* *
* ``` - * + * * Hint: for forms in symfony, you could use the `id` of the form element, * accessible through `{{ form.vars.id }}`. This id should be unique. - * - * + * + * * @returns {undefined} */ function listenerDisplayCheckbox() { var elements = document.querySelectorAll("[data-display-target]"); - + for (let i=0; i < elements.length; i = i+1) { elements[i].addEventListener("change", function(e) { _displayHideTargetWithCheckbox(e.target); @@ -421,3 +421,5 @@ var chill = function() { listenerDisplayCheckbox: listenerDisplayCheckbox, }; } (); + +export { chill }; diff --git a/Resources/public/js/collection/collection.scss b/Resources/public/js/collection/collection.scss new file mode 100644 index 000000000..13fc4a528 --- /dev/null +++ b/Resources/public/js/collection/collection.scss @@ -0,0 +1,23 @@ +div.chill-collection { + ul.chill-collection__list { + list-style: none; + padding: 0; + margin-bottom: 1.5rem; + + li.chill-collection__list__entry:nth-child(2n) { + background-color: var(--chill-light-gray); + padding: 0.5rem 0; + } + + // all entries, except the last one + li.chill-collection__list__entry:nth-last-child(1n+2) { + margin-bottom: 1rem; + } + } + + button.chill-collection__button--add { + + + } +} + diff --git a/Resources/public/js/collection/collections.js b/Resources/public/js/collection/collections.js new file mode 100644 index 000000000..60b34fb17 --- /dev/null +++ b/Resources/public/js/collection/collections.js @@ -0,0 +1,116 @@ +/** + * Javascript file which handle ChillCollectionType + * + * Two events are emitted by this module, both on window and on collection / ul. + * + * Collection (an UL element) and entry (a li element) are associated with those + * events. + * + * ``` + * window.addEventListener('collection-add-entry', function(e) { + * console.log(e.detail.collection); + * console.log(e.detail.entry); + * }); + * + * window.addEventListener('collection-remove-entry', function(e) { + * console.log(e.detail.collection); + * console.log(e.detail.entry); + * }); + * + * collection.addEventListener('collection-add-entry', function(e) { + * console.log(e.detail.collection); + * console.log(e.detail.entry); + * }); + * + * collection.addEventListener('collection-remove-entry', function(e) { + * console.log(e.detail.collection); + * console.log(e.detail.entry); + * }); + * ``` + */ +require('./collection.scss'); + +class CollectionEvent { + constructor(collection, entry) { + this.collection = collection; + this.entry = entry; + } +} + +/** + * + * @param {type} button + * @returns {handleAdd} + */ +var handleAdd = function(button) { + var + form_name = button.dataset.collectionAddTarget, + prototype = button.dataset.formPrototype, + collection = document.querySelector('ul[data-collection-name="'+form_name+'"]'), + entry = document.createElement('li'), + event = new CustomEvent('collection-add-entry', { detail: { collection: collection, entry: entry } }), + counter = collection.childNodes.length, + content + ; + content = prototype.replace(new RegExp('__name__', 'g'), counter); + entry.innerHTML = content; + entry.classList.add('chill-collection__list__entry'); + initializeRemove(collection, entry); + collection.appendChild(entry); + chill.initPikaday('fr'); + + collection.dispatchEvent(event); + window.dispatchEvent(event); +}; + +var initializeRemove = function(collection, entry) { + var + button = document.createElement('button'), + isPersisted = entry.dataset.collectionIsPersisted, + content = collection.dataset.collectionButtonRemoveLabel, + allowDelete = collection.dataset.collectionAllowDelete, + event = new CustomEvent('collection-remove-entry', { detail: { collection: collection, entry: entry } }) + ; + + if (allowDelete === '0' && isPersisted === '1') { + return; + } + + button.classList.add('sc-button', 'bt-delete'); + button.textContent = content; + + button.addEventListener('click', function(e) { + e.preventDefault(); + entry.remove(); + collection.dispatchEvent(event); + window.dispatchEvent(event); + }); + + entry.appendChild(button); +}; + +window.addEventListener('load', function() { + var + addButtons = document.querySelectorAll("button[data-collection-add-target]"), + collections = document.querySelectorAll("ul[data-collection-name]") + ; + + for (let i = 0; i < addButtons.length; i ++) { + let addButton = addButtons[i]; + addButton.addEventListener('click', function(e) { + e.preventDefault(); + handleAdd(e.target); + }); + } + + for (let i = 0; i < collections.length; i ++) { + let entries = collections[i].querySelectorAll(':scope > li'); + + for (let j = 0; j < entries.length; j ++) { + initializeRemove(collections[i], entries[j]); + } + } +}); + + + diff --git a/Resources/public/modules/breadcrumb/index.js b/Resources/public/modules/breadcrumb/index.js new file mode 100644 index 000000000..40173696d --- /dev/null +++ b/Resources/public/modules/breadcrumb/index.js @@ -0,0 +1,3 @@ +require("./layout.scss"); + + diff --git a/Resources/public/modules/breadcrumb/layout.scss b/Resources/public/modules/breadcrumb/layout.scss new file mode 100644 index 000000000..0a611da67 --- /dev/null +++ b/Resources/public/modules/breadcrumb/layout.scss @@ -0,0 +1,21 @@ +nav.chill-breadcrumb { + ul.list.on-left { + li { + border-right: 10px solid var(--chill-red); + } + li.active { + border-right: 10px solid var(--chill-green); + } + li.past { + border-right: 10px solid var(--chill-blue); + } + li.no-jump{ + border-right: 10px solid var(--chill-light-gray); + background-color: var(--chill-yellow); + padding: 0.3em 0.3em 0.3em 0.6em; + color: var(--chill-dark-gray); + } + } + + +} diff --git a/Resources/public/modules/download-report/download-report.js b/Resources/public/modules/download-report/download-report.js new file mode 100644 index 000000000..1900c04cf --- /dev/null +++ b/Resources/public/modules/download-report/download-report.js @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2018 Champs Libres Cooperative + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +var mime = require('mime-types') + +var download_report = (url, container) => { + var download_text = container.dataset.downloadText, + alias = container.dataset.alias; + + window.fetch(url, { credentials: 'same-origin' }) + .then(response => { + if (!response.ok) { + throw Error(response.statusText); + } + + return response.blob(); + }).then(blob => { + + var content = URL.createObjectURL(blob), + link = document.createElement("a"), + type = blob.type, + hasForcedType = 'mimeType' in container.dataset, + extension; + + if (hasForcedType) { + // force a type + type = container.dataset.mimeType; + blob = new Blob([ blob ], { 'type': type }); + content = URL.createObjectURL(blob); + } + + extension = mime.extension(type); + + link.appendChild(document.createTextNode(download_text)); + link.classList.add("sc-button", "btn-action"); + link.href = content; + link.download = alias; + if (extension !== false) { + link.download = link.download + '.' + extension; + } + container.innerHTML = ""; + container.appendChild(link); + }).catch(function(error) { + console.log(error); + var problem_text = + document.createTextNode("Problem during download"); + + container + .replaceChild(problem_text, container.firstChild); + }); +}; + +module.exports = download_report; \ No newline at end of file diff --git a/Resources/public/modules/download-report/index.js b/Resources/public/modules/download-report/index.js new file mode 100644 index 000000000..1b519e6c2 --- /dev/null +++ b/Resources/public/modules/download-report/index.js @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 Champs Libres Cooperative + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +chill.download_report = require("./download-report.js"); diff --git a/Resources/public/modules/login_page/index.js b/Resources/public/modules/login_page/index.js new file mode 100644 index 000000000..1e6676d8f --- /dev/null +++ b/Resources/public/modules/login_page/index.js @@ -0,0 +1,3 @@ +require('./login.scss'); + + diff --git a/Resources/public/modules/login_page/login.scss b/Resources/public/modules/login_page/login.scss new file mode 100644 index 000000000..4c764bdcc --- /dev/null +++ b/Resources/public/modules/login_page/login.scss @@ -0,0 +1,63 @@ +@import './../../fonts/OpenSans/OpenSans'; + +html, body { + height: 100%; + margin: 0; + padding: 0; +} + +body { + background-color: #333; + color: white; + text-align: center; + font-family: 'Open Sans'; +} + +#content { + position: relative; + height: 90%; + padding-top: 10%; +} + +#content:before { + bottom: 0; + content: ""; + left: 0; + opacity: 0.2; + position: absolute; + right: 0; + top: 0; + z-index: -1; + background-image: url('./../../img/background/desert.jpg'); + background-attachment: fixed; + background-repeat: no-repeat; + background-size: cover; + background-position: center; +} + +label { + width : 15em; + text-align: right; + display: inline-block; + font-weight: 300; + padding-right: 5px; +} +input { + +} +form { + +} +button { + margin-left: 15em; + margin-top: 1em; + background-color: #df4949; + border: medium none; + border-radius: 0; + box-shadow: none; + color: #fff; + padding: 4px 8px; + font-family: 'Open Sans'; + font-weight: 300; +} + diff --git a/Resources/public/modules/postal-code/index.js b/Resources/public/modules/postal-code/index.js new file mode 100644 index 000000000..ba81fba90 --- /dev/null +++ b/Resources/public/modules/postal-code/index.js @@ -0,0 +1,36 @@ + +window.addEventListener('load', function (e) { + var + postalCodes = document.querySelectorAll('[data-postal-code]') + ; + + for (let i = 0; i < postalCodes.length; i++) { + let + searchUrl = postalCodes[i].dataset.searchUrl, + noResultsLabel = postalCodes[i].dataset.noResultsLabel, + errorLoadLabel = postalCodes[i].dataset.errorLoadLabel, + searchingLabel = postalCodes[i].dataset.searchingLabel + ; + + + $(postalCodes[i]).select2({ + allowClear: true, + language: { + errorLoading: function () { + return errorLoadLabel; + }, + noResults: function () { + return noResultsLabel; + }, + searching: function () { + return searchingLabel; + } + }, + ajax: { + url: searchUrl, + dataType: 'json', + delay: 250 + } + }); + } +}); diff --git a/Resources/public/sass/_custom.scss b/Resources/public/sass/_custom.scss index 10f6d5e2e..393d16e75 100644 --- a/Resources/public/sass/_custom.scss +++ b/Resources/public/sass/_custom.scss @@ -1,4 +1,7 @@ // YOUR CUSTOM SCSS +@import 'custom/config/colors'; +@import 'custom/config/variables'; +@import 'custom/fonts'; @import 'custom/timeline'; @import 'custom/mixins/entity'; @import 'custom/activity'; @@ -11,7 +14,7 @@ @import 'custom/flash_messages'; -html,body { +html,body { min-height:100%; font-family: 'Open Sans'; } @@ -34,7 +37,7 @@ header { right: 0; top: 0; z-index: -1; - background-image: url("/bundles/chillmain/img/background/desert.jpg"); + background-image: url('./../../img/background/desert.jpg'); background-attachment: fixed; background-repeat: no-repeat; background-size: cover; @@ -74,18 +77,14 @@ ul.custom_fields.choice li { color: $red; } -.blop label { - line-height: 1 + ($base-spacing / 3); -} - .footer { p { font-family: 'Open Sans'; font-weight: 300; } - - a { - color: white; + + a { + color: white; text-decoration: underline; } } @@ -97,7 +96,7 @@ ul.custom_fields.choice li { display: inline-block; text-align: center; } - + .separator { margin-left: 0.2em; margin-right: 0.2em; @@ -107,7 +106,7 @@ ul.custom_fields.choice li { .open_sansbold { font-family: 'Open Sans'; font-weight: bold; - + } @@ -140,21 +139,21 @@ div.input_with_post_text input { dl.chill_report_view_data, dl.chill_view_data { - + dt { margin-top: 1.5em; color: $chill-blue; } - + dd { padding-left: 1.5em; margin-top: 0.2em; - + ul { padding-left: 0; } } - + } @@ -164,13 +163,13 @@ blockquote.chill-user-quote { padding: 0.5em 10px; quotes: "\201C""\201D""\2018""\2019"; background-color: $chill-llight-gray; - - + + p { display: inline; } - + } .chill-no-data-statement { font-style: italic; - + } diff --git a/Resources/public/sass/custom/_flash_messages.scss b/Resources/public/sass/custom/_flash_messages.scss index e539171d0..62f875dfa 100644 --- a/Resources/public/sass/custom/_flash_messages.scss +++ b/Resources/public/sass/custom/_flash_messages.scss @@ -4,11 +4,11 @@ // note that other level are defined in modules/_alerts.scss -.alert { - // override in modules/_alerts.scss - @include alert($red); -} - -.warning { - @include alert($orange); -} +// .alert { +// // override in modules/_alerts.scss +// @include alert($red); +// } +// +// .warning { +// @include alert($orange); +// } diff --git a/Resources/public/sass/custom/_fonts.scss b/Resources/public/sass/custom/_fonts.scss new file mode 100644 index 000000000..a129d32bc --- /dev/null +++ b/Resources/public/sass/custom/_fonts.scss @@ -0,0 +1,2 @@ + +@import './../../fonts/OpenSans/OpenSans'; \ No newline at end of file diff --git a/Resources/public/sass/custom/_record_actions.scss b/Resources/public/sass/custom/_record_actions.scss index 7b73db79f..3cd9fc9a5 100644 --- a/Resources/public/sass/custom/_record_actions.scss +++ b/Resources/public/sass/custom/_record_actions.scss @@ -6,29 +6,36 @@ ul.record_actions li { display: inline-block; }*/ -ul.record_actions { +ul.record_actions, ul.record_actions_column { display: flex; - flex-direction: row; justify-content: flex-end; padding: 0.5em 0; - + li { display: inline-block; list-style-type: none; margin-right: 1em; order: 99; - + &:last-child { margin-right: 0; } } - + li.cancel { order: 1; margin-right: auto; } - + +} + +ul.record_actions { + flex-direction: row; +} + +ul.record_actions_column { + flex-direction: column; } ul.record_actions.sticky-form-buttons { @@ -42,4 +49,4 @@ td ul.record_actions, li { margin-right: 0.2em; } -} \ No newline at end of file +} diff --git a/Resources/public/sass/custom/config/_colors.scss b/Resources/public/sass/custom/config/_colors.scss index fee58a852..91e4ebae0 100644 --- a/Resources/public/sass/custom/config/_colors.scss +++ b/Resources/public/sass/custom/config/_colors.scss @@ -6,11 +6,13 @@ $chill-yellow: #eec84a; $chill-orange: #e2793d; $chill-red: #df4949; $chill-gray: #ececec; -$chill-beige :#cabb9f; -$chill-pink :#dd506d; +$chill-beige: #cabb9f; +$chill-pink: #dd506d; $chill-dark-gray: #333333; $chill-light-gray: #b2b2b2; -$chill-llight-gray: $grey-10; +$chill-llight-gray: #e6e6e6; + +$dark-grey: $chill-dark-gray; $color-name: "blue" "green" "green-dark" "yellow" "orange" "red" "gray" "beige" "pink" "dark-gray" "light-gray"; $color-code: #334d5c #43b29d #328474 #eec84a #e2793d #df4949 #ececec #cabb9f #dd506d #333333 #b2b2b2; @@ -27,3 +29,37 @@ $green: $chill-green; $blue: $chill-blue; $yellow: $chill-yellow; +$black: #111111; +$white: #ffffff; +$light-grey: $chill-light-gray; + +/* + due to a bug in sass, we must re-declare the variable in css + (use of a sass variable after -- does not work) +*/ +:root { + --chill-blue: #334d5c; + --chill-green: #43b29d; + --chill-green-dark: #328474; + --chill-yellow: #eec84a; + --chill-orange: #e2793d; + --chill-red: #df4949; + --chill-gray: #ececec; + --chill-beige: #cabb9f; + --chill-pink: #dd506d; + --chill-dark-gray: #333333; + --chill-light-gray: #b2b2b2; + --chill-llight-gray: #e6e6e6; + + --dark-grey: #333333; + + --orange: #e2793d; + --red: #df4949; + --green: #43b29d; + --blue: #334d5c; + --yellow: #eec84a; + + --black: #111111; + --white: #ffffff; + --light-grey: #b2b2b2; +} diff --git a/Resources/public/sass/custom/modules/_buttons.scss b/Resources/public/sass/custom/modules/_buttons.scss index 9dfecf8a1..4a05c3089 100644 --- a/Resources/public/sass/custom/modules/_buttons.scss +++ b/Resources/public/sass/custom/modules/_buttons.scss @@ -1,27 +1,27 @@ .sc-button { &.bt-submit, &.bt-save, &.bt-create, &.bt-new { - @include button($green, $white); + @include button($green, $white); } &.bt-reset, &.bt-delete { - @include button($red, $white); + @include button($red, $white); } &.bt-action, &.bt-edit, &.bt-update { - @include button($orange, $white); + @include button($orange, $white); } &.bt-show, &.bt-view { @include button($blue, $white); } - + &:not(.change-icon) { - &.bt-create::before, + &.bt-create::before, &.bt-save::before, - &.bt-new::before, - &.bt-delete::before, - &.bt-update::before, + &.bt-new::before, + &.bt-delete::before, + &.bt-update::before, &.bt-edit::before, &.bt-cancel::before, &.bt-view::before, @@ -52,7 +52,7 @@ &.bt-cancel::before { // add an arrow left - content: ""; + content: ""; } &.bt-show::before, &.bt-view::before { @@ -70,11 +70,11 @@ margin-right: 0; } &:not(.change-icon) { - &.bt-create::before, + &.bt-create::before, &.bt-save::before, - &.bt-new::before, - &.bt-delete::before, - &.bt-update::before, + &.bt-new::before, + &.bt-delete::before, + &.bt-update::before, &.bt-edit::before, &.bt-cancel::before, &.bt-view::before, @@ -83,13 +83,13 @@ } } } - + &.has-hidden > span.show-on-hover { display: none; } - + &.has-hidden:hover { - + > span.show-on-hover { display: inline-block; } @@ -97,13 +97,13 @@ > i.fa { margin-right: 0.5em; } - + &:not(.change-icon) { - &.bt-create::before, + &.bt-create::before, &.bt-save::before, - &.bt-new::before, - &.bt-delete::before, - &.bt-update::before, + &.bt-new::before, + &.bt-delete::before, + &.bt-update::before, &.bt-edit::before, &.bt-cancel::before, &.bt-view::before, @@ -144,4 +144,4 @@ .sticky-form-buttons .margin-10 { margin-left: 10%; margin-right: 10%; -} \ No newline at end of file +} diff --git a/Resources/public/sass/custom/modules/_forms.scss b/Resources/public/sass/custom/modules/_forms.scss index 75b0e46a3..0e5099ef0 100644 --- a/Resources/public/sass/custom/modules/_forms.scss +++ b/Resources/public/sass/custom/modules/_forms.scss @@ -6,4 +6,23 @@ textarea { span.force-inline-label label { display: inline; +} + + +.chill-form-money { + display: flex; + flex-direction: row; + justify-content: flex-end; + + span.chill-form-money__money { + align-self: center; + margin-left: 1em; + } +} + + +.chill-form__errors { + .chill-form_errors__entry.chill-form__errors__entry--warning { + color: var(--chill-green-dark); + } } \ No newline at end of file diff --git a/Resources/public/sass/custom/modules/_navigation.scss b/Resources/public/sass/custom/modules/_navigation.scss index dc5e889e0..be28d0348 100644 --- a/Resources/public/sass/custom/modules/_navigation.scss +++ b/Resources/public/sass/custom/modules/_navigation.scss @@ -1,52 +1,88 @@ .navigation { - background-color: $chill-blue; + background-color: $chill-blue; - a.more:after { - color: $chill-dark-gray; - } + a.more:after { + color: $chill-dark-gray; + } - li.nav-link2 { - a { - margin-bottom: 2px; - } + li.nav-link2 { + a { + margin-bottom: 2px; + } - &.lang-selection { - color: $chill-light-gray; - font-size: 0.7em; - - a.more:after { + &.lang-selection { color: $chill-light-gray; - } - } + font-size: 0.7em; - ul { - top: 58px; + a.more:after { + color: $chill-light-gray; + } + } - a { - padding-left: 0; - } - } - } + ul { + top: 58px; - div.nav, div.navigation-search { - float: right; + a { + padding-left: 0; + } + } + } - input[type=search] { - padding: 0.2em; - float: left; + div.nav, div.navigation-search { + float: right; - border: none; - } - - button { - color: $chill-light-gray; - background-color: $chill-blue; - padding: 0 0 0 7px; - top: inherit; - font-size: 1.2em; - position: unset; - float: left; - } - } + input[type=search] { + padding: 0.2em; + float: left; + + border: none; + } + + button { + color: $chill-light-gray; + background-color: $chill-blue; + padding: 0 0 0 7px; + top: inherit; + font-size: 1.2em; + position: unset; + float: left; + } + } + + li.user-menu { + min-width: 14rem; + } + + ul.user-menu-list { + + li.user-menu__entry { + display: block; + background-color: $chill-dark-gray; + border-bottom: 1px solid #FFF; + padding-top: 0; + padding-bottom: 0; + line-height: 2; + } + + li.user-menu__entry--warning-entry { + background-color: $chill-red; + font-weight: 700; + } + } + + span.notification-counter { + display: inline-block; + padding: .25em .6em .25rem .6em; + font-size: 100%; + line-height: 1; + text-align: center; + white-space: nowrap; + + border-radius: 10rem; + background-color: $chill-red; + color: $white; + font-weight: 700; + margin-left: .5rem; + } } \ No newline at end of file diff --git a/Resources/test/Fixtures/App/app/AppKernel.php b/Resources/test/Fixtures/App/app/AppKernel.php index 4b9643491..10886518a 100644 --- a/Resources/test/Fixtures/App/app/AppKernel.php +++ b/Resources/test/Fixtures/App/app/AppKernel.php @@ -17,6 +17,8 @@ class AppKernel extends Kernel new Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(), new Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(), new Symfony\Bundle\MonologBundle\MonologBundle(), + new Knp\Bundle\MenuBundle\KnpMenuBundle(), + new Symfony\Bundle\DebugBundle\DebugBundle() ); } diff --git a/Resources/test/Fixtures/App/app/config/config_test.yml b/Resources/test/Fixtures/App/app/config/config_test.yml index b4f6d786a..e4b38ba73 100644 --- a/Resources/test/Fixtures/App/app/config/config_test.yml +++ b/Resources/test/Fixtures/App/app/config/config_test.yml @@ -41,7 +41,6 @@ security: form_login: csrf_parameter: _csrf_token csrf_token_id: authenticate - csrf_provider: form.csrf_provider logout: ~ diff --git a/Resources/translations/messages.fr.yml b/Resources/translations/messages.fr.yml index 06e63ce31..24c4eb710 100644 --- a/Resources/translations/messages.fr.yml +++ b/Resources/translations/messages.fr.yml @@ -31,7 +31,8 @@ not valid: non valide Confirm: Confirmer Cancel: Annuler Save: Enregistrer - +This form contains errors: Ce formulaire contient des erreurs +Choose an user: Choisir un utilisateur 'You are going to leave a page with unsubmitted data. Are you sure you want to leave ?': "Vous allez quitter la page alors que des données n'ont pas été enregistrées. Êtes vous sûr de vouloir partir ?" Edit: Modifier @@ -63,7 +64,9 @@ Advanced search: Recherche avancée #admin Create: Créer show: voir +Show: Voir edit: modifier +Edit: Modifier Main admin menu: Menu d'administration principal Actions: Actions Users and permissions: Utilisateurs et permissions @@ -179,4 +182,9 @@ column: colonne Comma separated values (CSV): Valeurs séparées par des virgules (CSV - tableur) # spreadsheet formatter -Choose the format: Choisir le format \ No newline at end of file +Choose the format: Choisir le format + +# select2 +'select2.no_results': Aucun résultat +'select2.error_loading': Erreur de chargement des résultats +'select2.searching': Recherche en cours... \ No newline at end of file diff --git a/Resources/translations/validators.fr.yml b/Resources/translations/validators.fr.yml index 4307be2fb..ba260f2c6 100644 --- a/Resources/translations/validators.fr.yml +++ b/Resources/translations/validators.fr.yml @@ -5,4 +5,7 @@ The role "%role%" should not be associated with a scope.: Le rôle "%role%" ne d "The password must contains one letter, one capitalized letter, one number and one special character as *[@#$%!,;:+\"'-/{}~=µ()£]). Other characters are allowed.": "Le mot de passe doit contenir une majuscule, une minuscule, et au moins un caractère spécial parmi *[@#$%!,;:+\"'-/{}~=µ()£]). Les autres caractères sont autorisés." The password fields must match: Les mots de passe doivent correspondre -A permission is already present for the same role and scope: Une permission est déjà présente pour le même rôle et cercle. \ No newline at end of file +A permission is already present for the same role and scope: Une permission est déjà présente pour le même rôle et cercle. + +#UserCircleConsistency +"{{ username }} is not allowed to see entities published in this circle": "{{ username }} n'est pas autorisé à voir l'élément publié dans ce cercle." \ No newline at end of file diff --git a/Resources/views/Export/download.html.twig b/Resources/views/Export/download.html.twig index d1f670d74..87c8b7989 100644 --- a/Resources/views/Export/download.html.twig +++ b/Resources/views/Export/download.html.twig @@ -27,50 +27,8 @@ window.addEventListener("DOMContentLoaded", function(e) { query = window.location.search, container = document.querySelector("#download_container") ; - - window.fetch(url+query, { credentials: 'same-origin' }) - .then(function(response) { - if (!response.ok) { - throw Error(response.statusText); - } - - return response.blob(); - }).then(function(blob) { - var content = URL.createObjectURL(blob), - link = document.createElement("a"), - suffix_file; - - switch (blob.type) { - case 'application/vnd.oasis.opendocument.spreadsheet': - suffix_file = '.ods'; - break; - case 'text/csv': - suffix_file = '.csv'; - break; - case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': - suffix_file = '.xlsx'; - break; - default: - suffix_file = ''; - } - - link.appendChild(document.createTextNode("{{ "Download your report"|trans }}")); - link.classList.add("sc-button", "btn-action"); - link.href = content; - link.download = "{{ alias }}"+suffix_file; - let waiting_text = container.querySelector("#waiting_text"); - container.removeChild(waiting_text); - container.appendChild(link); - }).catch(function(error) { - var problem_text = - document.createTextNode("{{ "Problem during download"|trans }}"); - - container - .replaceChild(problem_text, container.firstChild); - }) - ; - + chill.download_report(url+query, container); }); {% endblock %} @@ -79,6 +37,6 @@ window.addEventListener("DOMContentLoaded", function(e) {

{{ "Download export"|trans }}

-
{{ "Waiting for your report"|trans }}...
+
{{ "Waiting for your report"|trans }}...
{% endblock %} \ No newline at end of file diff --git a/Resources/views/Form/fields.html.twig b/Resources/views/Form/fields.html.twig index 099a42294..841dd72e2 100644 --- a/Resources/views/Form/fields.html.twig +++ b/Resources/views/Form/fields.html.twig @@ -61,7 +61,11 @@
{% endfor %} - + {% endspaceless %} {% endblock choice_widget_expanded %} @@ -90,11 +94,20 @@ {%- endif -%} {% endfor %} {% endspaceless %} {% endblock choice_with_other_widget %} +{% block money_widget %} +
+ {{ block('form_widget_simple') }} + {{ money_pattern|form_encode_currency }} +
+{% endblock money_widget %} + {% block date_widget %} {% spaceless %} @@ -128,9 +141,9 @@ {% block form_errors %} {% spaceless %} {% if errors|length > 0 %} -
    +
      {% for error in errors %} -
    • {{ error.message }}
    • +
    • {{ error.message }}
    • {% endfor %}
    {% endif %} @@ -152,3 +165,21 @@ {{ form_row(form.order) }} {% endblock %} + +{% block chill_collection_widget %} +
    +
      + {% for entry in form %} +
    • +
      + {{ form_widget(entry) }} +
      +
    • + {% endfor %} +
    + + {% if form.vars.allow_add == 1 %} + +
    + {% endif %} +{% endblock %} \ No newline at end of file diff --git a/Resources/views/Login/login.html.twig b/Resources/views/Login/login.html.twig index a4c3d622c..248ea0e5d 100644 --- a/Resources/views/Login/login.html.twig +++ b/Resources/views/Login/login.html.twig @@ -23,81 +23,16 @@ {{ 'Login to %installation_name%' | trans({ '%installation_name%' : installation.name } ) }} - - {% stylesheets output="css/login.css" filter="cssrewrite" - - "bundles/chillmain/fonts/OpenSans/OpenSans.css" %} - - {% endstylesheets %} - - + +
    - + -

    {{ error|trans }}

    + {% if error is not null %} +

    {{ error.message|trans }}

    + {% endif %}
    diff --git a/Resources/views/Menu/section.html.twig b/Resources/views/Menu/section.html.twig index 7d909ca17..db6db3b1a 100644 --- a/Resources/views/Menu/section.html.twig +++ b/Resources/views/Menu/section.html.twig @@ -21,16 +21,18 @@ Sections