Merge branch 'upgrade-sf3' into edit-user-password

This commit is contained in:
Julien Fastré 2018-08-16 11:31:07 +02:00
commit af803cc87d
99 changed files with 2929 additions and 497 deletions

View File

@ -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:

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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()
;

View 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 ] ]);
}
}

View 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
]);
}
}

View File

@ -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(

View File

@ -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"])
);
}
}
}
}

View 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]);
}
}
}
}

View 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\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)]);
}
}
}

View 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);
}
}

View 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);
}
}

View File

@ -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())
;
}
}

View File

@ -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;
}

View 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();
}

View File

@ -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();

View 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;
}
}

View File

@ -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);
}
}

View 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;
}
}

View 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);
}
}
}

View 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());
}
}

View File

@ -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,

View File

@ -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
));

View File

@ -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');
}
}

View File

@ -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;
}
}

View File

@ -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']);
})
;
}

View File

@ -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
));

View 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
{
}

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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}

View File

@ -0,0 +1,4 @@
chill_main_postal_code_search:
path: /search
defaults: { _controller: ChillMainBundle:PostalCode:search }

View File

@ -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:

View File

@ -0,0 +1,6 @@
services:
Chill\MainBundle\Controller\:
autowire: true
resource: '../../../Controller'
tags: ['controller.service_arguments']

View File

@ -0,0 +1,4 @@
services:
Chill\MainBundle\DataFixtures\ORM\:
resource: ../../../DataFixtures/ORM
tags: [ 'doctrine.fixture.orm' ]

View File

@ -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:

View 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' }

View File

@ -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

View File

@ -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'

View 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 }

View 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 }

View 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" }

View File

@ -0,0 +1,2 @@
services:
Chill\MainBundle\Templating\UI\CountNotificationUser: ~

View File

@ -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: ~

View 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');
}
}

View 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()');
}
}

View File

@ -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;

View File

@ -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 };

View 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 {
}
}

View 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]);
}
}
});

View File

@ -0,0 +1,3 @@
require("./layout.scss");

View 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);
}
}
}

View 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;

View 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");

View File

@ -0,0 +1,3 @@
require('./login.scss');

View 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;
}

View 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
}
});
}
});

View File

@ -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;
}

View File

@ -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);
// }

View File

@ -0,0 +1,2 @@
@import './../../fonts/OpenSans/OpenSans';

View File

@ -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;
}
}
}

View File

@ -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;
}

View File

@ -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%;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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()
);
}

View File

@ -41,7 +41,6 @@ security:
form_login:
csrf_parameter: _csrf_token
csrf_token_id: authenticate
csrf_provider: form.csrf_provider
logout: ~

View File

@ -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...

View File

@ -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."

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1 @@
{% if nb > 0 %}<span class="notification-counter">{{ nb }}{% endif %}

View File

@ -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' } } ) }}

View File

@ -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) }}

View File

@ -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 }}');

View 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);
}

View 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' ];
}
}

View 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' ];
}
}

View File

@ -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);
}
}

View File

@ -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()

View File

@ -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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}

View File

@ -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);
}
}

View File

@ -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 ];
}
}

View 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()
;
}
}
}

View 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;
}
}

View File

@ -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
View 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');

View File

@ -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": {

View File

@ -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>