add a custom field long choice basic

The `custom field long choice` aim to provide a way to deal with choices
with a big possibilities.

The `custom field long choice` allow :
- to persist different options in the database ;
- each option has a key, a text (translatable string), and eventually a
  parent, and an internal_key
- every key can be activate or not. If the parent is inactivated, all
  childs are inactivated
- the internal key have two purposes :
    - link to an external csv file, with their own key ;
    - add a special class to results, to allow custom layout.

Currently, the field exists, but some elements are missing :

- a script for CSV import
- possibility to select multiple items
- edition of options
- handle of option without parents
- tests are missing
This commit is contained in:
Julien Fastré 2015-12-11 10:50:39 +01:00
parent 3230659a4b
commit 4f13ec6959
10 changed files with 672 additions and 3 deletions

View File

@ -0,0 +1,156 @@
<?php
/*
* Copyright (C) 2015 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\CustomFieldsBundle\CustomFields;
use Chill\CustomFieldsBundle\CustomFields\CustomFieldInterface;
use Symfony\Component\Form\FormBuilderInterface;
use Chill\CustomFieldsBundle\Entity\CustomField;
use Chill\CustomFieldsBundle\EntityRepository\CustomFieldLongChoice\OptionRepository;
use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option;
use Chill\CustomFieldsBundle\Form\DataTransformer\CustomFieldDataTransformer;
use Symfony\Bridge\Twig\TwigEngine;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class CustomFieldLongChoice implements CustomFieldInterface
{
/**
*
* @var OptionRepository
*/
private $optionRepository;
/**
*
* @var TranslatableStringHelper
*/
private $translatableStringHelper;
/**
* @var TwigEngine
*/
private $templating;
const KEY = 'key';
public function __construct(OptionRepository $optionRepository,
TranslatableStringHelper $translatableStringHelper,
TwigEngine $twigEngine)
{
$this->optionRepository = $optionRepository;
$this->translatableStringHelper = $translatableStringHelper;
$this->templating = $twigEngine;
}
public function buildForm(FormBuilderInterface $builder, CustomField $customField)
{
$options = $customField->getOptions();
$entries = $this->optionRepository->findFilteredByKey($options[self::KEY],
false, true);
//create a local copy of translatable string helper
$translatableStringHelper = $this->translatableStringHelper;
$builder->add($customField->getSlug(), 'select2_choice', array(
'choices' => $entries,
'choice_label' => function(Option $option) use ($translatableStringHelper) {
return $translatableStringHelper->localize($option->getText());
},
'choice_value' => function ($key) use ($entries) {
if ($key === NULL) {
return null;
}
return $key->getId();
},
'choices_as_values' => true,
'multiple' => false,
'expanded' => false,
'group_by' => function(Option $option) use ($translatableStringHelper) {
if ($option->hasParent()) {
return $translatableStringHelper->localize($option->getParent()->getText());
} else {
return $translatableStringHelper->localize($option->getText());
}
},
'label' => $translatableStringHelper->localize($customField->getName())
));
$builder->get($customField->getSlug())
->addModelTransformer(new CustomFieldDataTransformer($this, $customField));
}
public function buildOptionsForm(FormBuilderInterface $builder)
{
//create a selector between different keys
$keys = $this->optionRepository->getKeys();
$choices = array();
foreach ($keys as $key) {
$choices[$key] = $key;
}
return $builder->add(self::KEY, 'choice', array(
'choices' => $choices,
'label' => 'Options key'
));
}
public function deserialize($serialized, \Chill\CustomFieldsBundle\Entity\CustomField $customField)
{
if ($serialized === NULL) {
return NULL;
}
return $this->optionRepository->find($serialized);
}
public function getName()
{
return 'Long Choice';
}
public function render($value, \Chill\CustomFieldsBundle\Entity\CustomField $customField, $documentType = 'html')
{
$option = $this->deserialize($value, $customField);
$template = 'ChillCustomFieldsBundle:CustomFieldsRendering:choice_long.'
.$documentType.'.twig';
return $this->templating
->render($template, array(
'values' => array($option)
));
}
public function serialize($value, \Chill\CustomFieldsBundle\Entity\CustomField $customField)
{
if (!$value instanceof Option) {
throw new \LogicException('the value should be an instance of '
. 'Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option, '
. is_object($value) ? get_class($value) : gettype($value).' given');
}
// we place the id in array, to allow in the future multiple select
return $value->getId();
}
}

View File

@ -0,0 +1,172 @@
<?php
/*
* Copyright (C) 2015 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\CustomFieldsBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option;
/**
* Load some Options
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class LoadOption extends AbstractFixture implements OrderedFixtureInterface
{
/**
*
* @var \Faker\Generator
*/
public $fakerFr;
/**
*
* @var \Faker\Generator
*/
public $fakerEn;
/**
*
* @var \Faker\Generator
*/
public $fakerNl;
public function __construct()
{
$this->fakerFr = \Faker\Factory::create('fr_FR');
$this->fakerEn = \Faker\Factory::create('en_EN');
$this->fakerNl = \Faker\Factory::create('nl_NL');
}
public function getOrder()
{
return 1000;
}
public function load(\Doctrine\Common\Persistence\ObjectManager $manager)
{
echo "Loading Options \n";
// load companies
$this->loadingCompanies($manager);
$this->loadingWords($manager);
$manager->flush();
}
private function loadingWords(\Doctrine\Common\Persistence\ObjectManager $manager)
{
echo "Loading some words...\n";
$parents = array(
array(
'fr' => 'Categorie 1',
'nl' => 'Categorie 1',
'en' => 'Category 1'
),
array(
'fr' => 'Categorie 2',
'nl' => 'Categorie 2',
'en' => 'Category 2'
)
);
foreach ($parents as $text) {
$parent = (new Option())
->setText($text)
->setKey('word')
;
$manager->persist($parent);
//Load children
$expected_nb_children = rand(10, 50);
for ($i=0; $i < $expected_nb_children; $i++) {
$manager->persist($this->createChildOption($parent, array(
'fr' => $this->fakerFr->word,
'nl' => $this->fakerNl->word,
'en' => $this->fakerEn->word
)));
}
}
}
private function loadingCompanies(\Doctrine\Common\Persistence\ObjectManager $manager)
{
echo "Loading companies \n";
$companiesParents = array(
array(
'fr' => 'Grandes Entreprises',
'nl' => 'Grotes Bedrijven',
'en' => 'Big Companies'
),
array(
'fr' => 'Moyennes Entreprises',
'nl' => 'Middelbare Bedrijven',
'en' => 'Middle Companies'
),
array(
'fr' => 'Petites Entreprises',
'nl' => 'Kleine Bedrijven',
'en' => 'Little Companies'
)
);
foreach ($companiesParents as $text) {
$parent = (new Option())
->setText($text)
->setKey('company')
;
$manager->persist($parent);
//Load children
$expected_nb_children = rand(10, 50);
for ($i=0; $i < $expected_nb_children; $i++) {
$companyName = $this->fakerFr->company;
$manager->persist($this->createChildOption($parent, array(
'fr' => $companyName,
'nl' => $companyName,
'en' => $companyName
)));
}
}
}
private $counter = 0;
/**
*
* @param Option $parent
* @param array $text
* @return Option
*/
private function createChildOption(Option $parent, array $text)
{
$this->counter ++;
return (new Option())
->setText($text)
->setParent($parent)
->setActive(true)
->setInternalKey($parent->getKey().'-'.$this->counter);
;
}
}

View File

@ -0,0 +1,153 @@
<?php
/*
* Copyright (C) 2015 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\CustomFieldsBundle\Entity\CustomFieldLongChoice;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class Option
{
/**
*
* @var int
*/
private $id;
/**
*
* @var string
*/
private $key;
/**
* a json representation of text (multilingual)
*
* @var array
*/
private $text;
/**
*
* @var \Doctrine\Common\Collections\Collection
*/
private $children;
/**
*
* @var Option
*/
private $parent;
/**
*
* @var string
*/
private $internalKey = '';
/**
*
* @var boolean
*/
private $active = true;
public function getId()
{
return $this->id;
}
public function getKey()
{
return $this->key;
}
public function getText()
{
return $this->text;
}
public function getChildren()
{
return $this->children;
}
public function getParent()
{
return $this->parent;
}
public function setKey($key)
{
$this->key = $key;
return $this;
}
public function setText(array $text)
{
$this->text = $text;
return $this;
}
public function setParent(Option $parent = null)
{
$this->parent = $parent;
$this->key = $parent->getKey();
return $this;
}
/**
*
* @return boolean
*/
public function hasParent()
{
return $this->parent === NULL ? false : true;
}
public function getInternalKey()
{
return $this->internalKey;
}
public function isActive()
{
return $this->active;
}
public function getActive()
{
return $this->isActive();
}
public function setInternalKey($internal_key)
{
$this->internalKey = $internal_key;
return $this;
}
public function setActive($active)
{
$this->active = $active;
return $this;
}
}

View File

@ -0,0 +1,77 @@
<?php
/*
* Copyright (C) 2015 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\CustomFieldsBundle\EntityRepository\CustomFieldLongChoice;
use Doctrine\ORM\EntityRepository;
use Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option;
/**
*
*
* @author Julien Fastré <julien.fastre@champs-libres.coop>
*/
class OptionRepository extends EntityRepository
{
/**
*
* @param string $key
* @return Option[]
*/
public function findFilteredByKey($key, $includeParents = true, $active = true)
{
$qb = $this->createQueryBuilder('option');
$qb->where('option.key = :key');
if ($active === true){
$qb->andWhere('option.active = true');
}
if ($includeParents === false) {
$qb->andWhere('option.parent IS NOT NULL');
if ($active === TRUE) {
$qb->join('option.parent', 'p');
$qb->andWhere('p.active = true');
}
}
$qb->setParameter('key', $key);
return $qb->getQuery()->getResult();
}
/**
*
* @return string[]
*/
public function getKeys()
{
$keys = $this->createQueryBuilder('option')
->select('option.key')
->distinct()
->getQuery()
->getScalarResult();
return array_map(function($r) {
return $r['key'];
}, $keys);
}
}

View File

@ -0,0 +1,33 @@
Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option:
type: entity
table: custom_field_long_choice_options
repositoryClass: Chill\CustomFieldsBundle\EntityRepository\CustomFieldLongChoice\OptionRepository
id:
id:
type: integer
id: true
generator:
strategy: AUTO
fields:
key:
type: string
length: 15
text:
type: json_array
internalKey:
type: string
length: 50
column: internal_key
active:
type: boolean
default: true
oneToMany:
children:
targetEntity: Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option
mappedBy: parent
manyToOne:
parent:
targetEntity: Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option
inversedBy: children
nullable: true

View File

@ -107,4 +107,20 @@ services:
calls:
- [setContainer, ["@service_container"]]
tags:
- { name: twig.extension }
- { name: twig.extension }
chill.custom_field.custom_field_long_choice:
class: Chill\CustomFieldsBundle\CustomFields\CustomFieldLongChoice
arguments:
- "@chill.custom_field.custom_field_long_choice_option_repository"
- "@chill.main.helper.translatable_string"
- "@templating"
tags:
- { name: 'chill.custom_field', type: 'long_choice' }
chill.custom_field.custom_field_long_choice_option_repository:
class: Chill\CustomFieldsBundle\EntityRepository\CustomFieldLongChoice\OptionRepository
factory: ["@doctrine", getRepository]
arguments:
- "Chill\CustomFieldsBundle\Entity\CustomFieldLongChoice\Option"

View File

@ -0,0 +1,47 @@
<?php
namespace Application\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Auto-generated Migration: Please modify to your needs!
*/
class Version20151210205610 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(Schema $schema)
{
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('CREATE SEQUENCE custom_field_long_choice_options_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE custom_field_long_choice_options (id INT NOT NULL, '
. 'parent_id INT DEFAULT NULL, '
. 'key VARCHAR(15) NOT NULL, '
. 'text jsonb NOT NULL, '
. 'active boolean NOT NULL,'
. 'internal_key VARCHAR(50) NOT NULL DEFAULT \'\', '
. 'PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_14BBB8E0727ACA70 ON custom_field_long_choice_options (parent_id)');
$this->addSql('ALTER TABLE custom_field_long_choice_options ADD CONSTRAINT cf_long_choice_self_referencing '
. 'FOREIGN KEY (parent_id) REFERENCES custom_field_long_choice_options (id) '
. 'NOT DEFERRABLE INITIALLY IMMEDIATE');
}
/**
* @param Schema $schema
*/
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() != 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('ALTER TABLE custom_field_long_choice_options DROP CONSTRAINT cf_long_choice_self_referencing');
$this->addSql('DROP SEQUENCE custom_field_long_choice_options_id_seq CASCADE');
$this->addSql('DROP TABLE custom_field_long_choice_options');
}
}

View File

@ -84,3 +84,6 @@ Greater or equal than: Plus grand ou égal à
Lesser or equal than: Plus petit ou égal à
Precision: Précision
Text after the field: Texte après le champ
#custom field long choice
Options key: Clé des options

View File

@ -0,0 +1,11 @@
{% if values|length > 0 %}
<ul class="custom_fields long_choice {% if values|length == 1 %}unique{% endif %}">
{%- for value in values -%}
<li class="long_choice_item long_choice-key-{{ value.key }} long_choice-ikey-{{ value.internalKey }} long_choice-parent-ikey-{{ value.parent.internalKey }}">
<i class="fa fa-check-square-o"></i>&nbsp;{{ value.text|localize_translatable_string }}
</li>
{%- endfor -%}
</ul>
{% else %}
<div class="custom_fields_choice long_choice empty">{{ 'None'|trans }}</div>
{% endif %}

View File

@ -34,8 +34,9 @@
"chill-project/main": "dev-master"
},
"require-dev": {
"doctrine/doctrine-fixtures-bundle": "~2.2@dev",
"chill-project/person": "dev-master@dev"
"chill-project/person": "dev-master@dev",
"fzaninotto/faker": "~1",
"doctrine/doctrine-fixtures-bundle": "~2.2"
},
"scripts": {
"post-install-cmd": [