mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-06-07 18:44:08 +00:00
Merge branch 'upgrade-sf3' into edit-user-password
This commit is contained in:
commit
af803cc87d
@ -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:
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
;
|
||||
|
89
Controller/PostalCodeController.php
Normal file
89
Controller/PostalCodeController.php
Normal file
@ -0,0 +1,89 @@
|
||||
<?php
|
||||
/*
|
||||
* Chill is a software for social workers
|
||||
*
|
||||
* Copyright (C) 2018, Champs Libres Cooperative SCRLFS, <http://www.champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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é <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
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 ] ]);
|
||||
}
|
||||
}
|
39
Controller/UIController.php
Normal file
39
Controller/UIController.php
Normal file
@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Controller;
|
||||
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
|
||||
use Chill\MainBundle\Templating\UI\CountNotificationUser;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
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
|
||||
]);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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é <julien.fastre@champs-libres.coop>
|
||||
@ -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"])
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
49
DependencyInjection/CompilerPass/MenuCompilerPass.php
Normal file
49
DependencyInjection/CompilerPass/MenuCompilerPass.php
Normal file
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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é <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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é <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
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)]);
|
||||
}
|
||||
}
|
||||
}
|
41
Doctrine/DQL/JsonbExistsInArray.php
Normal file
41
Doctrine/DQL/JsonbExistsInArray.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/*
|
||||
*
|
||||
*
|
||||
*/
|
||||
namespace Chill\MainBundle\Doctrine\DQL;
|
||||
|
||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||
use Doctrine\ORM\Query\Lexer;
|
||||
use Doctrine\ORM\Query\Parser;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
}
|
111
Doctrine/Type/NativeDateIntervalType.php
Normal file
111
Doctrine/Type/NativeDateIntervalType.php
Normal file
@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace Chill\MainBundle\Doctrine\Type;
|
||||
|
||||
use Doctrine\DBAL\Types\DateIntervalType;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\ConversionException;
|
||||
|
||||
/**
|
||||
* Re-declare the date interval to use the implementation of date interval in
|
||||
* postgreql
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -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())
|
||||
;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
35
Export/ExportElementsProviderInterface.php
Normal file
35
Export/ExportElementsProviderInterface.php
Normal file
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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é <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
interface ExportElementsProviderInterface
|
||||
{
|
||||
/**
|
||||
* @return ExportElementInterface[]
|
||||
*/
|
||||
public function getExportElements();
|
||||
}
|
@ -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();
|
||||
|
85
Form/ChoiceLoader/PostalCodeChoiceLoader.php
Normal file
85
Form/ChoiceLoader/PostalCodeChoiceLoader.php
Normal file
@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs-Libres <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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é <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
61
Form/Type/ChillCollectionType.php
Normal file
61
Form/Type/ChillCollectionType.php
Normal file
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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é <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
92
Form/Type/DataTransformer/DateIntervalTransformer.php
Normal file
92
Form/Type/DataTransformer/DateIntervalTransformer.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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é <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
57
Form/Type/DateIntervalType.php
Normal file
57
Form/Type/DateIntervalType.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
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());
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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
|
||||
));
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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']);
|
||||
})
|
||||
;
|
||||
}
|
||||
|
@ -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
|
||||
));
|
||||
|
||||
|
28
Repository/PostalCodeRepository.php
Normal file
28
Repository/PostalCodeRepository.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs-Libres <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Repository;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class PostalCodeRepository extends \Doctrine\ORM\EntityRepository
|
||||
{
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 }
|
@ -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}
|
||||
|
4
Resources/config/routing/postal-code.yml
Normal file
4
Resources/config/routing/postal-code.yml
Normal file
@ -0,0 +1,4 @@
|
||||
chill_main_postal_code_search:
|
||||
path: /search
|
||||
defaults: { _controller: ChillMainBundle:PostalCode:search }
|
||||
|
@ -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:
|
||||
|
6
Resources/config/services/controller.yml
Normal file
6
Resources/config/services/controller.yml
Normal file
@ -0,0 +1,6 @@
|
||||
services:
|
||||
|
||||
Chill\MainBundle\Controller\:
|
||||
autowire: true
|
||||
resource: '../../../Controller'
|
||||
tags: ['controller.service_arguments']
|
4
Resources/config/services/fixtures.yml
Normal file
4
Resources/config/services/fixtures.yml
Normal file
@ -0,0 +1,4 @@
|
||||
services:
|
||||
Chill\MainBundle\DataFixtures\ORM\:
|
||||
resource: ../../../DataFixtures/ORM
|
||||
tags: [ 'doctrine.fixture.orm' ]
|
@ -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:
|
||||
|
10
Resources/config/services/menu.yml
Normal file
10
Resources/config/services/menu.yml
Normal file
@ -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' }
|
@ -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
|
||||
|
@ -15,4 +15,13 @@ services:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ["@doctrine.orm.entity_manager", getRepository]
|
||||
arguments:
|
||||
- "Chill\\MainBundle\\Entity\\Scope"
|
||||
- "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'
|
||||
|
24
Resources/config/services/routing.yml
Normal file
24
Resources/config/services/routing.yml
Normal file
@ -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 }
|
23
Resources/config/services/security.yml
Normal file
23
Resources/config/services/security.yml
Normal file
@ -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 }
|
||||
|
13
Resources/config/services/validator.yml
Normal file
13
Resources/config/services/validator.yml
Normal file
@ -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" }
|
2
Resources/config/services/widget.yml
Normal file
2
Resources/config/services/widget.yml
Normal file
@ -0,0 +1,2 @@
|
||||
services:
|
||||
Chill\MainBundle\Templating\UI\CountNotificationUser: ~
|
@ -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: ~
|
||||
- NotNull: ~
|
||||
|
33
Resources/migrations/Version20180703191509.php
Normal file
33
Resources/migrations/Version20180703191509.php
Normal file
@ -0,0 +1,33 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* Add index to postal code
|
||||
*/
|
||||
final class Version20180703191509 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
$this->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');
|
||||
|
||||
}
|
||||
}
|
87
Resources/migrations/Version20180709181423.php
Normal file
87
Resources/migrations/Version20180709181423.php
Normal file
@ -0,0 +1,87 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
namespace Application\Migrations;
|
||||
|
||||
use Doctrine\DBAL\Migrations\AbstractMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
|
||||
/**
|
||||
* add username and username canonical, email and email canonical columns
|
||||
* to users
|
||||
*/
|
||||
final class Version20180709181423 extends AbstractMigration
|
||||
{
|
||||
public function up(Schema $schema) : void
|
||||
{
|
||||
$this->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(<<<SQL
|
||||
CREATE TRIGGER canonicalize_user_on_update
|
||||
AFTER UPDATE
|
||||
ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE canonicalize_user_on_update();
|
||||
SQL
|
||||
);
|
||||
|
||||
$this->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(<<<SQL
|
||||
CREATE TRIGGER canonicalize_user_on_insert
|
||||
AFTER INSERT
|
||||
ON users
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE canonicalize_user_on_insert();
|
||||
SQL
|
||||
);
|
||||
}
|
||||
|
||||
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 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()');
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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
|
||||
* <select name="country" class="chill-category-link-parent">
|
||||
* <option value="BE">Belgium</option>
|
||||
* <option value="FR">France</option>
|
||||
* </select>
|
||||
*
|
||||
*
|
||||
* <select name="cities">class="chill-category-link-children">
|
||||
* <option value="paris" data-link-category="FR">Paris</option>
|
||||
* <option value="toulouse" data-link-category="FR">Toulouse</option>
|
||||
@ -207,7 +207,7 @@ var chill = function() {
|
||||
* <option value="mons" data-link-category="BE">Mons</option>
|
||||
* </select>
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* 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 :
|
||||
*
|
||||
* ```
|
||||
* <input data-display-target="export_abc" value="1" type="checkbox">
|
||||
*
|
||||
*
|
||||
* <div data-display-show-hide="export_abc">
|
||||
* <!-- your content here will be hidden / shown according to checked state -->
|
||||
* </div>
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* 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 };
|
||||
|
23
Resources/public/js/collection/collection.scss
Normal file
23
Resources/public/js/collection/collection.scss
Normal file
@ -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 {
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
116
Resources/public/js/collection/collections.js
Normal file
116
Resources/public/js/collection/collections.js
Normal file
@ -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]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
3
Resources/public/modules/breadcrumb/index.js
Normal file
3
Resources/public/modules/breadcrumb/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
require("./layout.scss");
|
||||
|
||||
|
21
Resources/public/modules/breadcrumb/layout.scss
Normal file
21
Resources/public/modules/breadcrumb/layout.scss
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
67
Resources/public/modules/download-report/download-report.js
Normal file
67
Resources/public/modules/download-report/download-report.js
Normal file
@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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;
|
18
Resources/public/modules/download-report/index.js
Normal file
18
Resources/public/modules/download-report/index.js
Normal file
@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
chill.download_report = require("./download-report.js");
|
3
Resources/public/modules/login_page/index.js
Normal file
3
Resources/public/modules/login_page/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
require('./login.scss');
|
||||
|
||||
|
63
Resources/public/modules/login_page/login.scss
Normal file
63
Resources/public/modules/login_page/login.scss
Normal file
@ -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;
|
||||
}
|
||||
|
36
Resources/public/modules/postal-code/index.js
Normal file
36
Resources/public/modules/postal-code/index.js
Normal file
@ -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
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -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;
|
||||
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
// }
|
||||
|
2
Resources/public/sass/custom/_fonts.scss
Normal file
2
Resources/public/sass/custom/_fonts.scss
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
@import './../../fonts/OpenSans/OpenSans';
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,6 @@ security:
|
||||
form_login:
|
||||
csrf_parameter: _csrf_token
|
||||
csrf_token_id: authenticate
|
||||
csrf_provider: form.csrf_provider
|
||||
|
||||
logout: ~
|
||||
|
||||
|
@ -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
|
||||
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...
|
@ -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.
|
||||
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."
|
@ -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);
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -79,6 +37,6 @@ window.addEventListener("DOMContentLoaded", function(e) {
|
||||
|
||||
<h1>{{ "Download export"|trans }}</h1>
|
||||
|
||||
<div id="download_container"><span id="waiting_text">{{ "Waiting for your report"|trans }}...</span></div>
|
||||
<div id="download_container" data-alias="{{ alias|escape('html_attr') }}" {% if mime_type is defined %}data-mime-type="{{ mime_type|escape('html_attr') }}"{% endif %} data-download-text="{{ "Download your report"|trans|escape('html_attr') }}"><span id="waiting_text">{{ "Waiting for your report"|trans }}...</span></div>
|
||||
|
||||
{% endblock %}
|
@ -61,7 +61,11 @@
|
||||
<br/>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<script type="text/javascript">chill.checkNullValuesInChoices("{{ form.vars.full_name }}");</script>
|
||||
<script type="text/javascript">
|
||||
document.addEventListener('load', function(e) {
|
||||
chill.checkNullValuesInChoices("{{ form.vars.full_name }}");
|
||||
});
|
||||
</script>
|
||||
{% endspaceless %}
|
||||
{% endblock choice_widget_expanded %}
|
||||
|
||||
@ -90,11 +94,20 @@
|
||||
{%- endif -%}
|
||||
{% endfor %}
|
||||
<script type="text/javascript">
|
||||
chill.checkNullValuesInChoices("{{ form._choices.vars.full_name }}");
|
||||
document.addEventListener('load', function(e) {
|
||||
chill.checkNullValuesInChoices("{{ form._choices.vars.full_name }}");
|
||||
});
|
||||
</script>
|
||||
{% endspaceless %}
|
||||
{% endblock choice_with_other_widget %}
|
||||
|
||||
{% block money_widget %}
|
||||
<div class="chill-form-money">
|
||||
{{ block('form_widget_simple') }}
|
||||
<span class="chill-form-money__money">{{ money_pattern|form_encode_currency }}</span>
|
||||
</div>
|
||||
{% endblock money_widget %}
|
||||
|
||||
|
||||
{% block date_widget %}
|
||||
{% spaceless %}
|
||||
@ -128,9 +141,9 @@
|
||||
{% block form_errors %}
|
||||
{% spaceless %}
|
||||
{% if errors|length > 0 %}
|
||||
<ul class="errors">
|
||||
<ul class="errors chill-form__errors">
|
||||
{% for error in errors %}
|
||||
<li>{{ error.message }}</li>
|
||||
<li class="chill-form_errors__entry {% if 'severity' in error.cause.constraint.payload|keys %}chill-form__errors__entry--{{ error.cause.constraint.payload.severity }}{% endif %}">{{ error.message }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
@ -152,3 +165,21 @@
|
||||
{{ form_row(form.order) }}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block chill_collection_widget %}
|
||||
<div class="chill-collection">
|
||||
<ul class="chill-collection__list" data-collection-name="{{ form.vars.name|escape('html_attr') }}" data-collection-identifier="{{ form.vars.identifier|escape('html_attr') }}" data-collection-button-remove-label="{{ form.vars.button_remove_label|trans|e }}" data-collection-allow-add="{{ form.vars.allow_add|escape('html_attr') }}" data-collection-allow-delete="{{ form.vars.allow_delete|escape('html_attr') }}" >
|
||||
{% for entry in form %}
|
||||
<li class="chill-collection__list__entry" data-collection-is-persisted="1">
|
||||
<div>
|
||||
{{ form_widget(entry) }}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if form.vars.allow_add == 1 %}
|
||||
<button class="chill-collection__button--add sc-button" data-collection-add-target="{{ form.vars.name|escape('html_attr') }}" data-form-prototype="{{ ('<div>' ~ form_widget(form.vars.prototype) ~ '</div>')|escape('html_attr') }}" >{{ form.vars.button_add_label|trans }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
@ -23,81 +23,16 @@
|
||||
<title>
|
||||
{{ 'Login to %installation_name%' | trans({ '%installation_name%' : installation.name } ) }}
|
||||
</title>
|
||||
<link rel="shortcut icon" href="/bundles/chillmain/img/favicon.ico" type="image/x-icon">
|
||||
{% stylesheets output="css/login.css" filter="cssrewrite"
|
||||
|
||||
"bundles/chillmain/fonts/OpenSans/OpenSans.css" %}
|
||||
<link rel="stylesheet" href="{{ asset_url }}"/>
|
||||
{% endstylesheets %}
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #333;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#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("/bundles/chillmain/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-family: 'Open Sans';
|
||||
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;
|
||||
}
|
||||
</style>
|
||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||
<link rel="stylesheet" href="{{ asset('build/login.css') }}"/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="content">
|
||||
<img class="logo" src="/bundles/chillmain/img/logo-chill-outil-accompagnement_white.png">
|
||||
<img class="logo" src="{{ asset('build/images/logo-chill-outil-accompagnement_white.png') }}">
|
||||
|
||||
<p>{{ error|trans }}</p>
|
||||
{% if error is not null %}
|
||||
<p>{{ error.message|trans }}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="POST" action="{{ path('login_check') }}">
|
||||
<label for="_username">{{ 'Username'|trans }}</label>
|
||||
|
@ -21,16 +21,18 @@
|
||||
<a href="javascript:void(0)" class="more">Sections</a>
|
||||
</div>
|
||||
<ul class="submenu width-15-em" style="padding-left: 0; padding-right: 0; background-color:transparent;">
|
||||
{% for route in routes %}
|
||||
{% for menu in menus %}
|
||||
<li style="display:block; background-color: #333333; padding-left:1.5em; border-bottom:1px; border-bottom: 1px solid #FFF;padding-top:0; padding-bottom:0;">
|
||||
<div style="margin-bottom:2px;">
|
||||
<div style="font-family: 'Open Sans'; font-weight:300; font-size: 0.75em; text-align:left; height: 46px; display:inline-block; width: calc(100% - 5em - 1px); vertical-align:top;">
|
||||
<a href="{{ path(route.key, args ) }}">{{ route.label|trans }}</a>
|
||||
<a href="{{ menu.uri }}">{{ menu.label|trans }}</a>
|
||||
</div>
|
||||
<div style="background-color: #333333; text-align:center;width: 2em; margin-left:-0.15em; font-size:1.5em; color:#FFF; height: 46px; display:inline-block; vertical-align:top;float:right">{% spaceless %}
|
||||
{% for icon in route.icons %}
|
||||
{% if menu.extras.icons is defined %}
|
||||
{% for icon in menu.extras.icons %}
|
||||
<i class="fa fa-{{ icon }}"></i>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endspaceless %}</div>
|
||||
</div>
|
||||
</li>
|
||||
|
@ -1,5 +1,5 @@
|
||||
{#
|
||||
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
|
||||
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
|
||||
<info@champs-libres.coop> / <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@ -16,16 +16,27 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#}
|
||||
|
||||
<li class="nav-link2">
|
||||
<li class="nav-link2 user-menu">
|
||||
<div class="li-content">
|
||||
<a href="javascript:void(0)" style="font-size: 0.8em; font-family: 'Open Sans'; font-weight:300;">
|
||||
{{ 'Welcome' | trans }}<br/>
|
||||
<b>{{ app.user.username }}</b>
|
||||
<b>{{ app.user.username }}{{ render(controller('ChillMainBundle:UI:showNotificationUserCounter')) }}</b>
|
||||
</a>
|
||||
</div>
|
||||
<ul class="submenu width-11-em">
|
||||
{% for route in routes %}
|
||||
<li><a href="{{ path(route.key, args ) }}" style="font-family: 'Open Sans'; font-weight:300; font-size: 0.9em;"><i class="fa fa-{{ route.icon }}"></i> {{ route.label|trans }}</a></li>
|
||||
<ul class="submenu width-11-em user-menu-list" style="padding-left: 0; padding-right: 0; background-color:transparent;">
|
||||
{% for menu in menus %}
|
||||
<li style="display:block; background-color: #333333; padding-left:1.5em; border-bottom:1px; border-bottom: 1px solid #FFF;padding-top:0; padding-bottom:0;">
|
||||
<div style="margin-bottom:2px;">
|
||||
<div style="font-family: 'Open Sans'; font-weight:300; font-size: 0.75em; text-align:left; height: 46px; display:inline-block; width: calc(100% - 5em - 1px); vertical-align:top;">
|
||||
<a href="{{ menu.uri }}">{{ menu.label|trans }}</a>
|
||||
</div>
|
||||
<div style="background-color: #333333; text-align:center;width: 2em; margin-left:-0.15em; font-size:1.5em; color:#FFF; height: 46px; display:inline-block; vertical-align:top;float:right">{% spaceless %}
|
||||
{% if menu.extras.icon is defined %}
|
||||
<i class="fa fa-{{ menu.extras.icon }}"></i>
|
||||
{% endif %}
|
||||
{% endspaceless %}</div>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
|
1
Resources/views/UI/notification_user_counter.html.twig
Normal file
1
Resources/views/UI/notification_user_counter.html.twig
Normal file
@ -0,0 +1 @@
|
||||
{% if nb > 0 %}<span class="notification-counter">{{ nb }}{% endif %}
|
@ -8,6 +8,7 @@
|
||||
{{ form_start(edit_form) }}
|
||||
|
||||
{{ form_row(edit_form.username) }}
|
||||
{{ form_row(edit_form.email) }}
|
||||
{{ form_row(edit_form.enabled, { 'label': "User'status"}) }}
|
||||
|
||||
{{ form_widget(edit_form.submit, { 'attr': { 'class' : 'sc-button green center' } } ) }}
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.username) }}
|
||||
{{ form_row(form.email) }}
|
||||
{{ form_row(form.plainPassword.password) }}
|
||||
{{ form_widget(form.submit, { 'attr' : { 'class': 'sc-button blue' } }) }}
|
||||
{{ form_end(form) }}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{#
|
||||
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
|
||||
* Copyright (C) 2014-2015, Champs Libres Cooperative SCRLFS,
|
||||
<info@champs-libres.coop> / <http://www.champs-libres.coop>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
@ -22,19 +22,13 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{{ installation.name }} - {% block title %}{% endblock %}</title>
|
||||
<link rel="shortcut icon" href="/bundles/chillmain/img/favicon.ico" type="image/x-icon">
|
||||
|
||||
{% stylesheets output="css/all.css" filter="cssrewrite"
|
||||
"bundles/chillmain/css/scratch.css"
|
||||
"bundles/chillmain/css/chillmain.css"
|
||||
"bundles/chillmain/css/select2/select2.css"
|
||||
"bundles/chillmain/fonts/OpenSans/OpenSans.css"
|
||||
"bundles/chillmain/css/pikaday.css" %}
|
||||
<link rel="stylesheet" href="{{ asset_url }}"/>
|
||||
{% endstylesheets %}
|
||||
|
||||
<link rel="shortcut icon" href="{{ asset('build/images/favicon.ico') }}" type="image/x-icon">
|
||||
<link rel="stylesheet" href="{{ asset('build/chill.css') }}"/>
|
||||
{% block css%}<!-- nothing added to css -->{% endblock %}
|
||||
</head>
|
||||
|
||||
@ -43,7 +37,7 @@
|
||||
<div class="grid-4 hide-tablet hide-mobile parent">
|
||||
<div class="grid-10 push-2 grid-tablet-12 grid-mobile-12 push-tablet-0 grid-mobile-0 logo-container">
|
||||
<a href="{{ path('chill_main_homepage') }}">
|
||||
<img class="logo" src="/bundles/chillmain/img/logo-chill-sans-slogan_white.png">
|
||||
<img class="logo" src="{{ asset('build/images/logo-chill-sans-slogan_white.png') }}">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -107,7 +101,7 @@
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{% for flashMessage in app.session.flashbag.get('error') %}
|
||||
<div class="grid-8 centered error flash_message">
|
||||
<span>
|
||||
@ -115,14 +109,14 @@
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{% for flashMessage in app.session.flashbag.get('notice') %}
|
||||
<div class="grid-8 centered notice flash_message">
|
||||
<span>
|
||||
{{ flashMessage|raw }}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
@ -137,12 +131,12 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="homepage_widget">
|
||||
{{ chill_widget('homepage', {} ) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
@ -150,16 +144,8 @@
|
||||
<p>{{ 'This program is free software: you can redistribute it and/or modify it under the terms of the <strong>GNU Affero General Public License</strong>'|trans|raw }}
|
||||
<br/> <a href="https://{{ app.request.locale }}.wikibooks.org/wiki/Chill">{{ 'User manual'|trans }}</a></p>
|
||||
</footer>
|
||||
|
||||
{% javascripts output="js/libs.js"
|
||||
"bundles/chillmain/js/jquery.js"
|
||||
"bundles/chillmain/js/moment.js"
|
||||
"bundles/chillmain/js/pikaday/pikaday.js"
|
||||
"bundles/chillmain/js/select2/select2.js"
|
||||
"bundles/chillmain/js/pikaday/plugins/pikaday.jquery.js"
|
||||
"bundles/chillmain/js/chill.js" %}
|
||||
<script src="{{ asset_url }}" type="text/javascript"></script>
|
||||
{% endjavascripts %}
|
||||
|
||||
<script type="text/javascript" src="{{ asset('build/chill.js') }}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
chill.initPikaday('{{ app.request.locale }}');
|
||||
|
38
Routing/LocalMenuBuilderInterface.php
Normal file
38
Routing/LocalMenuBuilderInterface.php
Normal file
@ -0,0 +1,38 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Routing;
|
||||
|
||||
use Knp\Menu\MenuItem;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
interface LocalMenuBuilderInterface
|
||||
{
|
||||
/**
|
||||
* return an array of menu ids
|
||||
*
|
||||
* @internal this method is static to 1. keep all config in the class,
|
||||
* instead of tags arguments; 2. avoid a "supports" method, which could lead
|
||||
* to parsing all instances to get only one or two working instance.
|
||||
*/
|
||||
public static function getMenuIds(): array;
|
||||
|
||||
public function buildMenu($menuId, MenuItem $menu, array $parameters);
|
||||
}
|
68
Routing/MenuBuilder/SectionMenuBuilder.php
Normal file
68
Routing/MenuBuilder/SectionMenuBuilder.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Routing\MenuBuilder;
|
||||
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
use Knp\Menu\MenuItem;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Chill\MainBundle\Security\Authorization\ChillExportVoter;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class SectionMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationCheckerInterface
|
||||
*/
|
||||
protected $authorizationChecker;
|
||||
|
||||
public function __construct(AuthorizationCheckerInterface $authorizationChecker)
|
||||
{
|
||||
$this->authorizationChecker = $authorizationChecker;
|
||||
}
|
||||
|
||||
public function buildMenu($menuId, MenuItem $menu, array $parameters)
|
||||
{
|
||||
$menu->addChild('Homepage', [
|
||||
'route' => 'chill_main_homepage'
|
||||
])
|
||||
->setExtras([
|
||||
'icons' => ['home'],
|
||||
'order' => 0
|
||||
]);
|
||||
|
||||
if ($this->authorizationChecker->isGranted(ChillExportVoter::EXPORT)) {
|
||||
$menu->addChild('Export Menu', [
|
||||
'route' => 'chill_main_export_index'
|
||||
])
|
||||
->setExtras([
|
||||
'icons' => ['upload'],
|
||||
'order' => 20
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public static function getMenuIds(): array
|
||||
{
|
||||
return [ 'section' ];
|
||||
}
|
||||
}
|
46
Routing/MenuBuilder/UserMenuBuilder.php
Normal file
46
Routing/MenuBuilder/UserMenuBuilder.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Routing\MenuBuilder;
|
||||
|
||||
use Chill\MainBundle\Routing\LocalMenuBuilderInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class UserMenuBuilder implements LocalMenuBuilderInterface
|
||||
{
|
||||
public function buildMenu($menuId, \Knp\Menu\MenuItem $menu, array $parameters)
|
||||
{
|
||||
$menu->addChild(
|
||||
'Logout',
|
||||
[
|
||||
'route' => 'logout'
|
||||
])
|
||||
->setExtras([
|
||||
'order'=> 99999999999,
|
||||
'icon' => 'power-off'
|
||||
]);
|
||||
}
|
||||
|
||||
public static function getMenuIds(): array
|
||||
{
|
||||
return [ 'user' ];
|
||||
}
|
||||
}
|
@ -3,8 +3,11 @@
|
||||
namespace Chill\MainBundle\Routing;
|
||||
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Knp\Menu\FactoryInterface;
|
||||
use Knp\Menu\ItemInterface;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
|
||||
/**
|
||||
* This class permit to build menu from the routing information
|
||||
@ -14,27 +17,42 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
*
|
||||
* @author julien
|
||||
*/
|
||||
class MenuComposer implements ContainerAwareInterface
|
||||
class MenuComposer
|
||||
{
|
||||
|
||||
/**
|
||||
*
|
||||
* @var ContainerInterface
|
||||
* @var RouterInterface
|
||||
*/
|
||||
private $container;
|
||||
|
||||
private $router;
|
||||
|
||||
/**
|
||||
*
|
||||
* @internal using the service router in container cause circular references
|
||||
* @param ContainerInterface $container
|
||||
*
|
||||
* @var FactoryInterface
|
||||
*/
|
||||
public function setContainer(ContainerInterface $container = null)
|
||||
{
|
||||
if (NULL === $container) {
|
||||
throw new \LogicException('container should not be null');
|
||||
}
|
||||
//see remark in MenuComposer::setRouteCollection
|
||||
$this->container = $container;
|
||||
private $menuFactory;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var TranslatorInterface
|
||||
*/
|
||||
private $translator;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var
|
||||
*/
|
||||
private $localMenuBuilders = [];
|
||||
|
||||
|
||||
function __construct(
|
||||
RouterInterface $router,
|
||||
FactoryInterface $menuFactory,
|
||||
TranslatorInterface $translator
|
||||
) {
|
||||
$this->router = $router;
|
||||
$this->menuFactory = $menuFactory;
|
||||
$this->translator = $translator;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -61,10 +79,11 @@ class MenuComposer implements ContainerAwareInterface
|
||||
public function getRoutesFor($menuId, array $parameters = array())
|
||||
{
|
||||
$routes = array();
|
||||
$routeCollection = $this->container->get('router')->getRouteCollection();
|
||||
$routeCollection = $this->router->getRouteCollection();
|
||||
|
||||
foreach ($routeCollection->all() as $routeKey => $route) {
|
||||
if ($route->hasOption('menus')) {
|
||||
|
||||
if (array_key_exists($menuId, $route->getOption('menus'))) {
|
||||
$route = $route->getOption('menus')[$menuId];
|
||||
|
||||
@ -83,6 +102,37 @@ class MenuComposer implements ContainerAwareInterface
|
||||
return $routes;
|
||||
}
|
||||
|
||||
public function getMenuFor($menuId, array $parameters = array())
|
||||
{
|
||||
$routes = $this->getRoutesFor($menuId, $parameters);
|
||||
$menu = $this->menuFactory->createItem($menuId);
|
||||
|
||||
// build menu from routes
|
||||
foreach ($routes as $order => $route) {
|
||||
$menu->addChild($this->translator->trans($route['label']), [
|
||||
'route' => $route['key'],
|
||||
'routeParameters' => $parameters['args'],
|
||||
'order' => $order
|
||||
])
|
||||
->setExtras([
|
||||
'icon' => $route['icon'],
|
||||
'order' => $order
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
if ($this->hasLocalMenuBuilder($menuId)) {
|
||||
foreach ($this->localMenuBuilders[$menuId] as $builder) {
|
||||
/* @var $builder LocalMenuBuilderInterface */
|
||||
$builder->buildMenu($menuId, $menu, $parameters['args']);
|
||||
}
|
||||
}
|
||||
|
||||
$this->reorderMenu($menu);
|
||||
|
||||
return $menu;
|
||||
}
|
||||
|
||||
/**
|
||||
* recursive function to resolve the order of a array of routes.
|
||||
* If the order chosen in routing.yml is already in used, find the
|
||||
@ -92,12 +142,55 @@ class MenuComposer implements ContainerAwareInterface
|
||||
* @param int $order
|
||||
* @return int
|
||||
*/
|
||||
private function resolveOrder($routes, $order){
|
||||
private function resolveOrder($routes, $order)
|
||||
{
|
||||
if (isset($routes[$order])) {
|
||||
return $this->resolveOrder($routes, $order + 1);
|
||||
} else {
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
|
||||
private function reorderMenu(ItemInterface $menu)
|
||||
{
|
||||
$ordered = [];
|
||||
$unordered = [];
|
||||
|
||||
foreach ($menu->getChildren() as $name => $item) {
|
||||
$order = $item->getExtra('order');
|
||||
|
||||
if ($order !== null) {
|
||||
$ordered[$this->resolveOrder($ordered, $order)] = $name;
|
||||
} else {
|
||||
$unordered = $name;
|
||||
}
|
||||
}
|
||||
|
||||
ksort($ordered);
|
||||
|
||||
$menus = \array_merge(\array_values($ordered), $unordered);
|
||||
$menu->reorderChildren($menus);
|
||||
}
|
||||
|
||||
|
||||
public function addLocalMenuBuilder(LocalMenuBuilderInterface $menuBuilder, $menuId)
|
||||
{
|
||||
$this->localMenuBuilders[$menuId][] = $menuBuilder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the menu has at least one builder.
|
||||
*
|
||||
* This function is a helper to determine if the method `getMenuFor`
|
||||
* should be used, or `getRouteFor`. The method `getMenuFor` should be used
|
||||
* if the result is true (it **does** exists at least one menu builder.
|
||||
*
|
||||
* @param string $menuId
|
||||
* @return bool
|
||||
*/
|
||||
public function hasLocalMenuBuilder($menuId): bool
|
||||
{
|
||||
return \array_key_exists($menuId, $this->localMenuBuilders);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -64,7 +64,10 @@ class MenuTwig extends \Twig_Extension implements ContainerAwareInterface
|
||||
public function getFunctions()
|
||||
{
|
||||
return [new \Twig_SimpleFunction('chill_menu',
|
||||
array($this, 'chillMenu'), array('is_safe' => array('html')))
|
||||
array($this, 'chillMenu'), array(
|
||||
'is_safe' => array('html'),
|
||||
'needs_environment' => true
|
||||
))
|
||||
];
|
||||
}
|
||||
|
||||
@ -81,17 +84,22 @@ class MenuTwig extends \Twig_Extension implements ContainerAwareInterface
|
||||
* @param string $menuId
|
||||
* @param mixed[] $params
|
||||
*/
|
||||
public function chillMenu($menuId, array $params = array())
|
||||
public function chillMenu(\Twig_Environment $env, $menuId, array $params = array())
|
||||
{
|
||||
$resolvedParams = array_merge($this->defaultParams, $params);
|
||||
|
||||
$layout = $resolvedParams['layout'];
|
||||
unset($resolvedParams['layout']);
|
||||
|
||||
$resolvedParams['routes'] = $this->menuComposer->getRoutesFor($menuId);
|
||||
if ($this->menuComposer->hasLocalMenuBuilder($menuId) === false) {
|
||||
$resolvedParams['routes'] = $this->menuComposer->getRoutesFor($menuId, $resolvedParams);
|
||||
|
||||
return $this->container->get('templating')
|
||||
->render($layout, $resolvedParams);
|
||||
return $env->render($layout, $resolvedParams);
|
||||
} else {
|
||||
$resolvedParams['menus'] = $this->menuComposer->getMenuFor($menuId, $resolvedParams);
|
||||
|
||||
return $env->render($layout, $resolvedParams);
|
||||
}
|
||||
}
|
||||
|
||||
public function getName()
|
||||
|
@ -26,6 +26,10 @@ use Chill\MainBundle\Entity\HasScopeInterface;
|
||||
use Symfony\Component\Security\Core\Role\RoleHierarchyInterface;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
use Chill\MainBundle\Entity\Scope;
|
||||
use Chill\MainBundle\Security\RoleProvider;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Chill\MainBundle\Entity\GroupCenter;
|
||||
use Chill\MainBundle\Entity\RoleScope;
|
||||
|
||||
/**
|
||||
* Helper for authorizations.
|
||||
@ -42,12 +46,28 @@ class AuthorizationHelper
|
||||
*/
|
||||
protected $roleHierarchy;
|
||||
|
||||
protected $existingRoles = array('CHILL_MASTER_ROLE', 'CHILL_PERSON_SEE',
|
||||
'CHILL_PERSON_UPDATE',);
|
||||
/**
|
||||
* The role in a hierarchy, given by the parameter
|
||||
* `security.role_hierarchy.roles` from the container.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $hierarchy;
|
||||
|
||||
public function __construct(RoleHierarchyInterface $roleHierarchy)
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(
|
||||
RoleHierarchyInterface $roleHierarchy,
|
||||
$hierarchy,
|
||||
EntityManagerInterface $em
|
||||
) {
|
||||
$this->roleHierarchy = $roleHierarchy;
|
||||
$this->hierarchy = $hierarchy;
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -207,7 +227,40 @@ class AuthorizationHelper
|
||||
return $scopes;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param Role $role
|
||||
* @param Center $center
|
||||
* @param Scope $circle
|
||||
* @return Users
|
||||
*/
|
||||
public function findUsersReaching(Role $role, Center $center, Scope $circle = null)
|
||||
{
|
||||
$parents = $this->getParentRoles($role);
|
||||
$parents[] = $role;
|
||||
$parentRolesString = \array_map(function(Role $r) { return $r->getRole(); }, $parents);
|
||||
|
||||
$qb = $this->em->createQueryBuilder();
|
||||
$qb
|
||||
->select('u')
|
||||
->from(User::class, 'u')
|
||||
->join('u.groupCenters', 'gc')
|
||||
->join('gc.permissionsGroup', 'pg')
|
||||
->join('pg.roleScopes', 'rs')
|
||||
->where('gc.center = :center')
|
||||
->andWhere($qb->expr()->in('rs.role', $parentRolesString))
|
||||
;
|
||||
|
||||
$qb->setParameter('center', $center);
|
||||
|
||||
if ($circle !== null) {
|
||||
$qb->andWhere('rs.scope = :circle')
|
||||
->setParameter('circle', $circle)
|
||||
;
|
||||
}
|
||||
|
||||
return $qb->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if a parent role may give access to a given child role
|
||||
@ -223,4 +276,33 @@ class AuthorizationHelper
|
||||
|
||||
return in_array($childRole, $reachableRoles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all the role which give access to the given role. Only the role
|
||||
* which are registered into Chill are taken into account.
|
||||
*
|
||||
* @param Role $role
|
||||
* @return Role[] the role which give access to the given $role
|
||||
*/
|
||||
public function getParentRoles(Role $role)
|
||||
{
|
||||
$parentRoles = [];
|
||||
// transform the roles from role hierarchy from string to Role
|
||||
$roles = \array_map(
|
||||
function($string) {
|
||||
return new Role($string);
|
||||
},
|
||||
\array_keys($this->hierarchy)
|
||||
);
|
||||
|
||||
foreach ($roles as $r) {
|
||||
$childRoles = $this->roleHierarchy->getReachableRoles([$r]);
|
||||
|
||||
if (\in_array($role, $childRoles)) {
|
||||
$parentRoles[] = $r;
|
||||
}
|
||||
}
|
||||
|
||||
return $parentRoles;
|
||||
}
|
||||
}
|
||||
|
62
Security/Authorization/ChillExportVoter.php
Normal file
62
Security/Authorization/ChillExportVoter.php
Normal file
@ -0,0 +1,62 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Security\Authorization;
|
||||
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Security\Core\Role\Role;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class ChillExportVoter extends Voter
|
||||
{
|
||||
const EXPORT = 'chill_export';
|
||||
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $authorizationHelper;
|
||||
|
||||
public function __construct(AuthorizationHelper $authorizationHelper)
|
||||
{
|
||||
$this->authorizationHelper = $authorizationHelper;
|
||||
}
|
||||
|
||||
protected function supports($attribute, $subject): bool
|
||||
{
|
||||
return $attribute === self::EXPORT;
|
||||
}
|
||||
|
||||
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
|
||||
{
|
||||
if (!$token->getUser() instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$centers = $this->authorizationHelper
|
||||
->getReachableCenters($token->getUser(), new Role($attribute));
|
||||
|
||||
return count($centers) > 0;
|
||||
}
|
||||
}
|
83
Security/UserProvider/UserProvider.php
Normal file
83
Security/UserProvider/UserProvider.php
Normal file
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Security\UserProvider;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
|
||||
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class UserProvider implements UserProviderInterface
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
|
||||
public function loadUserByUsername($username): UserInterface
|
||||
{
|
||||
$user = $this->em->createQuery(sprintf(
|
||||
"SELECT u FROM %s u "
|
||||
. "WHERE u.usernameCanonical = UNACCENT(LOWER(:pattern)) "
|
||||
. "OR "
|
||||
. "u.emailCanonical = UNACCENT(LOWER(:pattern))",
|
||||
User::class))
|
||||
->setParameter('pattern', $username)
|
||||
->getSingleResult();
|
||||
|
||||
if (NULL === $user) {
|
||||
throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function refreshUser(UserInterface $user): UserInterface
|
||||
{
|
||||
if (!$user instanceof User) {
|
||||
throw new UnsupportedUserException("Unsupported user class: cannot reload this user");
|
||||
}
|
||||
|
||||
$reloadedUser = $this->em->getRepository(User::class)->find($user->getId());
|
||||
|
||||
if (NULL === $reloadedUser) {
|
||||
throw new UsernameNotFoundException(sprintf('User with ID "%s" could not be reloaded.', $user->getId()));
|
||||
}
|
||||
|
||||
return $reloadedUser;
|
||||
}
|
||||
|
||||
public function supportsClass($class): bool
|
||||
{
|
||||
return $class === User::class;
|
||||
}
|
||||
}
|
51
Templating/UI/CountNotificationUser.php
Normal file
51
Templating/UI/CountNotificationUser.php
Normal file
@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Templating\UI;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
|
||||
/**
|
||||
* Show a number of notification to user in the upper right corner
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class CountNotificationUser
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var NotificationCounterInterface[]
|
||||
*/
|
||||
protected $counters = [];
|
||||
|
||||
public function addNotificationCounter(NotificationCounterInterface $counter)
|
||||
{
|
||||
$this->counters[] = $counter;
|
||||
}
|
||||
|
||||
public function getSumNotification(UserInterface $u): int
|
||||
{
|
||||
$sum = 0;
|
||||
|
||||
foreach ($this->counters as $counter) {
|
||||
$sum += $counter->addNotification($u);
|
||||
}
|
||||
|
||||
return $sum;
|
||||
}
|
||||
}
|
32
Templating/UI/NotificationCounterInterface.php
Normal file
32
Templating/UI/NotificationCounterInterface.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Templating\UI;
|
||||
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
interface NotificationCounterInterface
|
||||
{
|
||||
/**
|
||||
* Add a number of notification
|
||||
*/
|
||||
public function addNotification(UserInterface $u): int;
|
||||
}
|
@ -443,6 +443,36 @@ class AuthorizationHelperTest extends KernelTestCase
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetParentRoles()
|
||||
{
|
||||
$parentRoles = $this->getAuthorizationHelper()
|
||||
->getParentRoles(new Role('CHILL_INHERITED_ROLE_1'));
|
||||
|
||||
$this->assertContains(
|
||||
'CHILL_MASTER_ROLE',
|
||||
\array_map(
|
||||
function(Role $role) {
|
||||
return $role->getRole();
|
||||
},
|
||||
$parentRoles
|
||||
),
|
||||
"Assert that `CHILL_MASTER_ROLE` is a parent of `CHILL_INHERITED_ROLE_1`");
|
||||
}
|
||||
|
||||
public function testFindUsersReaching()
|
||||
{
|
||||
$centerA = static::$kernel->getContainer()
|
||||
->get('doctrine.orm.entity_manager')
|
||||
->getRepository(Center::class)
|
||||
->findOneByName('Center A');
|
||||
|
||||
$users = $this->getAuthorizationHelper()
|
||||
->findUsersReaching(new Role('CHILL_PERSON_SEE'),
|
||||
$centerA);
|
||||
|
||||
$usernames = \array_map(function(User $u) { return $u->getUsername(); }, $users);
|
||||
|
||||
$this->assertContains('center a_social', $usernames);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Chill\MainBundle\Validation\Validator\UserUniqueEmailAndUsername;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class UserUniqueEmailAndUsernameConstraint extends Constraint
|
||||
{
|
||||
public $messageDuplicateUsername = "A user with the same or a close username already exists";
|
||||
public $messageDuplicateEmail = "A user with the same or a close email already exists";
|
||||
|
||||
public function validatedBy()
|
||||
{
|
||||
return UserUniqueEmailAndUsername::class;
|
||||
}
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return [ self::CLASS_CONSTRAINT ];
|
||||
}
|
||||
}
|
117
Validation/Validator/UserUniqueEmailAndUsername.php
Normal file
117
Validation/Validator/UserUniqueEmailAndUsername.php
Normal file
@ -0,0 +1,117 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Validation\Validator;
|
||||
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author Julien Fastré <julien.fastre@champs-libres.coop>
|
||||
*/
|
||||
class UserUniqueEmailAndUsername extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var EntityManagerInterface
|
||||
*/
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
{
|
||||
$this->em = $em;
|
||||
}
|
||||
|
||||
|
||||
public function validate($value, Constraint $constraint)
|
||||
{
|
||||
if (!$value instanceof User) {
|
||||
throw new \UnexpectedValueException("This validation should happens "
|
||||
. "only on class ".User::class);
|
||||
}
|
||||
|
||||
if ($value->getId() !== null) {
|
||||
$countUsersByUsername = $this->em->createQuery(
|
||||
sprintf(
|
||||
"SELECT COUNT(u) FROM %s u "
|
||||
. "WHERE u.usernameCanonical = LOWER(UNACCENT(:username)) "
|
||||
. "AND u != :user",
|
||||
User::class)
|
||||
)
|
||||
->setParameter('username', $value->getUsername())
|
||||
->setParameter('user', $value)
|
||||
->getSingleScalarResult();
|
||||
} else {
|
||||
$countUsersByUsername = $this->em->createQuery(
|
||||
sprintf(
|
||||
"SELECT COUNT(u) FROM %s u "
|
||||
. "WHERE u.usernameCanonical = LOWER(UNACCENT(:username)) ",
|
||||
User::class)
|
||||
)
|
||||
->setParameter('username', $value->getUsername())
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
if ($countUsersByUsername > 0) {
|
||||
$this->context
|
||||
->buildViolation($constraint->messageDuplicateUsername)
|
||||
->setParameters([
|
||||
'%username%' => $value->getUsername()
|
||||
])
|
||||
->atPath('username')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
|
||||
if ($value->getId() !== null) {
|
||||
$countUsersByEmail = $this->em->createQuery(
|
||||
sprintf(
|
||||
"SELECT COUNT(u) FROM %s u "
|
||||
. "WHERE u.emailCanonical = LOWER(UNACCENT(:email)) "
|
||||
. "AND u != :user",
|
||||
User::class)
|
||||
)
|
||||
->setParameter('email', $value->getEmail())
|
||||
->setParameter('user', $value)
|
||||
->getSingleScalarResult();
|
||||
} else {
|
||||
$countUsersByEmail = $this->em->createQuery(
|
||||
sprintf(
|
||||
"SELECT COUNT(u) FROM %s u "
|
||||
. "WHERE u.emailCanonical = LOWER(UNACCENT(:email))",
|
||||
User::class)
|
||||
)
|
||||
->setParameter('email', $value->getEmail())
|
||||
->getSingleScalarResult();
|
||||
}
|
||||
|
||||
if ($countUsersByEmail > 0) {
|
||||
$this->context
|
||||
->buildViolation($constraint->messageDuplicateEmail)
|
||||
->setParameters([
|
||||
'%email%' => $value->getEmail()
|
||||
])
|
||||
->atPath('email')
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
52
Validator/Constraints/Entity/UserCircleConsistency.php
Normal file
52
Validator/Constraints/Entity/UserCircleConsistency.php
Normal file
@ -0,0 +1,52 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Validator\Constraints\Entity;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class UserCircleConsistency extends Constraint
|
||||
{
|
||||
public $message = "{{ username }} is not allowed to see entities published in this circle";
|
||||
|
||||
public $role;
|
||||
|
||||
public $getUserFunction = 'getUser';
|
||||
|
||||
public $path = 'circle';
|
||||
|
||||
public function getDefaultOption()
|
||||
{
|
||||
return 'role';
|
||||
}
|
||||
|
||||
public function getRequiredOptions()
|
||||
{
|
||||
return [ 'role' ];
|
||||
}
|
||||
|
||||
public function getTargets()
|
||||
{
|
||||
return self::CLASS_CONSTRAINT;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2018 Champs Libres Cooperative <info@champs-libres.coop>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
namespace Chill\MainBundle\Validator\Constraints\Entity;
|
||||
|
||||
use Chill\MainBundle\Security\Authorization\AuthorizationHelper;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Chill\MainBundle\Entity\HasScopeInterface;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
*/
|
||||
class UserCircleConsistencyValidator extends ConstraintValidator
|
||||
{
|
||||
/**
|
||||
*
|
||||
* @var AuthorizationHelper
|
||||
*/
|
||||
protected $autorizationHelper;
|
||||
|
||||
function __construct(AuthorizationHelper $autorizationHelper)
|
||||
{
|
||||
$this->autorizationHelper = $autorizationHelper;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param object $value
|
||||
* @param UserCircleConsistency $constraint
|
||||
*/
|
||||
public function validate($value, Constraint $constraint)
|
||||
{
|
||||
/* @var $user \Chill\MainBundle\Entity\User */
|
||||
$user = \call_user_func([$value, $constraint->getUserFunction ]);
|
||||
|
||||
if ($user === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FALSE === $this->autorizationHelper->userHasAccess($user, $value, $constraint->role)) {
|
||||
$this->context
|
||||
->buildViolation($constraint->message)
|
||||
->setParameter('{{ username }}', $user->getUsername())
|
||||
->atPath($constraint->path)
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
35
chill.webpack.config.js
Normal file
35
chill.webpack.config.js
Normal file
@ -0,0 +1,35 @@
|
||||
// this file loads all assets from the Chill main bundle
|
||||
|
||||
// import jQuery
|
||||
const $ = require('jquery');
|
||||
// create global $ and jQuery variables
|
||||
global.$ = global.jQuery = $;
|
||||
|
||||
const moment = require('moment');
|
||||
global.moment = moment;
|
||||
|
||||
const pikaday = require('pikaday-jquery');
|
||||
|
||||
const select2 = require('select2');
|
||||
global.select2 = select2;
|
||||
|
||||
// import js
|
||||
import {chill} from './Resources/public/js/chill.js';
|
||||
global.chill = chill;
|
||||
|
||||
// css
|
||||
require('./Resources/public/sass/scratch.scss');
|
||||
require('./Resources/public/css/chillmain.css');
|
||||
require('./Resources/public/css/pikaday.css');
|
||||
require('./Resources/public/js/collection/collections.js');
|
||||
require('./Resources/public/modules/breadcrumb/index.js');
|
||||
require('./Resources/public/modules/download-report/index.js');
|
||||
//require('./Resources/public/css/scratch.css');
|
||||
//require('./Resources/public/css/select2/select2.css');
|
||||
require('select2/dist/css/select2.css');
|
||||
require('./Resources/public/modules/postal-code/index.js');
|
||||
|
||||
// img
|
||||
require('./Resources/public/img/favicon.ico');
|
||||
require('./Resources/public/img/logo-chill-sans-slogan_white.png');
|
||||
require('./Resources/public/img/logo-chill-outil-accompagnement_white.png');
|
@ -25,28 +25,26 @@
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "~5.5|~7.0",
|
||||
"twig/extensions": "~1.0",
|
||||
"symfony/assetic-bundle": "~2.3",
|
||||
"symfony/monolog-bundle": "~2.4",
|
||||
"symfony/framework-bundle": "~2.8",
|
||||
"symfony/yaml": "~2.7",
|
||||
"symfony/symfony": "~2.7",
|
||||
"doctrine/dbal": "~2.5",
|
||||
"doctrine/orm": "~2.4",
|
||||
"doctrine/common": "~2.4",
|
||||
"doctrine/doctrine-bundle": "~1.2",
|
||||
"php": "~7.2",
|
||||
"twig/extensions": "~1.5",
|
||||
"symfony/assetic-bundle": "~2.8",
|
||||
"symfony/monolog-bundle": "~3.2",
|
||||
"symfony/symfony": "~3.4",
|
||||
"doctrine/dbal": "~2.7",
|
||||
"doctrine/orm": "~2.6",
|
||||
"doctrine/common": "~2.8",
|
||||
"doctrine/doctrine-bundle": "~1.9",
|
||||
"champs-libres/composer-bundle-migration": "~1.0",
|
||||
"doctrine/doctrine-migrations-bundle": "~1.1",
|
||||
"doctrine/doctrine-migrations-bundle": "~1.3",
|
||||
"doctrine/migrations": "~1.0",
|
||||
"phpoffice/phpspreadsheet": "~1.2",
|
||||
"sensio/distribution-bundle": "^5.0"
|
||||
"sensio/distribution-bundle": "^5.0",
|
||||
"knplabs/knp-menu-bundle": "^2.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"symfony/dom-crawler": "2.5",
|
||||
"doctrine/doctrine-fixtures-bundle": "~2.2",
|
||||
"symfony/security": "~2.5",
|
||||
"symfony/phpunit-bridge": "^2.7",
|
||||
"symfony/dom-crawler": "~3.4",
|
||||
"doctrine/doctrine-fixtures-bundle": "~3.0",
|
||||
"symfony/phpunit-bridge": "~3.4",
|
||||
"phpunit/phpunit": "~5.6"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<phpunit bootstrap="./Resources/test/Fixtures/App/app/autoload.php" colors="true">
|
||||
<phpunit bootstrap="./vendor/autoload.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="ChillMain test suite">
|
||||
<directory suffix="Test.php">./Tests</directory>
|
||||
@ -19,5 +19,6 @@
|
||||
<php>
|
||||
<server name="KERNEL_DIR" value="./Resources/test/Fixtures/App/app" />
|
||||
<env name="SYMFONY_DEPRECATIONS_HELPER" value="weak" />
|
||||
<env name="APP_ENV" value="test" />
|
||||
</php>
|
||||
</phpunit>
|
||||
|
Loading…
x
Reference in New Issue
Block a user