diff --git a/src/Bundle/ChillPerson/.gitignore b/src/Bundle/ChillPerson/.gitignore new file mode 100644 index 000000000..f443cb81e --- /dev/null +++ b/src/Bundle/ChillPerson/.gitignore @@ -0,0 +1,11 @@ +composer.lock +vendor/* +parameters.yml +*~ +*.DS_Store +*.sass-cache +Resources/node_modules/ +Tests/Fixtures/App/app/config/parameters.yml +/nbproject/private/ +Resources/test/Fixtures/App/bootstrap.php.cache + diff --git a/src/Bundle/ChillPerson/.gitlab-ci.yml b/src/Bundle/ChillPerson/.gitlab-ci.yml new file mode 100644 index 000000000..be0b6078a --- /dev/null +++ b/src/Bundle/ChillPerson/.gitlab-ci.yml @@ -0,0 +1,64 @@ +.test_definition: &test_definition + services: + - chill/database:latest + before_script: + - if [ -z ${GITHUB_TOKEN+x} ]; then composer config github-oauth.github.com $GITHUB_TOKEN; fi + - php -d memory_limit=-1 /usr/local/bin/composer install --no-interaction + - cp Resources/test/Fixtures/App/app/config/parameters.gitlab-ci.yml Resources/test/Fixtures/App/app/config/parameters.yml + - php Resources/test/Fixtures/App/app/console --env=test cache:warmup + - php Resources/test/Fixtures/App/app/console doctrine:migrations:migrate --env=test --no-interaction + - php Resources/test/Fixtures/App/app/console doctrine:fixtures:load --env=test --no-interaction + +stages: + - deploy + - test + - build-doc + - deploy-doc + +test:php-7.2: + image: chill/ci-image:php-7.2 + stage: test + <<: *test_definition + script: APP_ENV=test php vendor/bin/phpunit + +deploy-packagist: + stage: deploy + image: chill/ci-image:php-7.2 + before_script: + # test that PACKAGIST USERNAME and PACKAGIST_TOKEN variable are set + - if [ -z ${PACKAGIST_USERNAME+x} ]; then echo "Please set PACKAGIST_USERNAME variable"; exit -1; fi + - if [ -z ${PACKAGIST_TOKEN+x} ]; then echo "Please set PACKAGIST_TOKEN variable"; exit -1; fi + script: + - STATUSCODE=$(curl -XPOST -H'content-type:application/json' "https://packagist.org/api/update-package?username=$PACKAGIST_USERNAME&apiToken=$PACKAGIST_TOKEN" -d"{\"repository\":{\"url\":\"$CI_PROJECT_URL.git\"}}" --silent --output /dev/stderr --write-out "%{http_code}") + - if [ $STATUSCODE = "202" ]; then exit 0; else exit $STATUSCODE; fi + +# deploy documentation +api-doc-build: + stage: build-doc + environment: api-doc + image: chill/ci-image:php-7.2 + before_script: + - mkdir api-doc + script: apigen generate --destination api-doc/$CI_BUILD_REF_NAME/$CI_PROJECT_NAME + artifacts: + paths: + - "api-doc/" + name: api + expire_in: '2h' + only: + - master + - tags + +api-doc-deploy: + stage: deploy-doc + image: pallet/swiftclient:latest + before_script: + # test that CONTAINER_API variable is set + - if [ -z ${CONTAINER_API+x} ]; then echo "Please set CONTAINER_API variable"; exit -1; fi + # go to api-doc to have and url with PROJECT/BUILD + - cd api-doc + # upload, and keep files during 1 year + script: "swift upload --header \"X-Delete-After: 31536000\" $CONTAINER_API $CI_BUILD_REF_NAME/$CI_PROJECT_NAME" + only: + - master + - tags diff --git a/src/Bundle/ChillPerson/Actions/ActionEvent.php b/src/Bundle/ChillPerson/Actions/ActionEvent.php new file mode 100644 index 000000000..5ac3055cb --- /dev/null +++ b/src/Bundle/ChillPerson/Actions/ActionEvent.php @@ -0,0 +1,143 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Actions; + +use Symfony\Component\EventDispatcher\Event; + +/** + * Event triggered when an entity attached to a person is removed. + * + * + */ +class ActionEvent extends Event +{ + const DELETE = 'CHILL_PERSON.DELETE_ASSOCIATED_ENTITY'; + const MOVE = 'CHILL_PERSON.MOVE_ASSOCIATED_ENTITY'; + + /** + * + * @var int + */ + protected $personId; + + /** + * the FQDN class name as recorded in doctrine + * + * @var string + */ + protected $entity; + + /** + * an array of key value data to describe the movement + * + * @var array + */ + protected $metadata; + + /** + * the sql statement + * + * @var string + */ + protected $sqlStatement; + + /** + * + * @var string[] + */ + protected $preSql = []; + + /** + * + * @var string[] + */ + protected $postSql = []; + + public function __construct($personId, $entity, $sqlStatement, $metadata = []) + { + $this->personId = $personId; + $this->entity = $entity; + $this->sqlStatement = $sqlStatement; + $this->metadata = $metadata; + } + + /** + * + * @return string[] + */ + public function getPreSql(): array + { + return $this->preSql; + } + + /** + * + * @return string[] + */ + public function getPostSql(): array + { + return $this->postSql; + } + + /* + * Add Sql which will be executed **before** the delete statement + */ + public function addPreSql(string $preSql) + { + $this->preSql[] = $preSql; + return $this; + } + + /** + * Add Sql which will be executed **after** the delete statement + * + * @param type $postSql + * @return $this + */ + public function addPostSql(string $postSql) + { + $this->postSql[] = $postSql; + return $this; + } + + public function getPersonId(): int + { + return $this->personId; + } + + /** + * get the entity name, as recorded in doctrine + * + * @return string + */ + public function getEntity(): string + { + return $this->entity; + } + + public function getSqlStatement() + { + return $this->sqlStatement; + } + + public function getMetadata() + { + return $this->metadata; + } + +} diff --git a/src/Bundle/ChillPerson/Actions/Remove/PersonMove.php b/src/Bundle/ChillPerson/Actions/Remove/PersonMove.php new file mode 100644 index 000000000..fb7e6b9b4 --- /dev/null +++ b/src/Bundle/ChillPerson/Actions/Remove/PersonMove.php @@ -0,0 +1,184 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Actions\Remove; + +use Doctrine\ORM\EntityManagerInterface; +use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Doctrine\ORM\Mapping\ClassMetadata; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Chill\PersonBundle\Actions\ActionEvent; + +/** + * Move or delete entities associated to a person to a new one, and delete the + * old person. The data associated to a person (birthdate, name, ...) are left + * untouched on the "new one". + * + * See `getSql` for details. + * + * + */ +class PersonMove +{ + /** + * + * @var EntityManagerInterface + */ + protected $em; + + /** + * + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + public function __construct( + EntityManagerInterface $em, + EventDispatcherInterface $eventDispatcher + ) { + $this->em = $em; + $this->eventDispatcher = $eventDispatcher; + } + + /** + * Return the sql used to move or delete entities associated to a person to + * a new one, and delete the old person. The data associated to a person + * (birthdate, name, ...) are left untouched on the "new one". + * + * The accompanying periods associated to a person are always removed. The other + * associated entity are updated: the new person id is associated to the entity. + * + * Optionnaly, you can ask for removing entity by passing them in $deleteEntities + * parameters. + * + * The following events are triggered: + * - `'CHILL_PERSON.DELETE_ASSOCIATED_ENTITY'` is triggered when an entity + * will be removed ; + * - `'CHILL_PERSON.MOVE_ASSOCIATED_ENTITY'` is triggered when an entity + * will be moved ; + * + * Those events have the following metadata: + * + * - 'original_action' : always 'move' ; + * - 'to': the person id to move ; + * + * @param Person $from + * @param Person $to + * @param array $deleteEntities + * @return type + */ + public function getSQL(Person $from, Person $to, array $deleteEntities = []) + { + $sqls = []; + $toDelete = \array_merge($deleteEntities, $this->getDeleteEntities()); + + foreach ($this->em->getMetadataFactory()->getAllMetadata() as $metadata) { + if ($metadata->isMappedSuperclass) { + continue; + } + + foreach ($metadata->getAssociationMappings() as $field => $mapping) { + if ($mapping['targetEntity'] === Person::class) { + + if (\in_array($metadata->getName(), $toDelete)) { + $sql = $this->createDeleteSQL($metadata, $from, $field); + $event = new ActionEvent($from->getId(), $metadata->getName(), $sql, + ['to' => $to->getId(), 'original_action' => 'move']); + $this->eventDispatcher->dispatch(ActionEvent::DELETE, $event); + + } else { + $sql = $this->createMoveSQL($metadata, $from, $to, $field); + $event = new ActionEvent($from->getId(), $metadata->getName(), $sql, + ['to' => $to->getId(), 'original_action' => 'move']); + $this->eventDispatcher->dispatch(ActionEvent::MOVE, $event); + } + + $sqls = \array_merge($sqls, $event->getPreSql(), [$event->getSqlStatement()], $event->getPostSql()); + } + } + } + + $personMetadata = $this->em->getClassMetadata(Person::class); + $sqls[] = sprintf("DELETE FROM %s WHERE id = %d", + $this->getTableName($personMetadata), + $from->getId()); + + return $sqls ?? []; + } + + protected function createMoveSQL(ClassMetadata $metadata, Person $from, Person $to, $field): string + { + $mapping = $metadata->getAssociationMapping($field); + + // Set part of the query, aka in "UPDATE table SET " + $sets = []; + foreach ($mapping["joinColumns"] as $columns) { + $sets[] = sprintf("%s = %d", $columns["name"], $to->getId()); + } + + $conditions = []; + foreach ($mapping["joinColumns"] as $columns) { + $conditions[] = sprintf("%s = %d", $columns["name"], $from->getId()); + } + + return \sprintf("UPDATE %s SET %s WHERE %s", + $this->getTableName($metadata), + \implode(" ", $sets), + \implode(" AND ", $conditions) + ); + } + + protected function createDeleteSQL(ClassMetadata $metadata, Person $from, $field): string + { + $mapping = $metadata->getAssociationMapping($field); + + $conditions = []; + foreach ($mapping["joinColumns"] as $columns) { + $conditions[] = sprintf("%s = %d", $columns["name"], $from->getId()); + } + + return \sprintf("DELETE FROM %s WHERE %s", + $this->getTableName($metadata), + \implode(" AND ", $conditions) + ); + } + + /** + * return an array of classes where entities should be deleted + * instead of moved + * + * @return array + */ + protected function getDeleteEntities(): array + { + return [ + AccompanyingPeriod::class + ]; + } + + /** + * get the full table name with schema if it does exists + */ + private function getTableName(ClassMetadata $metadata): string + { + return empty($metadata->getSchemaName()) ? + $metadata->getTableName() : + $metadata->getSchemaName().".".$metadata->getTableName(); + } + +} diff --git a/src/Bundle/ChillPerson/CHANGELOG.md b/src/Bundle/ChillPerson/CHANGELOG.md new file mode 100644 index 000000000..5ba71f288 --- /dev/null +++ b/src/Bundle/ChillPerson/CHANGELOG.md @@ -0,0 +1,117 @@ + +Version 1.5.1 +============= + +- Improve import of person to allow multiple centers by file ; +- Launch an event on person import ; +- Allow person to have a `null` gender ; +- Allow filters and aggregator to handle null gender ; +- remove inexistant `person.css` file +- fix bug in accompanying person validation + +Version 1.5.2 +============== + +- Add an column with fullname canonical (lowercase and unaccent) to persons entity ; +- Add a trigram index on fullname canonical ; +- Add a "similar person matcher", which allow to detect person with similar names when adding a person ; +- Add a research of persons by fuzzy name, returning result with a similarity of 0.15 ; + +Thanks to @matla :-) + +Version 1.5.3 +============= + +- add filtering on accompanying period +- fix problems in gender filter + +Version 1.5.4 +============= + +- add filenumber in person header + +Version 1.5.5 +============= + +- Fix bug in accompanying period filter + +Version 1.5.6 +============= + +- Update address validation +- Add command to move person and all data of a person to a new one, and delete the old one. + +Version 1.5.7 +============= + +- fix error on macro renderPerson / withLink not taken into account +- add a link between accompanying person and user +- add an icon when the file is opened / closed in result list, and in person rendering macro +- improve command to move person and all data: allow to delete some entities during move and add events + +Version 1.5.8 +============= + +- add search by phonenumber, with a custom SearchInterface + + This can be activated or desactivated by config: + + ``` + chill_person: + enabled: true + search: + enabled: true + + # enable search by phone. 'always' show the result on every result. 'on-domain' will show the result only if the domain is given in the search box. 'never' disable this feature + search_by_phone: on-domain # One of "always"; "on-domain"; "never" + ``` +- format phonenumber using twilio (if available) ; +- add `record_actions` in person search result list: users can click on a little eye to open person page ; +- add new fields (email, mobilenumber, gender) into importPeopleFromCSV command +- configure asset using a function + + +Version 1.5.9 +============= + +- create CRUD +- add the ability to add alt names to persons +- [UI] set action button bottom of edit form according to crud template +- [closing motive] add an hierarchy for closing motives ; +- [closing motive] Add an admin section for closing motives ; + +<<<<<<< HEAD +Version 1.5.10 +============== + +- [closing motive] display closing motive in remark + +Version 1.5.11 +============== + +- Fix versioning constraint to chill main + +Version 1.5.12 +============== + +- [addresses] add a homeless to person's addresses, and this information into + person list + +Version 1.5.13 +============== + +- [CRUD] add step delete +- [CRUD] improve index view in person CRUD +- [CRUD] filter by basis on person by default in EntityPersonCRUDController +- [CRUD] override relevant part of the main CRUD template +- [CRUD] fix redirection on person view: add a `person_id` to every page redirected. + +Version 1.5.14 +============== + +- [Accompanying period list] Fix period label in list +- [Accompanying period list] Fix label of closing motive +- [Person details] Add an "empty" statement on place of birth +- [Person list] Add a lock/unlock icon instead of open/closed folder in result list; +- [Admin closing motive] Remove links to Closing motive View; +- [Admin closing motive] Improve icons for active in list of closing motive; diff --git a/src/Bundle/ChillPerson/CRUD/Controller/EntityPersonCRUDController.php b/src/Bundle/ChillPerson/CRUD/Controller/EntityPersonCRUDController.php new file mode 100644 index 000000000..549792e51 --- /dev/null +++ b/src/Bundle/ChillPerson/CRUD/Controller/EntityPersonCRUDController.php @@ -0,0 +1,197 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\CRUD\Controller; + +use Chill\MainBundle\CRUD\Controller\CRUDController; +use Symfony\Component\HttpFoundation\Request; +use Chill\PersonBundle\Entity\Person; +use Doctrine\ORM\QueryBuilder; + +/** + * Class EntityPersonCRUDController + * CRUD Controller for entities attached to a Person + * + * @package Chill\PersonBundle\CRUD\Controller + */ +class EntityPersonCRUDController extends CRUDController +{ + /** + * Extract the person from the request + * + * the person parameter will be `person_id` and must be + * present in the query + * + * If the parameter is not set, this method will return null. + * + * If the person id does not exists, the method will throw a + * Symfony\Component\HttpKernel\Exception\NotFoundHttpException + * + * @param Request $request + * @throws Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the person with given id is not found + */ + protected function getPerson(Request $request): ?Person + { + if (FALSE === $request->query->has('person_id')) { + return null; + } + + $person = $this->getDoctrine() + ->getRepository(Person::class) + ->find($request->query->getInt('person_id')) + ; + + if (NULL === $person) { + throw $this->createNotFoundException('the person with this id is not found'); + } + + return $person; + } + + /** + * @param \Chill\MainBundle\CRUD\Controller\string|string $action + * @param Request $request + * @return object + */ + protected function createEntity($action, Request $request): object + { + $entity = parent::createEntity($action, $request); + + $person = $this->getPerson($request); + + $entity->setPerson($person); + + return $entity; + } + + /** + * @param string $action + * @param mixed $entity + * @param Request $request + * @param array $defaultTemplateParameters + * @return array + * @throws \Exception + */ + protected function generateTemplateParameter(string $action, $entity, Request $request, array $defaultTemplateParameters = array()): array + { + $person = $this->getPerson($request); + + if (NULL === $person) { + throw new \Exception("the `person_id` parameter is not set in the query. " + . "You should set it or override the current method to allow another " + . "behaviour: ".__METHOD__); + } + + return parent::generateTemplateParameter( + $action, + $entity, + $request, + \array_merge([ 'person' => $person ], $defaultTemplateParameters) + ); + } + + /** + * @param string $action + * @param mixed $entity + * @param Request $request + * @return string + */ + protected function getTemplateFor($action, $entity, Request $request) + { + if ($this->hasCustomTemplate($action, $entity, $request)) { + return $this->getActionConfig($action)['template']; + } + + switch ($action) { + case 'new': + return '@ChillPerson/CRUD/new.html.twig'; + case 'edit': + return '@ChillPerson/CRUD/edit.html.twig'; + case 'view': + return '@ChillPerson/CRUD/view.html.twig'; + case 'delete': + return '@ChillPerson/CRUD/delete.html.twig'; + case 'index': + return '@ChillPerson/CRUD/index.html.twig'; + default: + return parent::getTemplateFor($action, $entity, $request); + } + } + + /** + * @param string $action + * @param mixed $entity + * @param \Symfony\Component\Form\FormInterface $form + * @param Request $request + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + protected function onBeforeRedirectAfterSubmission(string $action, $entity, \Symfony\Component\Form\FormInterface $form, Request $request) + { + $next = $request->request->get("submit", "save-and-close"); + + switch ($next) { + case "save-and-close": + return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_index', [ + 'person_id' => $this->getPerson($request)->getId() + ]); + case "save-and-new": + return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_new', [ + 'person_id' => $this->getPerson($request)->getId() + ]); + case "new": + return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_view', [ + 'id' => $entity->getId(), + 'person_id' => $this->getPerson($request)->getId() + ]); + default: + return $this->redirectToRoute('chill_crud_'.$this->getCrudName().'_view', [ + 'id' => $entity->getId(), + 'person_id' => $this->getPerson($request)->getId() + ]); + } + } + + /** + * Override the base method to add a filtering step to a person. + * + * @param string $action + * @param Request $request + * @return QueryBuilder + */ + protected function buildQueryEntities(string $action, Request $request) + { + $qb = parent::buildQueryEntities($action, $request); + + return $this->filterQueryEntitiesByPerson($action, $qb, $request); + } + + /** + * Add a where clause to the buildQuery + * + * @param string $action + * @param \Chill\PersonBundle\CRUD\Controller\QueryBuilder $qb + * @param Request $request + * @return \Chill\PersonBundle\CRUD\Controller\QueryBuilder + */ + protected function filterQueryEntitiesByPerson(string $action, QueryBuilder $qb, Request $request): QueryBuilder + { + $qb->andWhere($qb->expr()->eq('e.person', ':person')); + $qb->setParameter('person', $this->getPerson($request)); + + return $qb; + } +} diff --git a/src/Bundle/ChillPerson/CRUD/Controller/OneToOneEntityPersonCRUDController.php b/src/Bundle/ChillPerson/CRUD/Controller/OneToOneEntityPersonCRUDController.php new file mode 100644 index 000000000..720de560c --- /dev/null +++ b/src/Bundle/ChillPerson/CRUD/Controller/OneToOneEntityPersonCRUDController.php @@ -0,0 +1,89 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\CRUD\Controller; + +use Chill\MainBundle\CRUD\Controller\CRUDController; +use Symfony\Component\HttpFoundation\Request; +use Chill\PersonBundle\Entity\Person; +use Symfony\Component\Form\FormInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\RedirectResponse; + +/** + * Controller for entities attached as one-to-on to a person + * + */ +class OneToOneEntityPersonCRUDController extends CRUDController +{ + protected function getTemplateFor($action, $entity, Request $request) + { + if (!empty($this->crudConfig[$action]['template'])) { + return $this->crudConfig[$action]['template']; + } + + switch ($action) { + case 'new': + return '@ChillPerson/CRUD/new.html.twig'; + case 'edit': + return '@ChillPerson/CRUD/edit.html.twig'; + case 'index': + return '@ChillPerson/CRUD/index.html.twig'; + default: + throw new \LogicException("the view for action $action is not " + . "defined. You should override ".__METHOD__." to add this " + . "action"); + } + } + + protected function getEntity($action, $id, Request $request): ?object + { + $entity = parent::getEntity($action, $id, $request); + + if (NULL === $entity) { + $entity = $this->createEntity($action, $request); + $person = $this->getDoctrine() + ->getManager() + ->getRepository(Person::class) + ->find($id); + + $entity->setPerson($person); + } + + return $entity; + } + + protected function onPreFlush(string $action, $entity, FormInterface $form, Request $request) + { + $this->getDoctrine()->getManager()->persist($entity); + } + + protected function onPostFetchEntity($action, Request $request, $entity): ?Response + { + if (FALSE === $this->getDoctrine()->getManager()->contains($entity)) { + return new RedirectResponse($this->generateRedirectOnCreateRoute($action, $request, $entity)); + } + + return null; + } + + protected function generateRedirectOnCreateRoute($action, Request $request, $entity) + { + throw new BadMethodCallException("not implemtented yet"); + } + +} diff --git a/src/Bundle/ChillPerson/ChillPersonBundle.php b/src/Bundle/ChillPerson/ChillPersonBundle.php new file mode 100644 index 000000000..18bf8fb79 --- /dev/null +++ b/src/Bundle/ChillPerson/ChillPersonBundle.php @@ -0,0 +1,21 @@ +getExtension('chill_main') + ->addWidgetFactory(new PersonListWidgetFactory()); + + $container->addCompilerPass(new AccompanyingPeriodTimelineCompilerPass()); + } +} diff --git a/src/Bundle/ChillPerson/Command/ChillPersonMoveCommand.php b/src/Bundle/ChillPerson/Command/ChillPersonMoveCommand.php new file mode 100644 index 000000000..4f2b2098a --- /dev/null +++ b/src/Bundle/ChillPerson/Command/ChillPersonMoveCommand.php @@ -0,0 +1,150 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Chill\PersonBundle\Actions\Remove\PersonMove; +use Doctrine\ORM\EntityManagerInterface; +use Chill\PersonBundle\Entity\Person; +use Symfony\Component\Console\Exception\RuntimeException; +use Psr\Log\LoggerInterface; + +class ChillPersonMoveCommand extends ContainerAwareCommand +{ + /** + * + * @var PersonMove + */ + protected $mover; + + /** + * + * @var EntityManagerInterface + */ + protected $em; + + /** + * + * @var LoggerInterface + */ + protected $chillLogger; + + public function __construct( + PersonMove $mover, + EntityManagerInterface $em, + LoggerInterface $chillLogger + ) { + parent::__construct('chill:person:move'); + + $this->mover = $mover; + $this->em = $em; + $this->chillLogger = $chillLogger; + } + + protected function configure() + { + $this + ->setName('chill:person:move') + ->setDescription('Move all the associated entities on a "from" person to a "to" person and remove the old person') + ->addOption('from', 'f', InputOption::VALUE_REQUIRED, "The person id to delete, all associated data will be moved before") + ->addOption('to', 't', InputOption::VALUE_REQUIRED, "The person id which will received data") + ->addOption('dump-sql', null, InputOption::VALUE_NONE, "dump sql to stdout") + ->addOption('force', null, InputOption::VALUE_NONE, "execute sql instead of dumping it") + ->addOption('delete-entity', null, InputOption::VALUE_REQUIRED|InputOption::VALUE_IS_ARRAY, "entity to delete", []) + ; + } + + protected function interact(InputInterface $input, OutputInterface $output) + { + if (FALSE === $input->hasOption('dump-sql') && FALSE === $input->hasOption('force')) { + $msg = "You must use \"--dump-sql\" or \"--force\""; + throw new RuntimeException($msg); + } + + foreach (["from", "to"] as $name) { + if (empty($input->getOption($name))) { + throw new RuntimeException("You must set a \"$name\" option"); + } + $id = $input->getOption($name); + if (\ctype_digit($id) === FALSE) { + throw new RuntimeException("The id in \"$name\" field does not contains " + . "only digits: $id"); + } + } + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $repository = $this->em->getRepository(Person::class); + $from = $repository->find($input->getOption('from')); + $to = $repository->find($input->getOption('to')); + $deleteEntities = $input->getOption('delete-entity'); + + if ($from === NULL) { + throw new RuntimeException(sprintf("Person \"from\" with id %d not found", $input->getOption('from'))); + } + if ($to === NULL) { + throw new RuntimeException(sprintf("Person \"to\" with id %d not found", $input->getOption('to'))); + } + + $sqls = $this->mover->getSQL($from, $to, $deleteEntities); + + if ($input->getOption('dump-sql')) { + foreach($sqls as $sql) { + $output->writeln($sql); + } + } else { + $ctxt = $this->buildLoggingContext($from, $to, $deleteEntities, $sqls); + $this->chillLogger->notice("Trying to move a person from command line", $ctxt); + $connection = $this->em->getConnection(); + $connection->beginTransaction(); + foreach($sqls as $sql) { + if ($output->isVerbose()) { + $output->writeln($sql); + } + $connection->executeQuery($sql); + } + $connection->commit(); + + $this->chillLogger->notice("Move a person from command line succeeded", $ctxt); + } + } + + protected function buildLoggingContext(Person $from, Person $to, $deleteEntities, $sqls) + { + $ctxt = [ + 'from' => $from->getId(), + 'to' => $to->getId() + ]; + + foreach ($deleteEntities as $key => $de) { + $ctxt['delete_entity_'.$key] = $de; + } + foreach ($sqls as $key => $sql) { + $ctxt['sql_'.$key] = $sql; + } + + return $ctxt; + } + +} diff --git a/src/Bundle/ChillPerson/Command/ImportPeopleFromCSVCommand.php b/src/Bundle/ChillPerson/Command/ImportPeopleFromCSVCommand.php new file mode 100644 index 000000000..d5e4c597c --- /dev/null +++ b/src/Bundle/ChillPerson/Command/ImportPeopleFromCSVCommand.php @@ -0,0 +1,1130 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Command; + +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Doctrine\ORM\EntityManagerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Logger\ConsoleLogger; +use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Console\Question\ChoiceQuestion; +use Symfony\Component\Console\Helper\Table; +use Chill\PersonBundle\Entity\Person; +use Chill\MainBundle\Entity\Address; +use Chill\MainBundle\Entity\PostalCode; +use Chill\MainBundle\Entity\Center; +use Chill\CustomFieldsBundle\Service\CustomFieldProvider; +use Symfony\Component\Console\Question\ConfirmationQuestion; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\EventDispatcher\Event; +use Symfony\Component\Form\FormFactory; + +/** + * Class ImportPeopleFromCSVCommand + * + * @package Chill\PersonBundle\Command + * @author Julien Fastré + */ +class ImportPeopleFromCSVCommand extends Command +{ + /** + * @var InputInterface + */ + protected $input; + + /** + * @var OutputInterface + */ + protected $output; + + /** + * @var \Psr\Log\LoggerInterface + */ + protected $logger; + + /** + * @var \Chill\MainBundle\Templating\TranslatableStringHelper + */ + protected $helper; + + /** + * @var \Doctrine\Persistence\ObjectManager + */ + protected $em; + + /** + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * the line currently read + * + * @var int + */ + protected $line; + + /** + * @var array where key are column names, and value the custom field slug + */ + protected $customFieldMapping = array(); + + /** + * @var CustomFieldProvider + */ + protected $customFieldProvider; + + /** + * Contains an array of information searched in the file. + * + * position 0: the information key (which will be used in this process) + * position 1: the helper + * position 2: the default value + * + * @var array + */ + protected static $mapping = array( + ['firstname', 'The column header for firstname', 'firstname'], + ['lastname', 'The column header for lastname', 'lastname'], + ['birthdate', 'The column header for birthdate', 'birthdate'], + ['gender', 'The column header for gender', 'gender'], + ['opening_date', 'The column header for opening date', 'opening_date'], + ['closing_date', 'The column header for closing date', 'closing_date'], + ['memo', 'The column header for memo', 'memo'], + ['email', 'The column header for email', 'email'], + ['phonenumber', 'The column header for phonenumber', 'phonenumber'], + ['mobilenumber', 'The column header for mobilenumber', 'mobilenumber'], + ['street1', 'The column header for street 1', 'street1'], + ['postalcode', 'The column header for postal code', 'postalcode'], + ['locality', 'The column header for locality', 'locality'], + ['center', 'The column header for center', 'center'] + ); + + /** + * Different possible format to interpret a date + * + * @var string + */ + protected static $defaultDateInterpreter = "%d/%m/%Y|%e/%m/%y|%d/%m/%Y|%e/%m/%Y"; + + /** + * @var FormFactory + */ + protected $formFactory; + + /** + * ImportPeopleFromCSVCommand constructor. + * + * @param LoggerInterface $logger + * @param TranslatableStringHelper $helper + * @param EntityManagerInterface $em + * @param CustomFieldProvider $customFieldProvider + * @param EventDispatcherInterface $eventDispatcher + * @param FormFactory $formFactory + */ + public function __construct( + LoggerInterface $logger, + TranslatableStringHelper $helper, + EntityManagerInterface $em, + CustomFieldProvider $customFieldProvider, + EventDispatcherInterface $eventDispatcher, + FormFactory $formFactory + ) { + $this->logger = $logger; + $this->helper = $helper; + $this->em = $em; + $this->customFieldProvider = $customFieldProvider; + $this->eventDispatcher = $eventDispatcher; + $this->formFactory = $formFactory; + + parent::__construct('chill:person:import'); + } + + /** + * + */ + protected function configure() + { + $this + ->addArgument('csv_file', InputArgument::REQUIRED, "The CSV file to import") + ->setDescription("Import people from a csv file") + ->setHelp(<<addArgument('locale', InputArgument::REQUIRED, + "The locale to use in displaying translatable strings from entities") + ->addOption( + 'force-center', + null, + InputOption::VALUE_REQUIRED, + "The id of the center" + ) + ->addOption( + 'force', + null, + InputOption::VALUE_NONE, + "Persist people in the database (default is not to persist people)" + ) + ->addOption( + 'delimiter', + 'd', + InputOption::VALUE_OPTIONAL, + "The delimiter character of the csv file", + ",") + ->addOption( + 'enclosure', + null, + InputOption::VALUE_OPTIONAL, + "The enclosure character of the csv file", + '"' + ) + ->addOption( + 'escape', + null, + InputOption::VALUE_OPTIONAL, + "The escape character of the csv file", + "\\" + ) + ->addOption( + 'length', + null, + InputOption::VALUE_OPTIONAL, + "The length of line to read. 0 means unlimited.", + 0 + ) + ->addOption( + 'dump-choice-matching', + null, + InputOption::VALUE_REQUIRED, + "The path of the file to dump the matching between label in CSV and answers" + ) + ->addOption( + 'load-choice-matching', + null, + InputOption::VALUE_OPTIONAL, + "The path of the file to load the matching between label in CSV and answers" + ) + ; + + // mapping columns + foreach (self::$mapping as $m) { + $this->addOptionShortcut($m[0], $m[1], $m[2]); + } + + // other information + $this->addOptionShortcut('birthdate_format', 'Format preference for ' + . 'birthdate. See help for date formats preferences.', + self::$defaultDateInterpreter); + $this->addOptionShortcut('opening_date_format', 'Format preference for ' + . 'opening date. See help for date formats preferences.', + self::$defaultDateInterpreter); + $this->addOptionShortcut('closing_date_format', 'Format preference for ' + . 'closing date. See help for date formats preferences.', + self::$defaultDateInterpreter); + + // mapping column to custom fields + $this->addOption('custom-field', NULL, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, + "Mapping a column to a custom fields key. Example: 1=cf_slug"); + $this->addOption('skip-interactive-field-mapping', null, InputOption::VALUE_NONE, + "Do not ask for interactive mapping"); + } + + /** + * This function is a shortcut to addOption. + * + * @param string $name + * @param string $description + * @param string $default + * @return ImportPeopleFromCSVCommand + */ + protected function addOptionShortcut($name, $description, $default) + { + $this->addOption($name, null, InputOption::VALUE_OPTIONAL, $description, $default); + + return $this; + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + */ + protected function interact(InputInterface $input, OutputInterface $output) + { + // preparing the basic + $this->input = $input; + $this->output = $output; + $this->logger = new ConsoleLogger($output); + + $csv = $this->openCSV(); + + // getting the first row + if (($row = fgetcsv( + $csv, + $input->getOption('length'), + $input->getOption('delimiter'), + $input->getOption('enclosure'), + $input->getOption('escape'))) !== false) { + + try { + $this->matchColumnToCustomField($row); + } finally { + $this->logger->debug('closing csv', array('method' => __METHOD__)); + fclose($csv); + } + } + + // load the matching between csv and label + $this->loadAnswerMatching(); + } + + /** + * @param $row + */ + protected function matchColumnToCustomField($row) + { + + $cfMappingsOptions = $this->input->getOption('custom-field'); + /* @var $em \Doctrine\Persistence\ObjectManager */ + $em = $this->em; + + foreach($cfMappingsOptions as $cfMappingStringOption) { + list($rowNumber, $cfSlug) = preg_split('|=|', $cfMappingStringOption); + + // check that the column exists, getting the column name + $column = $row[$rowNumber]; + + if (empty($column)) { + $message = "The column with row $rowNumber is empty."; + $this->logger->error($message); + throw new \RuntimeException($message); + } + + // check a custom field exists + try { + $customField = $em->createQuery("SELECT cf " + . "FROM ChillCustomFieldsBundle:CustomField cf " + . "JOIN cf.customFieldGroup g " + . "WHERE cf.slug = :slug " + . "AND g.entity = :entity") + ->setParameters(array( + 'slug' => $cfSlug, + 'entity' => Person::class + )) + ->getSingleResult(); + } catch (\Doctrine\ORM\NoResultException $e) { + $message = sprintf( + "The customfield with slug '%s' does not exists. It was associated with column number %d", + $cfSlug, + $rowNumber + ); + $this->logger->error($message); + throw new \RuntimeException($message); + } + // skip if custom field does not exists + if ($customField === NULL) { + $this->logger->error("The custom field with slug $cfSlug could not be found. " + . "Stopping this command."); + throw new \RuntimeException("The custom field with slug $cfSlug could not be found. " + . "Stopping this command."); + } + + $this->logger->notice(sprintf("Matched custom field %s (question : '%s') on column %d (displayed in the file as '%s')", + $customField->getSlug(), $this->helper->localize($customField->getName()), $rowNumber, $column)); + + $this->customFieldMapping[$rowNumber] = $customField; + } + } + + /** + * Load the mapping between answer in CSV and value in choices from a json file + */ + protected function loadAnswerMatching() + { + if ($this->input->hasOption('load-choice-matching')) { + $fs = new Filesystem(); + $filename = $this->input->getOption('load-choice-matching'); + + if (!$fs->exists($filename)) { + $this->logger->warning("The file $filename is not found. Choice matching not loaded"); + } else { + $this->logger->debug("Loading $filename as choice matching"); + $this->cacheAnswersMapping = \json_decode(\file_get_contents($filename), true); + } + } + } + + /** + * + */ + protected function dumpAnswerMatching() + { + if ($this->input->hasOption('dump-choice-matching') && !empty($this->input->getOption('dump-choice-matching'))) { + $this->logger->debug("Dump the matching between answer and choices"); + $str = json_encode($this->cacheAnswersMapping, JSON_PRETTY_PRINT); + + $fs = new Filesystem(); + $filename = $this->input->getOption('dump-choice-matching'); + + $fs->dumpFile($filename, $str); + } + } + + /** + * @param InputInterface $input + * @param OutputInterface $output + * @return int|null|void + * @throws \Exception + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $this->input = $input; + $this->output = $output; + + $this->logger->debug("Setting locale to ".$input->getArgument('locale')); + setlocale(LC_TIME, $input->getArgument('locale')); + + // opening csv as resource + $csv = $this->openCSV(); + + $num = 0; + $line = $this->line = 1; + + try { + while (($row = fgetcsv( + $csv, + $input->getOption('length'), + $input->getOption('delimiter'), + $input->getOption('enclosure'), + $input->getOption('escape'))) !== false) { + $this->logger->debug("Processing line ".$this->line); + if ($line === 1 ) { + $this->logger->debug('Processing line 1, headers'); + $rawHeaders = $row; + $headers = $this->processingHeaders($row); + } else { + $person = $this->createPerson($row, $headers); + + if (count($this->customFieldMapping) > 0) { + $this->processingCustomFields($person, $row); + } + + $event = new Event(); + $event->person = $person; + $event->rawHeaders = $rawHeaders; + $event->row = $row; + $event->headers = $headers; + $event->skipPerson = false; + $event->force = $this->input->getOption('force'); + $event->input = $this->input; + $event->output = $this->output; + $event->helperSet = $this->getHelperSet(); + + $this->eventDispatcher->dispatch('chill_person.person_import', $event); + + if ($this->input->getOption('force') === TRUE + && $event->skipPerson === false) { + $this->em->persist($person); + } + + $num ++; + } + + $line ++; + $this->line++; + } + + if ($this->input->getOption('force') === true) { + $this->logger->debug('persisting entitites'); + $this->em->flush(); + } + } finally { + $this->logger->debug('closing csv', array('method' => __METHOD__)); + fclose($csv); + // dump the matching between answer and choices + $this->dumpAnswerMatching(); + } + } + + /** + * + * @return resource + * @throws \RuntimeException + */ + protected function openCSV() + { + $fs = new Filesystem(); + $filename = $this->input->getArgument('csv_file'); + + if (!$fs->exists($filename)) { + throw new \RuntimeException("The file does not exists or you do not " + . "have the right to read it."); + } + + $resource = fopen($filename, 'r'); + + if ($resource == FALSE) { + throw new \RuntimeException("The file '$filename' could not be opened."); + } + + return $resource; + } + + /** + * + * @param type $firstRow + * @return array where keys are column number, and value is information mapped + */ + protected function processingHeaders($firstRow) + { + $availableOptions = array_map(function($m) { return $m[0]; }, self::$mapping); + $matchedColumnHeaders = array(); + $headers = array(); + + foreach($availableOptions as $option) { + $matchedColumnHeaders[$option] = $this->input->getOption($option); + } + + foreach($firstRow as $key => $content) { + $content = trim($content); + if (in_array($content, $matchedColumnHeaders)) { + $information = array_search($content, $matchedColumnHeaders); + $headers[$key] = $information; + $this->logger->notice("Matched $information on column $key (displayed in the file as '$content')"); + } else { + $this->logger->notice("Column with content '$content' is ignored"); + } + } + + return $headers; + } + + /** + * + * @param array $row + * @param array $headers the processed header : an array as prepared by self::processingHeaders + * @return Person + * @throws \Exception + */ + protected function createPerson($row, $headers) + { + // trying to get the opening date + $openingDateString = trim($row[array_search('opening_date', $headers)]); + $openingDate = $this->processDate($openingDateString, $this->input->getOption('opening_date_format')); + + $person = $openingDate instanceof \DateTime ? new Person($openingDate) : new Person(); + // add the center + $center = $this->getCenter($row, $headers); + + if ($center === null) { + throw new \Exception("center not found"); + } + + $person->setCenter($center); + + foreach($headers as $column => $info) { + + $value = trim($row[$column]); + + switch($info) { + case 'firstname': + $person->setFirstName($value); + break; + case 'lastname': + $person->setLastName($value); + break; + case 'birthdate': + $this->processBirthdate($person, $value); + break; + case 'gender': + $person->setGender($value); + break; + case 'opening_date': + // we have processed this when creating the person object, skipping; + break; + case 'closing_date': + $this->processClosingDate($person, $value); + break; + case 'memo': + $person->setMemo($value); + break; + case 'email': + $person->setEmail($value); + break; + case 'phonenumber': + $person->setPhonenumber($value); + break; + case 'mobilenumber': + $person->setMobilenumber($value); + break; + + // we just keep the column number for those data + case 'postalcode': + $postalCodeValue = $value; + break; + case 'street1': + $street1Value = $value; + break; + case 'locality': + $localityValue = $value; + break; + } + } + + // handle address + if (\in_array('postalcode', $headers)) { + + if (! empty($postalCodeValue)) { + + $address = new Address(); + $postalCode = $this->guessPostalCode($postalCodeValue, $localityValue ?? ''); + + if ($postalCode === null) { + throw new \Exception("The locality is not found"); + } + + $address->setPostcode($postalCode); + + if (\in_array('street1', $headers)) { + $address->setStreetAddress1($street1Value); + } + $address->setValidFrom(new \DateTime('today')); + + $person->addAddress($address); + } + } + + return $person; + } + + /** + * @param $row + * @param $headers + * @return Center|mixed|null|object + */ + protected function getCenter($row, $headers) + { + if ($this->input->hasOption('force-center') && !empty($this->input->getOption('force-center'))) { + return $this->em->getRepository('ChillMainBundle:Center') + ->find($this->input->getOption('force-center')); + } else { + $columnCenter = \array_search('center', $headers); + $centerName = \trim($row[$columnCenter]); + + try { + return $this->em->createQuery('SELECT c FROM ChillMainBundle:Center c ' + . 'WHERE c.name = :center_name') + ->setParameter('center_name', $centerName) + ->getSingleResult() + ; + } catch (\Doctrine\ORM\NonUniqueResultException $e) { + return $this->guessCenter($centerName); + } catch (\Doctrine\ORM\NoResultException $e) { + return $this->guessCenter($centerName); + } + } + } + + /** + * @param $centerName + * @return Center|mixed|null|object + */ + protected function guessCenter($centerName) + { + if (!\array_key_exists('_center_picked', $this->cacheAnswersMapping)) { + $this->cacheAnswersMapping['_center_picked'] = []; + } + + if (\array_key_exists($centerName, $this->cacheAnswersMapping['_center_picked'])) { + $id = $this->cacheAnswersMapping['_center_picked'][$centerName]; + + return $this->em->getRepository(Center::class) + ->find($id); + } + + $centers = $this->em->createQuery("SELECT c FROM ChillMainBundle:Center c " + . "ORDER BY SIMILARITY(c.name, :center_name) DESC") + ->setParameter('center_name', $centerName) + ->setMaxResults(10) + ->getResult() + ; + + if (count($centers) > 1) { + if (\strtolower($centers[0]->getName()) === \strtolower($centerName)) { + return $centers[0]; + } + } + + $centersByName = []; + $names = \array_map(function(Center $c) use (&$centersByName) { + $n = $c->getName(); + $centersByName[$n] = $c; + return $n; + + }, $centers); + $names[] = "none of them"; + + $helper = $this->getHelper('question'); + $question = new ChoiceQuestion(sprintf("Which center match the name \"%s\" ? (default to \"%s\")", $centerName, $names[0]), + $names, + 0); + + $answer = $helper->ask($this->input, $this->output, $question); + + if ($answer === 'none of them') { + $questionCreate = new ConfirmationQuestion("Would you like to create it ?", false); + $create = $helper->ask($this->input, $this->output, $questionCreate); + + if ($create) { + $center = (new Center()) + ->setName($centerName) + ; + + if ($this->input->getOption('force') === TRUE) { + $this->em->persist($center); + $this->em->flush(); + } + + return $center; + } + } + + $center = $centersByName[$answer]; + + $this->cacheAnswersMapping['_center_picked'][$centerName] = $center->getId(); + + return $center; + } + + /** + * @param $postalCode + * @param $locality + * @return mixed|null + */ + protected function guessPostalCode($postalCode, $locality) + { + if (!\array_key_exists('_postal_code_picked', $this->cacheAnswersMapping)) { + $this->cacheAnswersMapping['_postal_code_picked'] = []; + } + + if (\array_key_exists($postalCode, $this->cacheAnswersMapping['_postal_code_picked'])) { + if (\array_key_exists($locality, $this->cacheAnswersMapping['_postal_code_picked'][$postalCode])) { + $id = $this->cacheAnswersMapping['_postal_code_picked'][$postalCode][$locality]; + + return $this->em->getRepository(PostalCode::class)->find($id); + } + } + + $postalCodes = $this->em->createQuery("SELECT pc FROM ".PostalCode::class." pc " + . "WHERE pc.code = :postal_code " + . "ORDER BY SIMILARITY(pc.name, :locality) DESC " + ) + ->setMaxResults(10) + ->setParameter('postal_code', $postalCode) + ->setParameter('locality', $locality) + ->getResult() + ; + + if (count($postalCodes) >= 1) { + if ($postalCodes[0]->getCode() === $postalCode + && $postalCodes[0]->getName() === $locality) { + return $postalCodes[0]; + } + } + + if (count($postalCodes) === 0) { + return null; + } + + $postalCodeByName = []; + $names = \array_map(function(PostalCode $pc) use (&$postalCodeByName) { + $n = $pc->getName(); + $postalCodeByName[$n] = $pc; + + return $n; + }, $postalCodes); + $names[] = 'none of them'; + + $helper = $this->getHelper('question'); + $question = new ChoiceQuestion(sprintf("Which postal code match the " + . "name \"%s\" with postal code \"%s\" ? (default to \"%s\")", + $locality, $postalCode, $names[0]), + $names, + 0); + + $answer = $helper->ask($this->input, $this->output, $question); + + if ($answer === 'none of them') { + return null; + } + + $pc = $postalCodeByName[$answer]; + + $this->cacheAnswersMapping['_postal_code_picked'][$postalCode][$locality] = + $pc->getId(); + + return $pc; + } + + /** + * @param Person $person + * @param $value + * @throws \Exception + */ + protected function processBirthdate(Person $person, $value) + { + if (empty($value)) { return; } + + $date = $this->processDate($value, $this->input->getOption('birthdate_format')); + + if ($date instanceof \DateTime) { + // we correct birthdate if the date is in the future + // the most common error is to set date 100 years to late (ex. 2063 instead of 1963) + if ($date > new \DateTime('yesterday')) { + $date = $date->sub(new \DateInterval('P100Y')); + } + + $person->setBirthdate($date); + + return; + } + + // if we arrive here, we could not process the date + $this->logger->warning(sprintf( + "Line %d : the birthdate could not be interpreted. Was %s.", + $this->line, + $value)); + + } + + /** + * @param Person $person + * @param $value + * @throws \Exception + */ + protected function processClosingDate(Person $person, $value) + { + if (empty($value)) { return; } + + // we skip if the opening date is now (or after yesterday) + /* @var $period \Chill\PersonBundle\Entity\AccompanyingPeriod */ + $period = $person->getCurrentAccompanyingPeriod(); + + if ($period->getOpeningDate() > new \DateTime('yesterday')) { + $this->logger->debug(sprintf("skipping a closing date because opening date is after yesterday (%s)", + $period->getOpeningDate()->format('Y-m-d'))); + return; + } + + + $date = $this->processDate($value, $this->input->getOption('closing_date_format')); + + if ($date instanceof \DateTime) { + // we correct birthdate if the date is in the future + // the most common error is to set date 100 years to late (ex. 2063 instead of 1963) + if ($date > new \DateTime('yesterday')) { + $date = $date->sub(new \DateInterval('P100Y')); + } + + $period->setClosingDate($date); + $person->close(); + return; + } + + // if we arrive here, we could not process the date + $this->logger->warning(sprintf( + "Line %d : the closing date could not be interpreted. Was %s.", + $this->line, + $value)); + } + + /** + * @param Person $person + * @param $row + * @throws \Exception + */ + protected function processingCustomFields(Person $person, $row) + { + + /* @var $cfProvider \Chill\CustomFieldsBundle\Service\CustomFieldProvider */ + $cfProvider = $this->customFieldProvider; + $cfData = array(); + + /* @var $$customField \Chill\CustomFieldsBundle\Entity\CustomField */ + foreach($this->customFieldMapping as $rowNumber => $customField) { + $builder = $this->formFactory->createBuilder(); + $cfProvider->getCustomFieldByType($customField->getType()) + ->buildForm($builder, $customField); + $form = $builder->getForm(); + + // get the type of the form + $type = get_class($form->get($customField->getSlug()) + ->getConfig()->getType()->getInnerType()); + $this->logger->debug(sprintf("Processing a form of type %s", + $type)); + + switch ($type) { + case \Symfony\Component\Form\Extension\Core\Type\TextType::class: + $cfData[$customField->getSlug()] = + $this->processTextType($row[$rowNumber], $form, $customField); + break; + case \Symfony\Component\Form\Extension\Core\Type\ChoiceType::class: + case \Chill\MainBundle\Form\Type\Select2ChoiceType::class: + $cfData[$customField->getSlug()] = + $this->processChoiceType($row[$rowNumber], $form, $customField); + } + + } + + $person->setCFData($cfData); + } + + /** + * Process a text type on a custom field + * + * @param type $value + * @param \Symfony\Component\Form\FormInterface $form + * @return type + */ + protected function processTextType( + $value, + \Symfony\Component\Form\FormInterface $form, + \Chill\CustomFieldsBundle\Entity\CustomField $cf + ) + { + $form->submit(array($cf->getSlug() => $value)); + + $value = $form->getData()[$cf->getSlug()]; + + $this->logger->debug(sprintf("Found value : %s for custom field with question " + . "'%s'", $value, $this->helper->localize($cf->getName()))); + + return $value; + } + + protected $cacheAnswersMapping = array(); + + + /** + * Process a custom field choice. + * + * The method try to guess if the result exists amongst the text of the possible + * choices. If the texts exists, then this is picked. Else, ask the user. + * + * @param string $value + * @param \Symfony\Component\Form\FormInterface $form + * @param \Chill\CustomFieldsBundle\Entity\CustomField $cf + * @return string + * @throws \Exception + */ + protected function processChoiceType( + $value, + \Symfony\Component\Form\FormInterface $form, + \Chill\CustomFieldsBundle\Entity\CustomField $cf + ) + { + // getting the possible answer and their value : + $view = $form->get($cf->getSlug())->createView(); + $answers = $this->collectChoicesAnswers($view->vars['choices']); + + // if we do not have any answer on the question, throw an error. + if (count($answers) === 0) { + $message = sprintf( + "The question '%s' with slug '%s' does not count any answer.", + $this->helper->localize($cf->getName()), + $cf->getSlug() + ); + + $this->logger->error($message, array( + 'method' => __METHOD__, + 'slug' => $cf->getSlug(), + 'question' => $this->helper->localize($cf->getName()) + )); + + throw new \RuntimeException($message); + } + + if ($view->vars['required'] === false) { + $answers[null] = '** no answer'; + } + + // the answer does not exists in cache. Try to find it, or asks the user + if (!isset($this->cacheAnswersMapping[$cf->getSlug()][$value])) { + + // try to find the answer (with array_keys and a search value + $values = array_keys( + array_map(function($label) { return trim(strtolower($label)); }, $answers), + trim(strtolower($value)), + true + ); + + if (count($values) === 1) { + // we could guess an answer ! + $this->logger->info("This question accept multiple answers"); + $this->cacheAnswersMapping[$cf->getSlug()][$value] = + $view->vars['multiple'] == false ? $values[0] : array($values[0]); + $this->logger->info(sprintf("Guessed that value '%s' match with key '%s' " + . "because the CSV and the label are equals.", + $value, $values[0])); + } else { + // we could nog guess an answer. Asking the user. + $this->output->writeln("I do not know the answer to this question : "); + $this->output->writeln($this->helper->localize($cf->getName())); + + // printing the possible answers + /* @var $table \Symfony\Component\Console\Helper\Table */ + $table = new Table($this->output); + $table->setHeaders(array('#', 'label', 'value')); + $i = 0; + + foreach($answers as $key => $answer) { + $table->addRow(array( + $i, $answer, $key + )); + $matchingTableRowAnswer[$i] = $key; + $i++; + } + $table->render($this->output); + + $question = new ChoiceQuestion( + sprintf('Please pick your choice for the value "%s"', $value), + array_keys($matchingTableRowAnswer) + ); + $question->setErrorMessage("This choice is not possible"); + + if ($view->vars['multiple']) { + $this->logger->debug("this question is multiple"); + $question->setMultiselect(true); + } + + $selected = $this->getHelper('question')->ask($this->input, $this->output, $question); + + $this->output->writeln(sprintf('You have selected "%s"', + is_array($answers[$matchingTableRowAnswer[$selected]]) ? + implode(',', $answers[$matchingTableRowAnswer[$selected]]) : + $answers[$matchingTableRowAnswer[$selected]]) + ); + + // recording value in cache + $this->cacheAnswersMapping[$cf->getSlug()][$value] = $matchingTableRowAnswer[$selected]; + $this->logger->debug(sprintf("Setting the value '%s' in cache for customfield '%s' and answer '%s'", + is_array($this->cacheAnswersMapping[$cf->getSlug()][$value]) ? + implode(', ', $this->cacheAnswersMapping[$cf->getSlug()][$value]) : + $this->cacheAnswersMapping[$cf->getSlug()][$value], + $cf->getSlug(), + $value)); + } + } + + $form->submit(array($cf->getSlug() => $this->cacheAnswersMapping[$cf->getSlug()][$value])); + $value = $form->getData()[$cf->getSlug()]; + + $this->logger->debug(sprintf( + "Found value : %s for custom field with question '%s'", + is_array($value) ? implode(',', $value) : $value, + $this->helper->localize($cf->getName())) + ); + + return $value; + } + + /** + * Recursive method to collect the possibles answer from a ChoiceType (or + * its inherited types). + * + * @param \Symfony\Component\Form\FormInterface $form + * @return array where + * @throws \Exception + */ + private function collectChoicesAnswers($choices) + { + $answers = array(); + + /* @var $choice \Symfony\Component\Form\ChoiceList\View\ChoiceView */ + foreach($choices as $choice) { + if ($choice instanceof \Symfony\Component\Form\ChoiceList\View\ChoiceView) { + $answers[$choice->value] = $choice->label; + } elseif ($choice instanceof \Symfony\Component\Form\ChoiceList\View\ChoiceGroupView) { + $answers = $answers + $this->collectChoicesAnswers($choice->choices); + } else { + throw new \Exception(sprintf( + "The choice type is not know. Expected '%s' or '%s', get '%s'", + \Symfony\Component\Form\ChoiceList\View\ChoiceView::class, + \Symfony\Component\Form\ChoiceList\View\ChoiceGroupView::class, + get_class($choice) + )); + } + } + + return $answers; + } + + /** + * @param $value + * @param $formats + * @return bool|\DateTime + */ + protected function processDate($value, $formats) + { + $possibleFormats = explode("|", $formats); + + foreach($possibleFormats as $format) { + $this->logger->debug("Trying format $format", array(__METHOD__)); + $dateR = strptime($value, $format); + + if (is_array($dateR) && $dateR['unparsed'] === '') { + $string = sprintf("%04d-%02d-%02d %02d:%02d:%02d", + ($dateR['tm_year']+1900), + ($dateR['tm_mon']+1), + ($dateR['tm_mday']), + ($dateR['tm_hour']), + ($dateR['tm_min']), + ($dateR['tm_sec'])); + $date = \DateTime::createFromFormat("Y-m-d H:i:s", $string); + $this->logger->debug(sprintf("Interpreting %s as date %s", $value, $date->format("Y-m-d H:i:s"))); + + return $date; + } + } + + // if we arrive here, we could not process the date + $this->logger->debug(sprintf( + "Line %d : a date could not be interpreted. Was %s.", + $this->line, + $value)); + + return false; + } + + +} diff --git a/src/Bundle/ChillPerson/Config/ConfigPersonAltNamesHelper.php b/src/Bundle/ChillPerson/Config/ConfigPersonAltNamesHelper.php new file mode 100644 index 000000000..92c093385 --- /dev/null +++ b/src/Bundle/ChillPerson/Config/ConfigPersonAltNamesHelper.php @@ -0,0 +1,70 @@ +config = $config; + } + + /** + * Return true if at least one alt name is configured + * + * @return bool + */ + public function hasAltNames(): bool + { + return count($this->config) > 0; + } + + /** + * get the choices as key => values + * + * @return array + */ + public function getChoices(): array + { + $choices = []; + foreach ($this->config as $entry) { + + $labels = $entry['labels']; + $lang = false; + $label = false; + $cur = reset($labels); + while ($cur) { + if (key($labels) === 'lang') { + $lang = current($labels); + } + + if (key($labels) === 'label') { + $label = current($labels); + } + + if ($lang !== FALSE && $label !== FALSE) { + $choices[$entry['key']][$lang] = $label; + $lang = false; + $label = false; + } + $cur = next($labels); + } + } + + return $choices; + } + +} diff --git a/src/Bundle/ChillPerson/Controller/AccompanyingPeriodController.php b/src/Bundle/ChillPerson/Controller/AccompanyingPeriodController.php new file mode 100644 index 000000000..2f6925373 --- /dev/null +++ b/src/Bundle/ChillPerson/Controller/AccompanyingPeriodController.php @@ -0,0 +1,441 @@ +, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Controller; + +use Chill\PersonBundle\Privacy\PrivacyEvent; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Form\AccompanyingPeriodType; +use Chill\PersonBundle\Entity\AccompanyingPeriod; +use Doctrine\Common\Collections\Criteria; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; + +/** + * Class AccompanyingPeriodController + * + * @package Chill\PersonBundle\Controller + */ +class AccompanyingPeriodController extends AbstractController +{ + /** + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * ReportController constructor. + * + * @param EventDispatcherInterface $eventDispatcher + */ + public function __construct(EventDispatcherInterface $eventDispatcher) + { + $this->eventDispatcher = $eventDispatcher; + } + + /** + * @param $person_id + * @return Response + */ + public function listAction($person_id){ + + $person = $this->_getPerson($person_id); + + $event = new PrivacyEvent($person, array( + 'element_class' => AccompanyingPeriod::class, + 'action' => 'list' + )); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->render('ChillPersonBundle:AccompanyingPeriod:list.html.twig', + array('accompanying_periods' => $person->getAccompanyingPeriodsOrdered(), + 'person' => $person)); + } + + /** + * @param $person_id + * @param Request $request + * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response + */ + public function createAction($person_id, Request $request) + { + + $person = $this->_getPerson($person_id); + + $this->denyAccessUnlessGranted(PersonVoter::UPDATE, $person, + 'You are not allowed to update this person'); + + $accompanyingPeriod = new AccompanyingPeriod(new \DateTime()); + $accompanyingPeriod->setClosingDate(new \DateTime()); + + $person->addAccompanyingPeriod( + $accompanyingPeriod); + + $form = $this->createForm( + AccompanyingPeriodType::class, + $accompanyingPeriod, + [ + 'period_action' => 'create', + 'center' => $person->getCenter() + ]); + + if ($request->getMethod() === 'POST') { + $form->handleRequest($request); + $errors = $this->_validatePerson($person); + $flashBag = $this->get('session')->getFlashBag(); + + if ($form->isValid(array('Default', 'closed')) + && count($errors) === 0) { + + $em = $this->getDoctrine()->getManager(); + $em->persist($accompanyingPeriod); + $em->flush(); + $flashBag->add('success', + $this->get('translator')->trans( + 'A period has been created.')); + + return $this->redirect($this->generateUrl('chill_person_accompanying_period_list', + array('person_id' => $person->getId()))); + } else { + $flashBag->add('error', $this->get('translator') + ->trans('Error! Period not created!')); + + foreach($errors as $error) { + $flashBag->add('info', $error->getMessage()); + } + } + } + + return $this->render('ChillPersonBundle:AccompanyingPeriod:form.html.twig', + array( + 'form' => $form->createView(), + 'person' => $person, + 'accompanying_period' => $accompanyingPeriod + ) + ); + } + + /** + * @param $person_id + * @param $period_id + * @param Request $request + * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response|\Symfony\Component\HttpKernel\Exception\NotFoundHttpException + */ + public function updateAction($person_id, $period_id, Request $request){ + $em = $this->getDoctrine()->getManager(); + + $accompanyingPeriod = $em->getRepository('ChillPersonBundle:AccompanyingPeriod') + ->find($period_id); + + if ($accompanyingPeriod === null) { + return $this->createNotFoundException("Period with id ".$period_id. + " is not found"); + } + + $person = $accompanyingPeriod->getPerson(); + + $this->denyAccessUnlessGranted(PersonVoter::UPDATE, $person, + 'You are not allowed to update this person'); + + $form = $this->createForm(AccompanyingPeriodType::class, + $accompanyingPeriod, array('period_action' => 'update', + 'center' => $person->getCenter())); + + if ($request->getMethod() === 'POST') { + $form->handleRequest($request); + $errors = $this->_validatePerson($person); + $flashBag = $this->get('session')->getFlashBag(); + + if ($form->isValid(array('Default', 'closed')) + && count($errors) === 0) { + $em->flush(); + + $flashBag->add('success', + $this->get('translator')->trans( + 'An accompanying period has been updated.')); + + return $this->redirect($this->generateUrl('chill_person_accompanying_period_list', + array('person_id' => $person->getId()))); + } else { + $flashBag->add('error', $this->get('translator') + ->trans('Error when updating the period')); + + foreach($errors as $error) { + $flashBag->add('info', $error->getMessage()); + } + } + } + + return $this->render('ChillPersonBundle:AccompanyingPeriod:form.html.twig', + array( + 'form' => $form->createView(), + 'person' => $person, + 'accompanying_period' => $accompanyingPeriod + ) ); + } + + /** + * @param $person_id + * @param Request $request + * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response + * @throws \Exception + */ + public function closeAction($person_id, Request $request) + { + + $person = $this->_getPerson($person_id); + + $this->denyAccessUnlessGranted(PersonVoter::UPDATE, $person, + 'You are not allowed to update this person'); + + if ($person->isOpen() === false) { + $this->get('session')->getFlashBag() + ->add('error', $this->get('translator') + ->trans('Beware period is closed', + array('%name%' => $person->__toString()))); + + return $this->redirect( + $this->generateUrl('chill_person_accompanying_period_list', array( + 'person_id' => $person->getId() + ))); + } + + $current = $person->getCurrentAccompanyingPeriod(); + + $form = $this->createForm(AccompanyingPeriodType::class, $current, array( + 'period_action' => 'close', + 'center' => $person->getCenter() + )); + + if ($request->getMethod() === 'POST') { + $form->handleRequest($request); + + if ($form->isValid()){ + $person->close($current); + $errors = $this->_validatePerson($person); + + if (count($errors) === 0) { + $this->get('session')->getFlashBag() + ->add('success', $this->get('translator') + ->trans('An accompanying period has been closed.', + array('%name%' => $person->__toString()))); + + $this->getDoctrine()->getManager()->flush(); + + return $this->redirect( + $this->generateUrl('chill_person_accompanying_period_list', array( + 'person_id' => $person->getId() + )) + ); + } else { + $this->get('session')->getFlashBag() + ->add('error', $this->get('translator') + ->trans('Error! Period not closed!')); + + foreach ($errors as $error) { + $this->get('session')->getFlashBag() + ->add('info', $error->getMessage()); + } + } + } else { //if form is not valid + $this->get('session')->getFlashBag() + ->add('error', + $this->get('translator') + ->trans('Pediod closing form is not valid') + ); + + foreach ($form->getErrors() as $error) { + $this->get('session')->getFlashBag() + ->add('info', $error->getMessage()); + } + } + } + + return $this->render('ChillPersonBundle:AccompanyingPeriod:form.html.twig', + array( + 'form' => $form->createView(), + 'person' => $person, + 'accompanying_period' => $current + )); + } + + /** + * @param Person $person + * @return \Symfony\Component\Validator\ConstraintViolationListInterface + */ + private function _validatePerson(Person $person) { + $errors = $this->get('validator')->validate($person, null, + array('Default')); + $errors_accompanying_period = $this->get('validator')->validate($person, null, + array('accompanying_period_consistent')); + + foreach($errors_accompanying_period as $error ) { + $errors->add($error); + } + + return $errors; + } + + /** + * @param $person_id + * @param Request $request + * @return \Symfony\Component\HttpFoundation\RedirectResponse|Response + */ + public function openAction($person_id, Request $request) { + $person = $this->_getPerson($person_id); + + $this->denyAccessUnlessGranted(PersonVoter::UPDATE, $person, + 'You are not allowed to update this person'); + + //in case the person is already open + if ($person->isOpen()) { + $this->get('session')->getFlashBag() + ->add('error', $this->get('translator') + ->trans('Error! Period %name% is not closed ; it can be open', + array('%name%' => $person->__toString()))); + + return $this->redirect( + $this->generateUrl('chill_person_accompanying_period_list', array( + 'person_id' => $person->getId() + ))); + } + + $accompanyingPeriod = new AccompanyingPeriod(new \DateTime()); + + $form = $this->createForm(AccompanyingPeriodType::class, + $accompanyingPeriod, array('period_action' => 'open', + 'center' => $person->getCenter())); + + if ($request->getMethod() === 'POST') { + $form->handleRequest($request); + + if ($form->isValid()) { + $person->open($accompanyingPeriod); + + $errors = $this->_validatePerson($person); + + if (count($errors) <= 0) { + $this->get('session')->getFlashBag() + ->add('success', $this->get('translator') + ->trans('An accompanying period has been opened.', + array('%name%' => $person->__toString()))); + + $this->getDoctrine()->getManager()->flush(); + + return $this->redirect( + $this->generateUrl('chill_person_accompanying_period_list', array( + 'person_id' => $person->getId() + ))); + } else { + $this->get('session')->getFlashBag() + ->add('error', $this->get('translator') + ->trans('Period not opened')); + + foreach ($errors as $error) { + $this->get('session')->getFlashBag() + ->add('info', $error->getMessage()); + } + } + } else { // if errors in forms + $this->get('session')->getFlashBag() + ->add('error', $this->get('translator') + ->trans('Period not opened : form is invalid')); + } + } + + return $this->render('ChillPersonBundle:AccompanyingPeriod:form.html.twig', + array('form' => $form->createView(), + 'person' => $person, + 'accompanying_period' => $accompanyingPeriod)); + } + + /** + * @param $person_id + * @param $period_id + * @param Request $request + * @return object|\Symfony\Component\HttpFoundation\RedirectResponse|Response + */ + public function reOpenAction($person_id, $period_id, Request $request) + { + $person = $this->_getPerson($person_id); + + $criteria = Criteria::create(); + $criteria->where($criteria->expr()->eq('id', $period_id)); + + /* @var $period AccompanyingPeriod */ + $period = $person->getAccompanyingPeriods() + ->matching($criteria) + ->first(); + + if ($period === NULL) { + throw $this->createNotFoundException('period not found'); + } + + $confirm = $request->query->getBoolean('confirm', false); + + if ($confirm === true && $period->canBeReOpened()) { + $period->reOpen(); + + $this->_validatePerson($person); + + $this->getDoctrine()->getManager()->flush(); + + $this->addFlash('success', $this->get('translator')->trans( + 'The period has been re-opened')); + + return $this->redirectToRoute('chill_person_accompanying_period_list', + array('person_id' => $person->getId())); + } elseif ($confirm === false && $period->canBeReOpened()) { + return $this->render('ChillPersonBundle:AccompanyingPeriod:re_open.html.twig', array( + 'period' => $period, + 'person' => $person + )); + } else { + return (new Response()) + ->setStatusCode(Response::HTTP_BAD_REQUEST) + ->setContent("You cannot re-open this period"); + } + } + + /** + * + * @param int $id + * @return Person + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the person is not found + */ + private function _getPerson($id) { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person')->find($id); + + if ($person === null) { + throw $this->createNotFoundException('Person not found'); + } + + $this->denyAccessUnlessGranted(PersonVoter::SEE, $person, + "You are not allowed to see this person"); + + return $person; + } +} diff --git a/src/Bundle/ChillPerson/Controller/AdminClosingMotiveController.php b/src/Bundle/ChillPerson/Controller/AdminClosingMotiveController.php new file mode 100644 index 000000000..483619be6 --- /dev/null +++ b/src/Bundle/ChillPerson/Controller/AdminClosingMotiveController.php @@ -0,0 +1,55 @@ +query->has('parent_id')) { + $parentId = $request->query->getInt('parent_id'); + + $parent = $this->getDoctrine()->getManager() + ->getRepository($this->getEntityClass()) + ->find($parentId); + + if (NULL === $parent) { + throw $this->createNotFoundException('parent id not found'); + } + + $entity->setParent($parent); + } + return $entity; + } + + /** + * @param string $action + * @param \Doctrine\ORM\QueryBuilder|mixed $query + * @param Request $request + * @param PaginatorInterface $paginator + * @return \Doctrine\ORM\QueryBuilder|mixed + */ + protected function orderQuery(string $action, $query, Request $request, PaginatorInterface $paginator) + { + /** @var \Doctrine\ORM\QueryBuilder $query */ + return $query->orderBy('e.ordering', 'ASC'); + } +} diff --git a/src/Bundle/ChillPerson/Controller/AdminController.php b/src/Bundle/ChillPerson/Controller/AdminController.php new file mode 100644 index 000000000..751beb7a8 --- /dev/null +++ b/src/Bundle/ChillPerson/Controller/AdminController.php @@ -0,0 +1,31 @@ +render('ChillPersonBundle:Admin:layout.html.twig', []); + } + + /** + * @return \Symfony\Component\HttpFoundation\RedirectResponse + */ + public function redirectToAdminIndexAction() + { + return $this->redirectToRoute('chill_main_admin_central'); + } +} diff --git a/src/Bundle/ChillPerson/Controller/AdminMaritalStatusController.php b/src/Bundle/ChillPerson/Controller/AdminMaritalStatusController.php new file mode 100644 index 000000000..da46ac5c5 --- /dev/null +++ b/src/Bundle/ChillPerson/Controller/AdminMaritalStatusController.php @@ -0,0 +1,15 @@ +, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Controller; + +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Form\Extension\Core\Type\SubmitType; +use Chill\PersonBundle\Entity\Person; +use Chill\MainBundle\Form\Type\AddressType; +use Chill\MainBundle\Entity\Address; +use Doctrine\Common\Collections\Criteria; +use Symfony\Component\HttpFoundation\Request; + +/** + * Class PersonAddressController + * Controller for addresses associated with person + * + * @package Chill\PersonBundle\Controller + * @author Julien Fastré + * @author Champs Libres + */ +class PersonAddressController extends AbstractController +{ + + public function listAction($person_id) + { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === null) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found "); + } + + $this->denyAccessUnlessGranted( + 'CHILL_PERSON_SEE', + $person, + "You are not allowed to edit this person." + ); + + return $this->render('ChillPersonBundle:Address:list.html.twig', array( + 'person' => $person + )); + } + + public function newAction($person_id) + { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === null) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found "); + } + + $this->denyAccessUnlessGranted( + 'CHILL_PERSON_UPDATE', + $person, + "You are not allowed to edit this person." + ); + + $address = new Address(); + + $form = $this->createCreateForm($person, $address); + + return $this->render('ChillPersonBundle:Address:new.html.twig', array( + 'person' => $person, + 'form' => $form->createView() + )); + } + + public function createAction($person_id, Request $request) + { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === null) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found "); + } + + $this->denyAccessUnlessGranted( + 'CHILL_PERSON_UPDATE', + $person, + "You are not allowed to edit this person." + ); + + $address = new Address(); + + $form = $this->createCreateForm($person, $address); + $form->handleRequest($request); + + $person->addAddress($address); + + if ($form->isSubmitted() && $form->isValid()) { + $validatePersonErrors = $this->validatePerson($person); + + if (count($validatePersonErrors) !== 0) { + foreach ($validatePersonErrors as $error) { + $this->addFlash('error', $error->getMessage()); + } + } elseif ($form->isValid()) { + + $em = $this->getDoctrine()->getManager(); + $em->flush(); + + $this->addFlash( + 'success', + $this->get('translator')->trans('The new address was created successfully') + ); + + return $this->redirectToRoute('chill_person_address_list', array( + 'person_id' => $person->getId() + )); + } else { + $this->addFlash('error', $this->get('translator') + ->trans('Error! Address not created!')); + } + } + + return $this->render('ChillPersonBundle:Address:new.html.twig', array( + 'person' => $person, + 'form' => $form->createView() + )); + } + + public function editAction($person_id, $address_id) + { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === null) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found "); + } + + $this->denyAccessUnlessGranted( + 'CHILL_PERSON_UPDATE', + $person, + "You are not allowed to edit this person." + ); + + $address = $this->findAddressById($person, $address_id); + + $form = $this->createEditForm($person, $address); + + return $this->render('ChillPersonBundle:Address:edit.html.twig', array( + 'person' => $person, + 'address' => $address, + 'form' => $form->createView() + )); + } + + public function updateAction($person_id, $address_id, Request $request) + { + $person = $this->getDoctrine()->getManager() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === null) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found "); + } + + $this->denyAccessUnlessGranted( + 'CHILL_PERSON_UPDATE', + $person, + "You are not allowed to edit this person." + ); + + $address = $this->findAddressById($person, $address_id); + + $form = $this->createEditForm($person, $address); + $form->handleRequest($request); + + if ($form->isSubmitted() and $form->isValid()) { + $validatePersonErrors = $this->validatePerson($person); + + if (count($validatePersonErrors) !== 0) { + foreach ($validatePersonErrors as $error) { + $this->addFlash('error', $error->getMessage()); + } + } elseif ($form->isValid()) { + $this->getDoctrine()->getManager() + ->flush(); + + $this->addFlash('success', $this->get('translator')->trans( + "The address has been successfully updated" + )); + + return $this->redirectToRoute('chill_person_address_list', array( + 'person_id' => $person->getId() + )); + } else { + $this->addFlash('error', $this->get('translator') + ->trans('Error when updating the period')); + } + } + + return $this->render('ChillPersonBundle:Address:edit.html.twig', array( + 'person' => $person, + 'address' => $address, + 'form' => $form->createView() + )); + } + + /** + * @param Person $person + * @param Address $address + * @return \Symfony\Component\Form\Form + */ + protected function createEditForm(Person $person, Address $address) + { + $form = $this->createForm(AddressType::class, $address, array( + 'method' => 'POST', + 'action' => $this->generateUrl('chill_person_address_update', array( + 'person_id' => $person->getId(), + 'address_id' => $address->getId() + )), + 'has_no_address' => true + )); + + $form->add('submit', SubmitType::class, array( + 'label' => 'Submit' + )); + + return $form; + } + + /** + * + * @param Person $person + * @param Address $address + * @return \Symfony\Component\Form\Form + */ + protected function createCreateForm(Person $person, Address $address) + { + $form = $this->createForm(AddressType::class, $address, array( + 'method' => 'POST', + 'action' => $this->generateUrl('chill_person_address_create', array( + 'person_id' => $person->getId() + )), + 'has_no_address' => true + )); + + $form->add('submit', SubmitType::class, array( + 'label' => 'Submit' + )); + + return $form; + } + + /** + * + * @param Person $person + * @param int $address_id + * @return Address + * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException if the address id does not exists or is not associated with given person + */ + protected function findAddressById(Person $person, $address_id) + { + // filtering address + $criteria = Criteria::create() + ->where(Criteria::expr()->eq('id', $address_id)) + ->setMaxResults(1); + $addresses = $person->getAddresses()->matching($criteria); + + if (count($addresses) === 0) { + throw $this->createNotFoundException("Address with id $address_id " + . "matching person $person_id not found "); + } + + return $addresses->first(); + } + + /** + * @param Chill\PersonBundle\Entity\Person $person + * @return \Symfony\Component\Validator\ConstraintViolationListInterface + */ + private function validatePerson(Person $person) + { + $errors = $this->get('validator') + ->validate($person, null, array('Default')); + $errors_addresses_consistent = $this->get('validator') + ->validate($person, null, array('addresses_consistent')); + + foreach($errors_addresses_consistent as $error) { + $errors->add($error); + } + + return $errors; + } +} diff --git a/src/Bundle/ChillPerson/Controller/PersonController.php b/src/Bundle/ChillPerson/Controller/PersonController.php new file mode 100644 index 000000000..28a960520 --- /dev/null +++ b/src/Bundle/ChillPerson/Controller/PersonController.php @@ -0,0 +1,416 @@ +, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Controller; + +use Chill\PersonBundle\Privacy\PrivacyEvent; +use Psr\Log\LoggerInterface; +use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Form\PersonType; +use Chill\PersonBundle\Form\CreationPersonType; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Role\Role; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\PersonBundle\Search\SimilarPersonMatcher; +use Symfony\Component\Translation\TranslatorInterface; +use Chill\MainBundle\Search\SearchProvider; +use Chill\PersonBundle\Repository\PersonRepository; +use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; + +/** + * Class PersonController + * + * @package Chill\PersonBundle\Controller + */ +class PersonController extends AbstractController +{ + /** + * + * @var SimilarPersonMatcher + */ + protected $similarPersonMatcher; + + /** + * + * @var TranslatorInterface + */ + protected $translator; + + + /** + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * + * @var PersonRepository; + */ + protected $personRepository; + + /** + * + * @var ConfigPersonAltNamesHelper + */ + protected $configPersonAltNameHelper; + + /** + * @var \Psr\Log\LoggerInterface + */ + private $logger; + + public function __construct( + SimilarPersonMatcher $similarPersonMatcher, + TranslatorInterface $translator, + EventDispatcherInterface $eventDispatcher, + PersonRepository $personRepository, + ConfigPersonAltNamesHelper $configPersonAltNameHelper, + LoggerInterface $logger + ) { + $this->similarPersonMatcher = $similarPersonMatcher; + $this->translator = $translator; + $this->eventDispatcher = $eventDispatcher; + $this->configPersonAltNameHelper = $configPersonAltNameHelper; + $this->personRepository = $personRepository; + $this->logger = $logger; + } + + public function getCFGroup() + { + $cFGroup = null; + + $em = $this->getDoctrine()->getManager(); + $cFDefaultGroup = $em->getRepository("ChillCustomFieldsBundle:CustomFieldsDefaultGroup") + ->findOneByEntity("Chill\PersonBundle\Entity\Person"); + + if($cFDefaultGroup) { + $cFGroup = $cFDefaultGroup->getCustomFieldsGroup(); + } + + return $cFGroup; + } + + public function viewAction($person_id) + { + $person = $this->_getPerson($person_id); + + if ($person === null) { + throw $this->createNotFoundException("Person with id $person_id not" + . " found on this server"); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_SEE', $person, + "You are not allowed to see this person."); + + $event = new PrivacyEvent($person); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->render('ChillPersonBundle:Person:view.html.twig', + array( + "person" => $person, + "cFGroup" => $this->getCFGroup(), + "alt_names" => $this->configPersonAltNameHelper->getChoices(), + )); + } + + public function editAction($person_id) + { + $person = $this->_getPerson($person_id); + + if ($person === null) { + return $this->createNotFoundException(); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, + 'You are not allowed to edit this person'); + + $form = $this->createForm(PersonType::class, $person, + array( + "action" => $this->generateUrl('chill_person_general_update', + array("person_id" => $person_id)), + "cFGroup" => $this->getCFGroup() + ) + ); + + return $this->render('ChillPersonBundle:Person:edit.html.twig', + array('person' => $person, 'form' => $form->createView())); + } + + public function updateAction($person_id, Request $request) + { + $person = $this->_getPerson($person_id); + + if ($person === null) { + return $this->createNotFoundException(); + } + + $this->denyAccessUnlessGranted('CHILL_PERSON_UPDATE', $person, + 'You are not allowed to edit this person'); + + $form = $this->createForm(PersonType::class, $person, + array("cFGroup" => $this->getCFGroup())); + + if ($request->getMethod() === 'POST') { + $form->handleRequest($request); + + if ( ! $form->isValid() ) { + $this->get('session') + ->getFlashBag()->add('error', $this->translator + ->trans('This form contains errors')); + + return $this->render('ChillPersonBundle:Person:edit.html.twig', + array('person' => $person, + 'form' => $form->createView())); + } + + $this->get('session')->getFlashBag() + ->add('success', + $this->get('translator') + ->trans('The person data has been updated') + ); + + $em = $this->getDoctrine()->getManager(); + $em->flush(); + + $url = $this->generateUrl('chill_person_view', array( + 'person_id' => $person->getId() + )); + + return $this->redirect($url); + } + } + + public function newAction() + { + // this is a dummy default center. + $defaultCenter = $this->get('security.token_storage') + ->getToken() + ->getUser() + ->getGroupCenters()[0] + ->getCenter(); + + $person = (new Person(new \DateTime('now'))) + ->setCenter($defaultCenter); + + $form = $this->createForm( + CreationPersonType::class, + $person, + array( + 'action' => $this->generateUrl('chill_person_review'), + 'form_status' => CreationPersonType::FORM_NOT_REVIEWED + )); + + return $this->_renderNewForm($form); + } + + private function _renderNewForm($form) + { + return $this->render('ChillPersonBundle:Person:create.html.twig', + array( + 'form' => $form->createView() + )); + } + + /** + * + * @param type $form + * @return \Chill\PersonBundle\Entity\Person + */ + private function _bindCreationForm($form) + { + /** + * @var Person + */ + $person = $form->getData(); + + $periods = $person->getAccompanyingPeriodsOrdered(); + $period = $periods[0]; + $period->setOpeningDate($form['creation_date']->getData()); +// $person = new Person($form['creation_date']->getData()); +// +// $person->setFirstName($form['firstName']->getData()) +// ->setLastName($form['lastName']->getData()) +// ->setGender($form['gender']->getData()) +// ->setBirthdate($form['birthdate']->getData()) +// ->setCenter($form['center']->getData()) +// ; + + return $person; + } + + /** + * + * @param \Chill\PersonBundle\Entity\Person $person + * @return \Symfony\Component\Validator\ConstraintViolationListInterface + */ + private function _validatePersonAndAccompanyingPeriod(Person $person) + { + $errors = $this->get('validator') + ->validate($person, null, array('creation')); + + //validate accompanying periods + $periods = $person->getAccompanyingPeriods(); + + foreach ($periods as $period) { + $period_errors = $this->get('validator') + ->validate($period); + + //group errors : + foreach($period_errors as $error) { + $errors->add($error); + } + } + + return $errors; + } + + public function reviewAction(Request $request) + { + if ($request->getMethod() !== 'POST') { + $r = new Response("You must send something to review the creation of a new Person"); + $r->setStatusCode(400); + return $r; + } + + $form = $this->createForm( + //CreationPersonType::NAME, + CreationPersonType::class, + new Person(), + array( + 'action' => $this->generateUrl('chill_person_create'), + 'form_status' => CreationPersonType::FORM_BEING_REVIEWED + )); + + $form->handleRequest($request); + + $person = $this->_bindCreationForm($form); + + $errors = $this->_validatePersonAndAccompanyingPeriod($person); + $this->logger->info(sprintf('Person created with %d errors ', count($errors))); + + if ($errors->count() > 0) { + $this->logger->info('The created person has errors'); + $flashBag = $this->get('session')->getFlashBag(); + $translator = $this->get('translator'); + + $flashBag->add('error', $translator->trans('The person data are not valid')); + + foreach($errors as $error) { + $flashBag->add('info', $error->getMessage()); + } + + $form = $this->createForm( + CreationPersonType::NAME, + $person, + array( + 'action' => $this->generateUrl('chill_person_review'), + 'form_status' => CreationPersonType::FORM_NOT_REVIEWED + )); + + $form->handleRequest($request); + + return $this->_renderNewForm($form); + } else { + $this->logger->info('Person created without errors'); + } + + $alternatePersons = $this->similarPersonMatcher + ->matchPerson($person); + + if (count($alternatePersons) === 0) { + return $this->forward('ChillPersonBundle:Person:create'); + } + + $this->get('session')->getFlashBag()->add('info', + $this->get('translator')->trans( + '%nb% person with similar name. Please verify that this is a new person', + array('%nb%' => count($alternatePersons))) + ); + + return $this->render('ChillPersonBundle:Person:create_review.html.twig', + array( + 'person' => $person, + 'alternatePersons' => $alternatePersons, + 'firstName' => $form['firstName']->getData(), + 'lastName' => $form['lastName']->getData(), + 'birthdate' => $form['birthdate']->getData(), + 'gender' => $form['gender']->getData(), + 'creation_date' => $form['creation_date']->getData(), + 'form' => $form->createView())); + } + + public function createAction(Request $request) + { + + if ($request->getMethod() !== 'POST') { + $r = new Response('You must send something to create a person !'); + $r->setStatusCode(400); + return $r; + } + + $form = $this->createForm(CreationPersonType::class, null, array( + 'form_status' => CreationPersonType::FORM_REVIEWED + )); + + $form->handleRequest($request); + + $person = $this->_bindCreationForm($form); + + $errors = $this->_validatePersonAndAccompanyingPeriod($person); + + $this->denyAccessUnlessGranted('CHILL_PERSON_CREATE', $person, + 'You are not allowed to create this person'); + + if ($errors->count() === 0) { + $em = $this->getDoctrine()->getManager(); + + $em->persist($person); + + $em->flush(); + + return $this->redirect($this->generateUrl('chill_person_general_edit', + array('person_id' => $person->getId()))); + } else { + $text = "this should not happen if you reviewed your submission\n"; + foreach ($errors as $error) { + $text .= $error->getMessage()."\n"; + } + $r = new Response($text); + $r->setStatusCode(400); + return $r; + } + } + + /** + * easy getting a person by his id + * @return \Chill\PersonBundle\Entity\Person + */ + private function _getPerson($id) + { + $person = $this->personRepository->find($id); + + return $person; + } +} diff --git a/src/Bundle/ChillPerson/Controller/TimelinePersonController.php b/src/Bundle/ChillPerson/Controller/TimelinePersonController.php new file mode 100644 index 000000000..774351581 --- /dev/null +++ b/src/Bundle/ChillPerson/Controller/TimelinePersonController.php @@ -0,0 +1,109 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Controller; + +use Chill\PersonBundle\Privacy\PrivacyEvent; +use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\Request; +use Chill\MainBundle\Timeline\TimelineBuilder; +use Chill\MainBundle\Pagination\PaginatorFactory; +use Chill\PersonBundle\Security\Authorization\PersonVoter; + +/** + * Class TimelinePersonController + * + * @package Chill\PersonBundle\Controller + * @author Julien Fastré + */ +class TimelinePersonController extends AbstractController +{ + + /** + * @var EventDispatcherInterface + */ + protected $eventDispatcher; + + /** + * + * @var TimelineBuilder + */ + protected $timelineBuilder; + + /** + * + * @var PaginatorFactory + */ + protected $paginatorFactory; + + /** + * TimelinePersonController constructor. + * + * @param EventDispatcherInterface $eventDispatcher + */ + public function __construct( + EventDispatcherInterface $eventDispatcher, + TimelineBuilder $timelineBuilder, + PaginatorFactory $paginatorFactory + ) { + $this->eventDispatcher = $eventDispatcher; + $this->timelineBuilder = $timelineBuilder; + $this->paginatorFactory = $paginatorFactory; + } + + + public function personAction(Request $request, $person_id) + { + $person = $this->getDoctrine() + ->getRepository('ChillPersonBundle:Person') + ->find($person_id); + + if ($person === NULL) { + throw $this->createNotFoundException(); + } + + $this->denyAccessUnlessGranted(PersonVoter::SEE, $person); + + $nbItems = $this->timelineBuilder->countItems('person', + [ 'person' => $person ] + ); + + $paginator = $this->paginatorFactory->create($nbItems); + + $event = new PrivacyEvent($person, array('action' => 'timeline')); + $this->eventDispatcher->dispatch(PrivacyEvent::PERSON_PRIVACY_EVENT, $event); + + return $this->render('ChillPersonBundle:Timeline:index.html.twig', array + ( + 'timeline' => $this->timelineBuilder->getTimelineHTML( + 'person', + array('person' => $person), + $paginator->getCurrentPage()->getFirstItemNumber(), + $paginator->getItemsPerPage() + ), + 'person' => $person, + 'nb_items' => $nbItems, + 'paginator' => $paginator + ) + ); + } + +} diff --git a/src/Bundle/ChillPerson/DataFixtures/ORM/LoadAccompanyingPeriodClosingMotive.php b/src/Bundle/ChillPerson/DataFixtures/ORM/LoadAccompanyingPeriodClosingMotive.php new file mode 100644 index 000000000..fad0596a6 --- /dev/null +++ b/src/Bundle/ChillPerson/DataFixtures/ORM/LoadAccompanyingPeriodClosingMotive.php @@ -0,0 +1,89 @@ +, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Persistence\ObjectManager; +use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive; + +/** + * Load closing motives into database + * + * @author Julien Fastré + */ +class LoadAccompanyingPeriodClosingMotive extends AbstractFixture + implements OrderedFixtureInterface +{ + + public function getOrder() { + return 9500; + } + + public static $closingMotives = array( + 'nothing_to_do' => array( + 'name' => array( + 'fr' => 'Plus rien à faire', + 'en' => 'Nothing to do', + 'nl' => 'nieks meer te doen' + ) + ), + 'did_not_come_back' => array( + 'name' => array( + 'fr' => "N'est plus revenu", + 'en' => "Did'nt come back", + 'nl' => "Niet teruggekomen" + ) + ), + 'no_more_money' => array( + 'active' => false, + 'name' => array( + 'fr' => "Plus d'argent", + 'en' => "No more money", + 'nl' => "Geen geld" + ) + ) + ); + + public static $references = array(); + + public function load(ObjectManager $manager) + { + foreach (static::$closingMotives as $ref => $new) { + $motive = new ClosingMotive(); + $motive->setName($new['name']) + ->setActive((isset($new['active']) ? $new['active'] : true)) + ; + + $manager->persist($motive); + $this->addReference($ref, $motive); + echo "Adding ClosingMotive $ref\n"; + } + + $manager->flush(); + } + + + + +} diff --git a/src/Bundle/ChillPerson/DataFixtures/ORM/LoadCustomFields.php b/src/Bundle/ChillPerson/DataFixtures/ORM/LoadCustomFields.php new file mode 100644 index 000000000..235cadd95 --- /dev/null +++ b/src/Bundle/ChillPerson/DataFixtures/ORM/LoadCustomFields.php @@ -0,0 +1,229 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\DataFixtures\ORM; + +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Persistence\ObjectManager; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Chill\CustomFieldsBundle\Entity\CustomField; +use Chill\CustomFieldsBundle\Entity\CustomFieldsGroup; +use Chill\CustomFieldsBundle\CustomFields\CustomFieldTitle; +use Chill\CustomFieldsBundle\CustomFields\CustomFieldText; +use Chill\CustomFieldsBundle\CustomFields\CustomFieldChoice; +use Chill\CustomFieldsBundle\Entity\CustomFieldsDefaultGroup; +use Chill\PersonBundle\Entity\Person; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * + * + * @author Julien Fastré + */ +class LoadCustomFields extends AbstractFixture implements OrderedFixtureInterface, + ContainerAwareInterface +{ + /** + * + * @var ContainerInterface + */ + private $container; + + /** + * + * @var CustomField + */ + private $customFieldText; + + /** + * + * @var CustomField + */ + private $customFieldChoice; + + /** + * @var TranslatableStringHelper + */ + private $translatableStringHelper; + + /** + * @var TranslatorInterface + */ + private $translator; + + /** + * LoadCustomFields constructor. + * + * @param TranslatableStringHelper $translatableStringHelper + * @param TranslatorInterface $translator + */ + public function __construct( + TranslatableStringHelper $translatableStringHelper, + TranslatorInterface $translator + ) { + $this->translatableStringHelper = $translatableStringHelper; + $this->translator = $translator; + } + + //put your code here + public function getOrder() + { + return 10003; + } + + public function setContainer(ContainerInterface $container = null) + { + if ($container === null) { + throw new \RuntimeException("The given container should not be null"); + } + + $this->container = $container; + } + + public function load(ObjectManager $manager) + { + $this->loadFields($manager); + $this->loadData($manager); + $manager->flush(); + } + + private function loadData(ObjectManager $manager) + { + $personIds = $this->container->get('doctrine.orm.entity_manager') + ->createQuery("SELECT person.id FROM ChillPersonBundle:Person person") + ->getScalarResult(); + + // get possible values for cfGroup + $choices = array_map( + function($a) { return $a["slug"]; }, + $this->customFieldChoice->getOptions()["choices"] + ); + // create faker + $faker = \Faker\Factory::create('fr_FR'); + // select a set of people and add data + foreach ($personIds as $id) { + // add info on 1 person on 2 + if (rand(0,1) === 1) { + /* @var $person Person */ + $person = $manager->getRepository(Person::class)->find($id); + $person->setCFData(array( + "remarques" => $this->createCustomFieldText() + ->serialize($faker->text(rand(150, 250)), $this->customFieldText), + "document-d-identite" => $this->createCustomFieldChoice() + ->serialize(array($choices[array_rand($choices)]), $this->customFieldChoice) + )); + } + } + } + + private function createCustomFieldText() + { + return new CustomFieldText( + $this->container->get('request_stack'), + $this->container->get('templating'), + $this->translatableStringHelper + ); + } + + private function createCustomFieldChoice() + { + return new CustomFieldChoice( + $this->translator, + $this->container->get('templating'), + $this->translatableStringHelper + ); + } + + private function loadFields(ObjectManager $manager) + { + $cfGroup = (new CustomFieldsGroup()) + ->setEntity(Person::class) + ->setName(array("fr" => "Données")) + ; + $manager->persist($cfGroup); + + // make this group default for Person::class + $manager->persist( + (new CustomFieldsDefaultGroup()) + ->setCustomFieldsGroup($cfGroup) + ->setEntity(Person::class) + ); + + // create title field + $customField0 = (new CustomField()) + ->setActive(true) + ->setName(array("fr" => "Données personnalisées")) + ->setSlug("personal-data") + ->setOrdering(10) + ->setType('title') + ->setOptions(array(CustomFieldTitle::TYPE => CustomFieldTitle::TYPE_TITLE)) + ->setCustomFieldsGroup($cfGroup) + ; + $manager->persist($customField0); + + // create text field + $this->customFieldText = (new CustomField()) + ->setActive(true) + ->setName(array("fr" => "Remarques")) + ->setSlug("remarques") + ->setOrdering(20) + ->setType('text') + ->setOptions(array('maxLength' => 5000)) + ->setCustomFieldsGroup($cfGroup) + ; + $manager->persist($this->customFieldText); + + // create choice field + $this->customFieldChoice = (new CustomField()) + ->setActive(true) + ->setName(array("fr" => "Document d'identité")) + ->setSlug("document-d-identite") + ->setOrdering(30) + ->setType('choice') + ->setCustomFieldsGroup($cfGroup) + ->setOptions(array( + "multiple" => true, + "other" => false, + "expanded" => true, + "active" => true, + "slug" => "document-d-identite", + "choices" => array( + array( + "name" => array("fr" => "Carte d'identité"), + "active" => true, + "slug" => "carte-d-identite" + ), + array( + "name" => array("fr" => "Passeport"), + "active" => true, + "slug" => "passeport" + ), + array( + "name" => array("fr" => "Titre de séjour"), + "active" => true, + "slug" => "passeport" + ) + ) + )) + ; + $manager->persist($this->customFieldChoice); + } + +} diff --git a/src/Bundle/ChillPerson/DataFixtures/ORM/LoadMaritalStatus.php b/src/Bundle/ChillPerson/DataFixtures/ORM/LoadMaritalStatus.php new file mode 100644 index 000000000..b0cf57637 --- /dev/null +++ b/src/Bundle/ChillPerson/DataFixtures/ORM/LoadMaritalStatus.php @@ -0,0 +1,66 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Persistence\ObjectManager; +use Chill\PersonBundle\Entity\MaritalStatus; + +/** + * Load marital status into database + * + * @author Marc Ducobu + */ +class LoadMaritalStatus extends AbstractFixture implements OrderedFixtureInterface +{ + private $maritalStatuses = [ + ['id' => 'single', 'name' =>['en' => 'single', 'fr' => 'célibataire']], + ['id' => 'married', 'name' =>['en' => 'married', 'fr' => 'marié(e)']], + ['id' => 'widow', 'name' =>['en' => 'widow', 'fr' => 'veuf – veuve ']], + ['id' => 'separat', 'name' =>['en' => 'separated', 'fr' => 'séparé(e)']], + ['id' => 'divorce', 'name' =>['en' => 'divorced', 'fr' => 'divorcé(e)']], + ['id' => 'legalco', 'name' =>['en' => 'legal cohabitant', 'fr' => 'cohabitant(e) légal(e)']], + ['id' => 'unknown', 'name' =>['en' => 'unknown', 'fr' => 'indéterminé']] + ]; + + public function getOrder() + { + return 9999; + } + + public function load(ObjectManager $manager) + { + echo "loading maritalStatuses... \n"; + + foreach ($this->maritalStatuses as $ms) { + echo $ms['name']['en'].' '; + $new_ms = new MaritalStatus(); + $new_ms->setId($ms['id']); + $new_ms->setName($ms['name']); + $this->addReference('ms_'.$ms['id'], $new_ms); + $manager->persist($new_ms); + } + + $manager->flush(); + } +} diff --git a/src/Bundle/ChillPerson/DataFixtures/ORM/LoadPeople.php b/src/Bundle/ChillPerson/DataFixtures/ORM/LoadPeople.php new file mode 100644 index 000000000..0977eef2c --- /dev/null +++ b/src/Bundle/ChillPerson/DataFixtures/ORM/LoadPeople.php @@ -0,0 +1,326 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Persistence\ObjectManager; +use Chill\PersonBundle\Entity\Person; +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Chill\MainBundle\DataFixtures\ORM\LoadPostalCodes; +use Chill\MainBundle\Entity\Address; + +/** + * Load people into database + * + * @author Julien Fastré + * @author Marc Ducobu + */ +class LoadPeople extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface +{ + + use \Symfony\Component\DependencyInjection\ContainerAwareTrait; + + protected $faker; + + public function __construct() + { + $this->faker = \Faker\Factory::create('fr_FR'); + } + + public function prepare() + { + //prepare days, month, years + $y = 1950; + do { + $this->years[] = $y; + $y = $y +1; + } while ($y >= 1990); + + $m = 1; + do { + $this->month[] = $m; + $m = $m +1; + } while ($m >= 12); + + $d = 1; + do { + $this->day[] = $d; + $d = $d + 1; + } while ($d <= 28); + } + + public function getOrder() + { + return 10000; + } + + public function load(ObjectManager $manager) + { + $this->loadRandPeople($manager); + $this->loadExpectedPeople($manager); + + $manager->flush(); + } + + public function loadExpectedPeople(ObjectManager $manager) + { + echo "loading expected people...\n"; + + foreach ($this->peoples as $person) { + $this->addAPerson($this->fillWithDefault($person), $manager); + } + } + + public function loadRandPeople(ObjectManager $manager) + { + echo "loading rand people...\n"; + + $this->prepare(); + + $chooseLastNameOrTri = array('tri', 'tri', 'name', 'tri'); + + $i = 0; + + do { + $i++; + + $sex = $this->genders[array_rand($this->genders)]; + + if ($chooseLastNameOrTri[array_rand($chooseLastNameOrTri)] === 'tri' ) { + $length = rand(2, 3); + $lastName = ''; + for ($j = 0; $j <= $length; $j++) { + $lastName .= $this->lastNamesTrigrams[array_rand($this->lastNamesTrigrams)]; + } + $lastName = ucfirst($lastName); + } else { + $lastName = $this->lastNames[array_rand($this->lastNames)]; + } + + if ($sex === Person::MALE_GENDER) { + $firstName = $this->firstNamesMale[array_rand($this->firstNamesMale)]; + } else { + $firstName = $this->firstNamesFemale[array_rand($this->firstNamesFemale)]; + } + + // add an address on 80% of the created people + if (rand(0,100) < 80) { + $address = $this->getRandomAddress(); + // on 30% of those person, add multiple addresses + if (rand(0,10) < 4) { + $address = array( + $address, + $this->getRandomAddress() + ); + } + } else { + $address = null; + } + + $person = array( + 'FirstName' => $firstName, + 'LastName' => $lastName, + 'Gender' => $sex, + 'Nationality' => (rand(0,100) > 50) ? NULL: 'BE', + 'center' => (rand(0,1) == 0) ? 'centerA': 'centerB', + 'Address' => $address, + 'maritalStatus' => $this->maritalStatusRef[array_rand($this->maritalStatusRef)] + ); + + $this->addAPerson($this->fillWithDefault($person), $manager); + + } while ($i <= 100); + } + + /** + * fill a person array with default value + * + * @param string[] $specific + */ + private function fillWithDefault(array $specific) + { + return array_merge(array( + 'Birthdate' => "1960-10-12", + 'PlaceOfBirth' => "Ottignies Louvain-La-Neuve", + 'Gender' => Person::MALE_GENDER, + 'Email' => "Email d'un ami: roger@tt.com", + 'CountryOfBirth' => 'BE', + 'Nationality' => 'BE', + 'CFData' => array(), + 'Address' => null + ), $specific); + } + + /** + * create a new person from array data + * + * @param array $person + * @param ObjectManager $manager + * @throws \Exception + */ + private function addAPerson(array $person, ObjectManager $manager) + { + $p = new Person(); + + foreach ($person as $key => $value) { + switch ($key) { + case 'CountryOfBirth': + case 'Nationality': + $value = $this->getCountry($value); + break; + case 'Birthdate': + $value = new \DateTime($value); + break; + case 'center': + case 'maritalStatus': + $value = $this->getReference($value); + break; + } + + //try to add the data using the setSomething function, + // if not possible, fallback to addSomething function + if (method_exists($p, 'set'.$key)) { + call_user_func(array($p, 'set'.$key), $value); + } elseif (method_exists($p, 'add'.$key)) { + // if we have a "addSomething", we may have multiple items to add + // so, we set the value in an array if it is not an array, and + // will call the function addSomething multiple times + if (!is_array($value)) { + $value = array($value); + } + + foreach($value as $v) { + if ($v !== NULL) { + call_user_func(array($p, 'add'.$key), $v); + } + } + + } + } + + $manager->persist($p); + echo "add person'".$p->__toString()."'\n"; + } + + /** + * Creata a random address + * + * @return Address + */ + private function getRandomAddress() + { + return (new Address()) + ->setStreetAddress1($this->faker->streetAddress) + ->setStreetAddress2( + rand(0,9) > 5 ? $this->faker->streetAddress : '' + ) + ->setPostcode($this->getReference( + LoadPostalCodes::$refs[array_rand(LoadPostalCodes::$refs)] + )) + ->setValidFrom($this->faker->dateTimeBetween('-5 years')) + ; + } + + private function getCountry($countryCode) + { + if ($countryCode === NULL) { + return NULL; + } + return $this->container->get('doctrine.orm.entity_manager') + ->getRepository('ChillMainBundle:Country') + ->findOneByCountryCode($countryCode); + } + + private $maritalStatusRef = ['ms_single', 'ms_married', 'ms_widow', 'ms_separat', + 'ms_divorce', 'ms_legalco', 'ms_unknown']; + + private $firstNamesMale = array("Jean", "Mohamed", "Alfred", "Robert", + "Compère", "Jean-de-Dieu", + "Charles", "Pierre", "Luc", "Mathieu", "Alain", "Etienne", "Eric", + "Corentin", "Gaston", "Spirou", "Fantasio", "Mahmadou", "Mohamidou", + "Vursuv" ); + private $firstNamesFemale = array("Svedana", "Sevlatina","Irène", "Marcelle", + "Corentine", "Alfonsine","Caroline","Solange","Gostine", "Fatoumata", + "Groseille", "Chana", "Oxana", "Ivana"); + + private $lastNames = array("Diallo", "Bah", "Gaillot"); + private $lastNamesTrigrams = array("fas", "tré", "hu", 'blart', 'van', 'der', 'lin', 'den', + 'ta', 'mi', 'gna', 'bol', 'sac', 'ré', 'jo', 'du', 'pont', 'cas', 'tor', 'rob', 'al', + 'ma', 'gone', 'car',"fu", "ka", "lot", "no", "va", "du", "bu", "su", + "lo", 'to', "cho", "car", 'mo','zu', 'qi', 'mu'); + + private $genders = array(Person::MALE_GENDER, Person::FEMALE_GENDER); + + private $years = array(); + + private $month = array(); + + private $day = array(); + + private $peoples = array( + array( + 'FirstName' => "Depardieu", + 'LastName' => "Gérard", + 'Birthdate' => "1948-12-27", + 'PlaceOfBirth' => "Châteauroux", + 'Gender' => Person::MALE_GENDER, + 'CountryOfBirth' => 'FR', + 'Nationality' => 'RU', + 'center' => 'centerA', + 'maritalStatus' => 'ms_divorce' + ), + array( + //to have a person with same firstname as Gérard Depardieu + 'FirstName' => "Depardieu", + 'LastName' => "Jean", + 'Birthdate' => "1960-10-12", + 'CountryOfBirth' => 'FR', + 'Nationality' => 'FR', + 'center' => 'centerA', + 'maritalStatus' => 'ms_divorce' + ), + array( + //to have a person with same birthdate of Gérard Depardieu + 'FirstName' => 'Van Snick', + 'LastName' => 'Bart', + 'Birthdate' => '1948-12-27', + 'center' => 'centerA', + 'maritalStatus' => 'ms_legalco' + ), + array( + //to have a woman with Depardieu as FirstName + 'FirstName' => 'Depardieu', + 'LastName' => 'Charline', + 'Gender' => Person::FEMALE_GENDER, + 'center' => 'centerA', + 'maritalStatus' => 'ms_legalco' + ), + array( + //to have a special character in lastName + 'FirstName' => 'Manço', + 'LastName' => 'Étienne', + 'center' => 'centerA', + 'maritalStatus' => 'ms_unknown' + ) + ); +} diff --git a/src/Bundle/ChillPerson/DataFixtures/ORM/LoadPersonACL.php b/src/Bundle/ChillPerson/DataFixtures/ORM/LoadPersonACL.php new file mode 100644 index 000000000..0e3cb6dc9 --- /dev/null +++ b/src/Bundle/ChillPerson/DataFixtures/ORM/LoadPersonACL.php @@ -0,0 +1,87 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\DataFixtures\ORM; + +use Doctrine\Common\DataFixtures\AbstractFixture; +use Doctrine\Common\DataFixtures\OrderedFixtureInterface; +use Doctrine\Persistence\ObjectManager; +use Chill\MainBundle\DataFixtures\ORM\LoadPermissionsGroup; +use Chill\MainBundle\Entity\RoleScope; +use Chill\PersonBundle\Security\Authorization\PersonVoter; + +/** + * Add a role CHILL_PERSON_UPDATE & CHILL_PERSON_CREATE for all groups except administrative, + * and a role CHILL_PERSON_SEE for administrative + * + * @author Julien Fastré + */ +class LoadPersonACL extends AbstractFixture implements OrderedFixtureInterface +{ + public function getOrder() + { + return 9600; + } + + + public function load(ObjectManager $manager) + { + foreach (LoadPermissionsGroup::$refs as $permissionsGroupRef) { + $permissionsGroup = $this->getReference($permissionsGroupRef); + + //create permission group + switch ($permissionsGroup->getName()) { + case 'social': + case 'direction': + printf("Adding CHILL_PERSON_UPDATE & CHILL_PERSON_CREATE to %s permission group \n", $permissionsGroup->getName()); + $roleScopeUpdate = (new RoleScope()) + ->setRole('CHILL_PERSON_UPDATE') + ->setScope(null); + $permissionsGroup->addRoleScope($roleScopeUpdate); + $roleScopeCreate = (new RoleScope()) + ->setRole('CHILL_PERSON_CREATE') + ->setScope(null); + $permissionsGroup->addRoleScope($roleScopeCreate); + $roleScopeList = (new RoleScope()) + ->setRole(PersonVoter::LISTS) + ->setScope(null); + $permissionsGroup->addRoleScope($roleScopeList); + $roleScopeStats = (new RoleScope()) + ->setRole(PersonVoter::STATS) + ->setScope(null); + $permissionsGroup->addRoleScope($roleScopeStats); + $manager->persist($roleScopeUpdate); + $manager->persist($roleScopeCreate); + break; + case 'administrative': + printf("Adding CHILL_PERSON_SEE to %s permission group \n", $permissionsGroup->getName()); + $roleScopeSee = (new RoleScope()) + ->setRole('CHILL_PERSON_SEE') + ->setScope(null); + $permissionsGroup->addRoleScope($roleScopeSee); + $manager->persist($roleScopeSee); + break; + } + + } + + $manager->flush(); + } + +} diff --git a/src/Bundle/ChillPerson/DependencyInjection/ChillPersonExtension.php b/src/Bundle/ChillPerson/DependencyInjection/ChillPersonExtension.php new file mode 100644 index 000000000..c1ea40486 --- /dev/null +++ b/src/Bundle/ChillPerson/DependencyInjection/ChillPersonExtension.php @@ -0,0 +1,310 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\DependencyInjection; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\DependencyInjection\Loader; +use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; +use Chill\MainBundle\DependencyInjection\MissingBundleException; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\MainBundle\Security\Authorization\ChillExportVoter; +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * Class ChillPersonExtension + * Loads and manages your bundle configuration + * + * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html} + * @package Chill\PersonBundle\DependencyInjection + */ +class ChillPersonExtension extends Extension implements PrependExtensionInterface +{ + + /** + * {@inheritDoc} + * @param array $configs + * @param ContainerBuilder $container + * @throws \Exception + */ + public function load(array $configs, ContainerBuilder $container) + { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + // set configuration for validation + $container->setParameter('chill_person.validation.birtdate_not_before', + $config['validation']['birthdate_not_after']); + + $this->handlePersonFieldsParameters($container, $config['person_fields']); + $this->handleAccompanyingPeriodsFieldsParameters($container, $config['accompanying_periods_fields']); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../config')); + $loader->load('services.yaml'); + $loader->load('services/widgets.yaml'); + $loader->load('services/exports.yaml'); + $loader->load('services/fixtures.yaml'); + $loader->load('services/controller.yaml'); + $loader->load('services/search.yaml'); + $loader->load('services/menu.yaml'); + $loader->load('services/privacyEvent.yaml'); + $loader->load('services/command.yaml'); + $loader->load('services/actions.yaml'); + $loader->load('services/form.yaml'); + $loader->load('services/repository.yaml'); + $loader->load('services/templating.yaml'); + $loader->load('services/alt_names.yaml'); + + // load service advanced search only if configure + if ($config['search']['search_by_phone'] != 'never') { + $loader->load('services/search_by_phone.yaml'); + $container->setParameter('chill_person.search.search_by_phone', + $config['search']['search_by_phone']); + } + + if ($container->getParameter('chill_person.accompanying_period') !== 'hidden') { + $loader->load('services/exports_accompanying_period.yaml'); + } + } + + /** + * @param ContainerBuilder $container + * @param $config + */ + private function handlePersonFieldsParameters(ContainerBuilder $container, $config) + { + if (array_key_exists('enabled', $config)) { + unset($config['enabled']); + } + + $container->setParameter('chill_person.person_fields', $config); + + foreach ($config as $key => $value) { + switch($key) { + case 'accompanying_period': + $container->setParameter('chill_person.accompanying_period', $value); + break; + default: + $container->setParameter('chill_person.person_fields.'.$key, $value); + break; + } + } + } + + /** + * @param ContainerBuilder $container + * @param $config + */ + private function handleAccompanyingPeriodsFieldsParameters(ContainerBuilder $container, $config) + { + $container->setParameter('chill_person.accompanying_period_fields', $config); + + foreach ($config as $key => $value) { + switch($key) { + case 'enabled': + break; + default: + $container->setParameter('chill_person.accompanying_period_fields.'.$key, $value); + break; + } + } + } + + /** + * @param ContainerBuilder $container + * @throws MissingBundleException + */ + private function declarePersonAsCustomizable (ContainerBuilder $container) + { + $bundles = $container->getParameter('kernel.bundles'); + if (!isset($bundles['ChillCustomFieldsBundle'])) { + throw new MissingBundleException('ChillCustomFieldsBundle'); + } + + $container->prependExtensionConfig('chill_custom_fields', + array('customizables_entities' => + array( + array('class' => 'Chill\PersonBundle\Entity\Person', 'name' => 'PersonEntity') + ) + ) + ); + } + + /** + * @param ContainerBuilder $container + * @throws MissingBundleException + */ + public function prepend(ContainerBuilder $container) + { + $this->prependRoleHierarchy($container); + $this->prependHomepageWidget($container); + $this->prependDoctrineDQL($container); + $this->prependCruds($container); + + //add person_fields parameter as global + $chillPersonConfig = $container->getExtensionConfig($this->getAlias()); + $config = $this->processConfiguration(new Configuration(), $chillPersonConfig); + $twigConfig = array( + 'globals' => array( + 'chill_person' => array( + 'fields' => $config['person_fields'] + ), + 'chill_accompanying_periods' => [ + 'fields' => $config['accompanying_periods_fields'] + ] + ), + 'form_themes' => array('ChillPersonBundle:Export:ListPersonFormFields.html.twig') + ); + $container->prependExtensionConfig('twig', $twigConfig); + + $this-> declarePersonAsCustomizable($container); + + //declare routes for person bundle + $container->prependExtensionConfig('chill_main', array( + 'routing' => array( + 'resources' => array( + '@ChillPersonBundle/config/routes.yaml' + ) + ) + )); + } + + /** + * Add a widget "add a person" on the homepage, automatically + * + * @param \Chill\PersonBundle\DependencyInjection\containerBuilder $container + */ + protected function prependHomepageWidget(containerBuilder $container) + { + $container->prependExtensionConfig('chill_main', array( + 'widgets' => array( + 'homepage' => array( + array( + 'widget_alias' => 'add_person', + 'order' => 2 + ) + ) + ) + )); + } + + /** + * Add role hierarchy. + * + * @param ContainerBuilder $container + */ + protected function prependRoleHierarchy(ContainerBuilder $container) + { + $container->prependExtensionConfig('security', array( + 'role_hierarchy' => array( + 'CHILL_PERSON_UPDATE' => array('CHILL_PERSON_SEE'), + 'CHILL_PERSON_CREATE' => array('CHILL_PERSON_SEE'), + PersonVoter::LISTS => [ ChillExportVoter::EXPORT ], + PersonVoter::STATS => [ ChillExportVoter::EXPORT ] + ) + )); + } + + /** + * Add DQL function linked with person + * + * @param ContainerBuilder $container + */ + protected function prependDoctrineDQL(ContainerBuilder $container) + { + //add DQL function to ORM (default entity_manager) + + $container->prependExtensionConfig('doctrine', array( + 'orm' => array( + 'dql' => array( + 'string_functions' => array( + 'GET_PERSON_ADDRESS_ADDRESS_ID' => AddressPart\AddressPartAddressId::class, + 'GET_PERSON_ADDRESS_STREET_ADDRESS_1' => AddressPart\AddressPartStreetAddress1::class, + 'GET_PERSON_ADDRESS_STREET_ADDRESS_2' => AddressPart\AddressPartStreetAddress2::class, + 'GET_PERSON_ADDRESS_VALID_FROM' => AddressPart\AddressPartValidFrom::class, + 'GET_PERSON_ADDRESS_POSTCODE_LABEL' => AddressPart\AddressPartPostCodeLabel::class, + 'GET_PERSON_ADDRESS_POSTCODE_CODE' => AddressPart\AddressPartPostCodeCode::class, + 'GET_PERSON_ADDRESS_POSTCODE_ID' => AddressPart\AddressPartPostCodeId::class, + 'GET_PERSON_ADDRESS_COUNTRY_NAME' => AddressPart\AddressPartCountryName::class, + 'GET_PERSON_ADDRESS_COUNTRY_CODE' => AddressPart\AddressPartCountryCode::class, + 'GET_PERSON_ADDRESS_COUNTRY_ID' => AddressPart\AddressPartCountryId::class, + ), + 'numeric_functions' => [ + 'GET_PERSON_ADDRESS_ISNOADDRESS' => AddressPart\AddressPartIsNoAddress::class, + ] + ) + ) + )); + } + + /** + * @param ContainerBuilder $container + */ + protected function prependCruds(ContainerBuilder $container) + { + $container->prependExtensionConfig('chill_main', [ + 'cruds' => [ + [ + 'class' => \Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive::class, + 'name' => 'closing_motive', + 'base_path' => '/admin/closing-motive', + 'form_class' => \Chill\PersonBundle\Form\ClosingMotiveType::class, + 'controller' => \Chill\PersonBundle\Controller\AdminClosingMotiveController::class, + 'actions' => [ + 'index' => [ + 'template' => '@ChillPerson/ClosingMotive/index.html.twig', + 'role' => 'ROLE_ADMIN' + ], + 'new' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillPerson/ClosingMotive/new.html.twig', + ], + 'edit' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillPerson/ClosingMotive/edit.html.twig', + ] + ] + ], + [ + 'class' => \Chill\PersonBundle\Entity\MaritalStatus::class, + 'name' => 'marital_status', + 'base_path' => '/admin/marital-status', + 'form_class' => \Chill\PersonBundle\Form\MaritalStatusType::class, + 'controller' => \Chill\PersonBundle\Controller\AdminMaritalStatusController::class, + 'actions' => [ + 'index' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillPerson/MaritalStatus/index.html.twig', + ], + 'new' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillPerson/MaritalStatus/new.html.twig', + ], + 'edit' => [ + 'role' => 'ROLE_ADMIN', + 'template' => '@ChillPerson/MaritalStatus/edit.html.twig', + ] + ] + ] + ] + ]); + } +} diff --git a/src/Bundle/ChillPerson/DependencyInjection/CompilerPass/AccompanyingPeriodTimelineCompilerPass.php b/src/Bundle/ChillPerson/DependencyInjection/CompilerPass/AccompanyingPeriodTimelineCompilerPass.php new file mode 100644 index 000000000..1ee7782d3 --- /dev/null +++ b/src/Bundle/ChillPerson/DependencyInjection/CompilerPass/AccompanyingPeriodTimelineCompilerPass.php @@ -0,0 +1,66 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\DependencyInjection\CompilerPass; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; + +/** + * Remove services which add AccompanyingPeriod to timeline if + * accompanying_periods are set to `hidden` + * + */ +class AccompanyingPeriodTimelineCompilerPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + // remove services when accompanying period are hidden + if ($container->getParameter('chill_person.accompanying_period') !== 'hidden') { + return; + } + + $definitions = [ + 'chill.person.timeline.accompanying_period_opening', + 'chill.person.timeline.accompanying_period_closing' + ]; + + foreach($definitions as $definition) { + $container + ->removeDefinition($definition) + ; + } + + $definition = $container->getDefinition('chill.main.timeline_builder'); + + // we have to remove all methods call, and re-add them if not linked + // to this service + $calls = $definition->getMethodCalls(); + + foreach($calls as list($method, $arguments)) { + if ($method !== 'addProvider') { + continue; + } + + $definition->removeMethodCall('addProvider'); + + if (FALSE === \in_array($arguments[1], $definitions)) { + $definition->addMethodCall($method, $arguments); + } + } + } +} diff --git a/src/Bundle/ChillPerson/DependencyInjection/Configuration.php b/src/Bundle/ChillPerson/DependencyInjection/Configuration.php new file mode 100644 index 000000000..9784e8523 --- /dev/null +++ b/src/Bundle/ChillPerson/DependencyInjection/Configuration.php @@ -0,0 +1,138 @@ +getRootNode('cl_chill_person'); + + $rootNode + ->canBeDisabled() + ->children() + ->arrayNode('search') + ->canBeDisabled() + ->children() + ->enumNode('search_by_phone') + ->values(['always', 'on-domain', 'never']) + ->defaultValue('on-domain') + ->info('enable search by phone. \'always\' show the result ' + . 'on every result. \'on-domain\' will show the result ' + . 'only if the domain is given in the search box. ' + . '\'never\' disable this feature') + ->end() + ->end() //children for 'search', parent = array node 'search' + ->end() // array 'search', parent = children of root + ->arrayNode('validation') + ->canBeDisabled() + ->children() + ->scalarNode('birthdate_not_after') + ->info($this->validationBirthdateNotAfterInfos) + ->defaultValue('P1D') + ->validate() + ->ifTrue(function($period) { + try { + $interval = new \DateInterval($period); + } catch (\Exception $ex) { + return true; + } + return false; + }) + ->thenInvalid('Invalid period for birthdate validation : "%s" ' + . 'The parameter should match duration as defined by ISO8601 : ' + . 'https://en.wikipedia.org/wiki/ISO_8601#Durations') + ->end() // birthdate_not_after, parent = children of validation + + ->end() // children for 'validation', parent = validation + ->end() //validation, parent = children of root + ->end() // children of root, parent = root + ->arrayNode('person_fields') + ->canBeDisabled() + ->children() + ->append($this->addFieldNode('place_of_birth')) + ->append($this->addFieldNode('email')) + ->append($this->addFieldNode('phonenumber')) + ->append($this->addFieldNode('mobilenumber')) + ->append($this->addFieldNode('contact_info')) + ->append($this->addFieldNode('nationality')) + ->append($this->addFieldNode('country_of_birth')) + ->append($this->addFieldNode('marital_status')) + ->append($this->addFieldNode('spoken_languages')) + ->append($this->addFieldNode('address')) + ->append($this->addFieldNode('accompanying_period')) + ->append($this->addFieldNode('memo')) + ->arrayNode('alt_names') + ->defaultValue([]) + ->arrayPrototype() + ->children() + ->scalarNode('key') + ->isRequired()->cannotBeEmpty() + ->end() + ->arrayNode('labels') + ->children() + ->scalarNode('lang')->isRequired()->cannotBeEmpty() + ->example('fr') + ->end() + ->scalarNode('label')->isRequired()->cannotBeEmpty() + ->example('Nom de jeune fille') + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() //children for 'person_fields', parent = array 'person_fields' + ->end() // person_fields, parent = children of root + ->arrayNode('accompanying_periods_fields') + ->canBeDisabled() + ->children() + ->append($this->addFieldNode('user')) + ->end() //children for 'accompanying_person_fields', parent = array 'person_fields' + ->end() // paccompanying_person_fields, parent = children of root + ->end() // children of 'root', parent = root + ; + + + return $treeBuilder; + } + + private function addFieldNode($key) + { + $tree = new TreeBuilder($key,'enum'); + $node = $tree->getRootNode($key); + + switch($key) { + case 'accompanying_period': + $info = "If the accompanying periods are shown"; + break; + default: + $info = "If the field $key must be shown"; + break; + } + + $node + ->values(array('hidden', 'visible')) + ->defaultValue('visible') + ->info($info) + ->end(); + + return $node; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart.php new file mode 100644 index 000000000..98e362dfb --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart.php @@ -0,0 +1,103 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL; + +use Doctrine\ORM\Query\AST\Functions\FunctionNode; +use Doctrine\ORM\Query\Lexer; +use Doctrine\ORM\Query\Parser; +use Doctrine\ORM\Query\SqlWalker; + +/** + * + * USAGE GET_ADDRESS_(person.id, :date, 'postcode') where part + * should be replace by the part of the address. + * + * This function return the current address part at the given date, for the + * given person (identified by his id) + * + * The aim of this function is to be used within reports + * + * @author Julien Fastré + */ +abstract class AddressPart extends FunctionNode +{ + public $fields = array( + 'address_id', + 'streetaddress1', + 'streetaddress2', + 'validfrom', + 'postcode_label', + 'postcode_code', + 'postcode_id', + 'country_name', + 'country_code', + 'country_id' + ); + + /** + * + * @var \Doctrine\ORM\Query\AST\Node + */ + private $pid; + + /** + * + * @var \Doctrine\ORM\Query\AST\Node + */ + private $date; + + /** + * + * @var \Doctrine\ORM\Query\AST\Node + */ + private $part; + + /** + * return the part of the address + * + * Should be one value of the "public" amongst + * 'address_id', 'streetaddress1', + * 'streetaddress2', 'validfrom', 'postcode_label', 'postcode_code', + * 'postcode_id', 'country_name', 'country_code', 'country_id', 'isnoaddress' + * + * @return string + */ + abstract public function getPart(); + + public function getSql(SqlWalker $sqlWalker) + { + return sprintf( + 'get_last_address_%s(%s, %s)', + $this->getPart(), + $this->pid->dispatch($sqlWalker), + $this->date->dispatch($sqlWalker) + ); + } + + public function parse(Parser $parser) + { + $a = $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + // person id + $this->pid = $parser->SingleValuedPathExpression(); + $parser->match(Lexer::T_COMMA); + // date + $this->date = $parser->ArithmeticPrimary(); + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartAddressId.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartAddressId.php new file mode 100644 index 000000000..2f40796cf --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartAddressId.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartAddressId extends AddressPart +{ + public function getPart() + { + return 'address_id'; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartCountryCode.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartCountryCode.php new file mode 100644 index 000000000..e997dbb91 --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartCountryCode.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartCountryCode extends AddressPart +{ + public function getPart() + { + return 'country_code'; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartCountryId.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartCountryId.php new file mode 100644 index 000000000..ab0ab0da2 --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartCountryId.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartCountryId extends AddressPart +{ + public function getPart() + { + return 'country_id'; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartCountryName.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartCountryName.php new file mode 100644 index 000000000..c37902909 --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartCountryName.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartCountryName extends AddressPart +{ + public function getPart() + { + return 'country_name'; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartIsNoAddress.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartIsNoAddress.php new file mode 100644 index 000000000..bd08bb4df --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartIsNoAddress.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartIsNoAddress extends AddressPart +{ + public function getPart() + { + return 'isnoaddress'; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartPostCodeCode.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartPostCodeCode.php new file mode 100644 index 000000000..ce792cede --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartPostCodeCode.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartPostCodeCode extends AddressPart +{ + public function getPart() + { + return 'postcode_code'; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartPostCodeId.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartPostCodeId.php new file mode 100644 index 000000000..5400a04a7 --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartPostCodeId.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartPostCodeId extends AddressPart +{ + public function getPart() + { + return 'postcode_id'; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartPostCodeLabel.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartPostCodeLabel.php new file mode 100644 index 000000000..ab36a8c46 --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartPostCodeLabel.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartPostCodeLabel extends AddressPart +{ + public function getPart() + { + return 'postcode_label'; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartStreetAddress1.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartStreetAddress1.php new file mode 100644 index 000000000..c06cf2b07 --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartStreetAddress1.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartStreetAddress1 extends AddressPart +{ + public function getPart() + { + return 'streetaddress1'; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartStreetAddress2.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartStreetAddress2.php new file mode 100644 index 000000000..739120e5e --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartStreetAddress2.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartStreetAddress2 extends AddressPart +{ + public function getPart() + { + return 'streetaddress2'; + } +} diff --git a/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartValidFrom.php b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartValidFrom.php new file mode 100644 index 000000000..4a366bf1a --- /dev/null +++ b/src/Bundle/ChillPerson/Doctrine/DQL/AddressPart/AddressPartValidFrom.php @@ -0,0 +1,33 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Doctrine\DQL\AddressPart; + +use Chill\PersonBundle\Doctrine\DQL\AddressPart; + +/** + * + * + * @author Julien Fastré + */ +class AddressPartValidFrom extends AddressPart +{ + public function getPart() + { + return 'validfrom'; + } +} diff --git a/src/Bundle/ChillPerson/Entity/AccompanyingPeriod.php b/src/Bundle/ChillPerson/Entity/AccompanyingPeriod.php new file mode 100644 index 000000000..255cf056f --- /dev/null +++ b/src/Bundle/ChillPerson/Entity/AccompanyingPeriod.php @@ -0,0 +1,330 @@ +, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Entity; + +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Chill\MainBundle\Entity\User; + +/** + * AccompanyingPeriod + * + * @ORM\Entity() + * @ORM\Table(name="chill_person_accompanying_period") + */ +class AccompanyingPeriod +{ + /** + * @var integer + * + * @ORM\Id + * @ORM\Column(name="id", type="integer") + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var \DateTime + * + * @ORM\Column(type="date") + */ + private $openingDate; + + /** + * @var \DateTime + * + * @ORM\Column(type="date", nullable=true) + */ + private $closingDate = null; + + /** + * @var string + * + * @ORM\Column(type="text") + */ + private $remark = ''; + + /** + * @var Person + * + * @ORM\ManyToOne( + * targetEntity="Chill\PersonBundle\Entity\Person", + * inversedBy="accompanyingPeriods", + * cascade={"refresh"}) + */ + private $person; + + /** + * @var AccompanyingPeriod\ClosingMotive + * + * @ORM\ManyToOne( + * targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive") + * @ORM\JoinColumn(nullable=true) + */ + private $closingMotive = null; + + /** + * The user making the accompanying + * @var User + * + * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\User") + * @ORM\JoinColumn(nullable=true) + */ + private $user; + + + /** + * AccompanyingPeriod constructor. + * + * @param \DateTime $dateOpening + * @uses AccompanyingPeriod::setClosingDate() + */ + public function __construct(\DateTime $dateOpening) { + $this->setOpeningDate($dateOpening); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Set openingDate + * + * @param \DateTime $dateOpening + * @return AccompanyingPeriod + */ + public function setOpeningDate($openingDate) + { + $this->openingDate = $openingDate; + + return $this; + } + + /** + * Get openingDate + * + * @return \DateTime + */ + public function getOpeningDate() + { + return $this->openingDate; + } + + /** + * Set closingDate + * + * For closing a Person file, you should use Person::setClosed instead. + * + * @param \DateTime $dateClosing + * @return AccompanyingPeriod + * + */ + public function setClosingDate($closingDate) + { + $this->closingDate = $closingDate; + + return $this; + } + + /** + * Get closingDate + * + * @return \DateTime + */ + public function getClosingDate() + { + return $this->closingDate; + } + + /** + * @return boolean + */ + public function isOpen(): bool + { + if ($this->getOpeningDate() > new \DateTime('now')) { + return false; + } + + if ($this->getClosingDate() === null) { + return true; + } else { + return false; + } + } + + /** + * Set remark + * + * @param string $remark + * @return AccompanyingPeriod + */ + public function setRemark($remark) + { + if ($remark === null) { + $remark = ''; + } + + $this->remark = $remark; + + return $this; + } + + /** + * Get remark + * + * @return string + */ + public function getRemark() + { + return $this->remark; + } + + /** + * Set person. + * + * For consistency, you should use Person::addAccompanyingPeriod instead. + * + * @param Person $person + * @return AccompanyingPeriod + * @see Person::addAccompanyingPeriod + */ + public function setPerson(Person $person = null) + { + $this->person = $person; + + return $this; + } + + /** + * Get person + * + * @return Person + */ + public function getPerson() + { + return $this->person; + } + + /** + * @return AccompanyingPeriod\ClosingMotive + */ + public function getClosingMotive() + { + return $this->closingMotive; + } + + /** + * @param AccompanyingPeriod\ClosingMotive|null $closingMotive + * @return $this + */ + public function setClosingMotive(AccompanyingPeriod\ClosingMotive $closingMotive = null) + { + $this->closingMotive = $closingMotive; + return $this; + } + + /** + * If the period can be reopened. + * + * This function test if the period is closed and if the period is the last + * for the associated person + * + * @return boolean + */ + public function canBeReOpened() + { + if ($this->isOpen() === true) { + return false; + } + + $periods = $this->getPerson()->getAccompanyingPeriodsOrdered(); + + return end($periods) === $this; + } + + /** + */ + public function reOpen() + { + $this->setClosingDate(null); + $this->setClosingMotive(null); + } + + /** + * Validation function + */ + public function isDateConsistent(ExecutionContextInterface $context) + { + if ($this->isOpen()) { + return; + } + + if (! $this->isClosingAfterOpening()) { + $context->buildViolation('The date of closing is before the date of opening') + ->atPath('dateClosing') + ->addViolation(); + } + } + + /** + * Returns true if the closing date is after the opening date. + * + * @return boolean + */ + public function isClosingAfterOpening() + { + $diff = $this->getOpeningDate()->diff($this->getClosingDate()); + + if ($diff->invert === 0) { + return true; + } else { + return false; + } + } + + /** + * @return User|null + */ + function getUser(): ?User + { + return $this->user; + } + + /** + * @param User $user + * @return AccompanyingPeriod + */ + function setUser(User $user): self + { + $this->user = $user; + + return $this; + } + +} diff --git a/src/Bundle/ChillPerson/Entity/AccompanyingPeriod/ClosingMotive.php b/src/Bundle/ChillPerson/Entity/AccompanyingPeriod/ClosingMotive.php new file mode 100644 index 000000000..05149e23f --- /dev/null +++ b/src/Bundle/ChillPerson/Entity/AccompanyingPeriod/ClosingMotive.php @@ -0,0 +1,276 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Entity\AccompanyingPeriod; + +use Doctrine\ORM\Mapping as ORM; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\ArrayCollection; + +/** + * ClosingMotive give an explanation why we closed the Accompanying period + * + * @ORM\Entity( + * repositoryClass="Chill\PersonBundle\Repository\ClosingMotiveRepository") + * @ORM\Table(name="chill_person_closingmotive") + */ +class ClosingMotive +{ + /** + * @var integer + * + * @ORM\Id + * @ORM\Column(name="id", type="integer") + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @var array + * + * @ORM\Column(type="json_array") + */ + private $name; + + /** + * @var boolean + * + * @ORM\Column(type="boolean") + */ + private $active = true; + + /** + * @var self + * + * @ORM\ManyToOne( + * targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive", + * inversedBy="children") + */ + private $parent = null; + + /** + * Child Accompanying periods + * @var Collection + * + * @ORM\OneToMany( + * targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive", + * mappedBy="parent") + */ + private $children; + + /** + * @var float + * + * @ORM\Column(type="float") + */ + private $ordering = 0.0; + + + /** + * ClosingMotive constructor. + */ + public function __construct() + { + $this->children = new ArrayCollection(); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Set name + * + * @param array $name + * + * @return ClosingMotive + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get name + * + * @return array + */ + public function getName() + { + return $this->name; + } + + /** + * @return bool + */ + public function isActive(): bool + { + return $this->active; + } + + /** + * @param bool $active + * @return $this + */ + public function setActive(bool $active) + { + $this->active = $active; + + if ($this->active === FALSE) { + foreach ($this->getChildren() as $child) { + $child->setActive(FALSE); + } + } + + return $this; + } + + /** + * @return ClosingMotive + */ + public function getParent() + { + return $this->parent; + } + + /** + * @return Collection + */ + public function getChildren(): Collection + { + return $this->children; + } + + /** + * @param ClosingMotive|null $parent + * @return ClosingMotive + */ + public function setParent(?ClosingMotive $parent): ClosingMotive + { + $this->parent = $parent; + + if (NULL !== $parent) { + //$parent->addChildren($this); + } + + return $this; + } + + /** + * @param Collection $children + * @return ClosingMotive + */ + public function setChildren(Collection $children): ClosingMotive + { + $this->children = $children; + + return $this; + } + + /** + * @param ClosingMotive $child + * @return ClosingMotive + */ + public function addChildren(ClosingMotive $child): ClosingMotive + { + if ($this->children->contains($child)) { + return $this; + } + + $this->children->add($child); + $child->setParent($this); + + return $this; + } + + /** + * @param ClosingMotive $child + * @return ClosingMotive + */ + public function removeChildren(ClosingMotive $child): ClosingMotive + { + if ($this->children->removeElement($child)) { + $child->setParent(null); + } + + return $this; + } + + /** + * @return float + */ + public function getOrdering(): float + { + return $this->ordering; + } + + /** + * @param float $ordering + * @return $this + */ + public function setOrdering(float $ordering) + { + $this->ordering = $ordering; + + return $this; + } + + /** + * @return bool + */ + public function isChild(): bool + { + return $this->parent !== null; + } + + /** + * @return bool + */ + public function isParent(): bool + { + return $this->children->count() > 0; + } + + /** + * @return bool + */ + public function isLeaf(): bool + { + return $this->children->count() === 0; + } + + /** + * @return bool + */ + public function hasParent(): bool + { + return $this->parent !== null; + } + +} + diff --git a/src/Bundle/ChillPerson/Entity/HasPerson.php b/src/Bundle/ChillPerson/Entity/HasPerson.php new file mode 100644 index 000000000..e200182bc --- /dev/null +++ b/src/Bundle/ChillPerson/Entity/HasPerson.php @@ -0,0 +1,34 @@ +, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Entity; + +use Chill\PersonBundle\Entity\Person; + +/** + * Interface which applies to entities which are associated to a single person + * + */ +interface HasPerson +{ + public function setPerson(Person $person = null): HasPerson; + + public function getPerson(): ?Person; +} diff --git a/src/Bundle/ChillPerson/Entity/MaritalStatus.php b/src/Bundle/ChillPerson/Entity/MaritalStatus.php new file mode 100644 index 000000000..f1addbb2c --- /dev/null +++ b/src/Bundle/ChillPerson/Entity/MaritalStatus.php @@ -0,0 +1,92 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Entity; + +use Doctrine\ORM\Mapping as ORM; + +/** + * MaritalStatus + * + * @ORM\Entity() + * @ORM\Table(name="chill_person_marital_status") + * @ORM\HasLifecycleCallbacks() + */ +class MaritalStatus +{ + /** + * @var string + * + * @ORM\Id() + * @ORM\Column(type="string", length=7) + */ + private $id; + + /** + * @var string array + * @ORM\Column(type="json_array") + */ + private $name; + + /** + * Get id + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Set id + * + * @param string $id + * @return MaritalStatus + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * Set name + * + * @param string array $name + * @return MaritalStatus + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get name + * + * @return string array + */ + public function getName() + { + return $this->name; + } +} diff --git a/src/Bundle/ChillPerson/Entity/Person.php b/src/Bundle/ChillPerson/Entity/Person.php new file mode 100644 index 000000000..36993ce3b --- /dev/null +++ b/src/Bundle/ChillPerson/Entity/Person.php @@ -0,0 +1,1054 @@ +, + * + * 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 . + */ + +use Doctrine\ORM\Mapping as ORM; +use Doctrine\Common\Collections\Collection; +use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Criteria; +use Chill\MainBundle\Entity\Center; +use Chill\MainBundle\Entity\Country; +use Chill\PersonBundle\Entity\MaritalStatus; +use Chill\MainBundle\Entity\HasCenterInterface; +use Chill\MainBundle\Entity\Address; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * Person Class + * + * @ORM\Entity(repositoryClass="Chill\PersonBundle\Repository\PersonRepository") + * @ORM\Table(name="chill_person_person", + * indexes={@ORM\Index( + * name="person_names", + * columns={"firstName", "lastName"} + * )}) + * sf4 check index name + * @ORM\HasLifecycleCallbacks() + */ +class Person implements HasCenterInterface +{ + /** + * The person's id + * @var integer + * + * @ORM\Id + * @ORM\Column(name="id", type="integer") + * @ORM\GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * The person's first name + * @var string + * + * @ORM\Column(type="string", length=255) + */ + private $firstName; + + /** + * The person's last name + * @var string + * + * @ORM\Column(type="string", length=255) + */ + private $lastName; + + /** + * @var Collection + * + * @ORM\OneToMany( + * targetEntity="Chill\PersonBundle\Entity\PersonAltName", + * mappedBy="person", + * cascade={"persist", "remove", "merge", "detach"}, + * orphanRemoval=true) + */ + private $altNames; + + /** + * The person's birthdate + * @var \DateTime + * + * @ORM\Column(type="date", nullable=true) + */ + private $birthdate; //to change in birthdate + + /** + * The person's place of birth + * @var string + * + * @ORM\Column(type="string", length=255, name="place_of_birth") + */ + private $placeOfBirth = ''; + + /** + * The person's country of birth + * @var Country + * + * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Country") + * + * sf4 check: option inversedBy="birthsIn" return error mapping !! + * + * @ORM\JoinColumn(nullable=true) + */ + private $countryOfBirth; + + /** + * The person's nationality + * @var Country + * + * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Country") + * + * sf4 check: option inversedBy="nationals" return error mapping !! + * + * @ORM\JoinColumn(nullable=true) + */ + private $nationality; + + /** + * The person's gender + * @var string + * + * @ORM\Column(type="string", length=9, nullable=true) + */ + private $gender; + + const MALE_GENDER = 'man'; + const FEMALE_GENDER = 'woman'; + const BOTH_GENDER = 'both'; + + /** + * The marital status of the person + * @var MaritalStatus + * + * @ORM\ManyToOne(targetEntity="Chill\PersonBundle\Entity\MaritalStatus") + * @ORM\JoinColumn(nullable=true) + */ + private $maritalStatus; + + /** + * Contact information for contacting the person + * @var string + * + * @ORM\Column(type="text", nullable=true) + */ + private $contactInfo = ''; + + /** + * The person's email + * @var string + * + * @ORM\Column(type="text", nullable=true) + */ + private $email = ''; + + /** + * The person's phonenumber + * @var string + * + * @ORM\Column(type="text", length=40, nullable=true) + */ + private $phonenumber = ''; + + /** + * The person's mobile phone number + * @var string + * + * @ORM\Column(type="text", length=40, nullable=true) + */ + private $mobilenumber = ''; + + //TO-ADD caseOpeningDate + //TO-ADD nativeLanguag + + /** + * The person's spoken languages + * @var ArrayCollection + * + * @ORM\ManyToMany(targetEntity="Chill\MainBundle\Entity\Language") + * @ORM\JoinTable( + * name="persons_spoken_languages", + * joinColumns={@ORM\JoinColumn(name="person_id", referencedColumnName="id")}, + * inverseJoinColumns={@ORM\JoinColumn(name="language_id", referencedColumnName="id")} + * ) + */ + private $spokenLanguages; + + /** + * The person's center + * @var Center + * + * @ORM\ManyToOne(targetEntity="Chill\MainBundle\Entity\Center") + * @ORM\JoinColumn(nullable=false) + */ + private $center; + + /** + * The person's accompanying periods (when the person was accompanied by the center) + * @var ArrayCollection + * + * @ORM\OneToMany( + * targetEntity="Chill\PersonBundle\Entity\AccompanyingPeriod", + * mappedBy="person", + * cascade={"persist", "remove", "merge", "detach"}) + */ + private $accompanyingPeriods; //TO-CHANGE in accompanyingHistory + + /** + * A remark over the person + * @var string + * + * @ORM\Column(type="text") + */ + private $memo = ''; // TO-CHANGE in remark + + /** + * @var boolean + * @deprecated + * + * @ORM\Column(type="boolean") + */ + private $proxyAccompanyingPeriodOpenState = false; //TO-DELETE ? + + /** + * Array where customfield's data are stored + * @var array + * + * @ORM\Column(type="json_array") + */ + private $cFData; + + /** + * Addresses + * @var Collection + * + * @ORM\ManyToMany( + * targetEntity="Chill\MainBundle\Entity\Address", + * cascade={"persist", "remove", "merge", "detach"}) + * @ORM\JoinTable(name="chill_person_persons_to_addresses") + * @ORM\OrderBy({"validFrom" = "DESC"}) + */ + private $addresses; + + /** + * @var string + * + * @ORM\Column(type="text", nullable=true) + */ + private $fullnameCanonical; + + + /** + * Person constructor. + * + * @param \DateTime|null $opening + */ + public function __construct(\DateTime $opening = null) + { + $this->accompanyingPeriods = new ArrayCollection(); + $this->spokenLanguages = new ArrayCollection(); + $this->addresses = new ArrayCollection(); + $this->altNames = new ArrayCollection(); + + if ($opening === null) { + $opening = new \DateTime(); + } + + $this->open(new AccompanyingPeriod($opening)); + } + + /** + * @param AccompanyingPeriod $accompanyingPeriod + * @uses AccompanyingPeriod::setPerson + */ + public function addAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod) + { + $accompanyingPeriod->setPerson($this); + $this->accompanyingPeriods->add($accompanyingPeriod); + } + + /** + * @param AccompanyingPeriod $accompanyingPeriod + */ + public function removeAccompanyingPeriod(AccompanyingPeriod $accompanyingPeriod) + { + $this->accompanyingPeriods->remove($accompanyingPeriod); + } + + /** + * set the Person file as open at the given date. + * + * For updating a opening's date, you should update AccompanyingPeriod instance + * directly. + * + * For closing a file, @see this::close + * + * To check if the Person and its accompanying period is consistent, use validation. + * + * @param AccompanyingPeriod $accompanyingPeriod + */ + public function open(AccompanyingPeriod $accompanyingPeriod) + { + $this->proxyAccompanyingPeriodOpenState = true; + $this->addAccompanyingPeriod($accompanyingPeriod); + } + + /** + * Set the Person file as closed at the given date. + * + * For update a closing date, you should update AccompanyingPeriod instance + * directly. + * + * To check if the Person and its accompanying period are consistent, use validation. + * + * @param accompanyingPeriod + * @throws \Exception if two lines of the accompanying period are open. + */ + public function close(AccompanyingPeriod $accompanyingPeriod = null) + { + $this->proxyAccompanyingPeriodOpenState = false; + } + + /** + * Return the opened accompanying period. + * + * @return AccompanyingPeriod + */ + public function getOpenedAccompanyingPeriod() + { + if ($this->isOpen() === false) { + return null; + } + + foreach ($this->accompanyingPeriods as $period) { + if ($period->isOpen()) { + return $period; + } + } + } + + /** + * Returns the opened accompanying period. + * + * @return AccompanyingPeriod + * @deprecated since 1.1 use `getOpenedAccompanyingPeriod instead + */ + public function getCurrentAccompanyingPeriod() + { + return $this->getOpenedAccompanyingPeriod(); + } + + /** + * @return ArrayCollection + */ + public function getAccompanyingPeriods() + { + return $this->accompanyingPeriods; + } + + /** + * Get the accompanying periods of a give person with the + * chronological order. + * + * @return AccompanyingPeriod[] + */ + public function getAccompanyingPeriodsOrdered() + { + $periods = $this->getAccompanyingPeriods()->toArray(); + + //order by date : + usort($periods, function($a, $b) { + $dateA = $a->getOpeningDate(); + $dateB = $b->getOpeningDate(); + + if ($dateA == $dateB) { + $dateEA = $a->getClosingDate(); + $dateEB = $b->getClosingDate(); + + if ($dateEA == $dateEB) { + return 0; + } + + if ($dateEA < $dateEB) { + return -1; + } else { + return +1; + } + } + + if ($dateA < $dateB) { + return -1 ; + } else { + return 1; + } + }); + + return $periods; + } + + /** + * check if the person is opened + * + * @return boolean + */ + public function isOpen() + { + foreach ($this->getAccompanyingPeriods() as $period) { + if ($period->isOpen()) { + return true; + } + } + + return false; + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Set firstName + * + * @param string $firstName + * @return Person + */ + public function setFirstName($firstName) + { + $this->firstName = $firstName; + + return $this; + } + + /** + * Get firstName + * + * @return string + */ + public function getFirstName() + { + return $this->firstName; + } + + /** + * Set lastName + * + * @param string $lastName + * @return Person + */ + public function setLastName($lastName) + { + $this->lastName = $lastName; + + return $this; + } + + /** + * Get lastName + * + * @return string + */ + public function getLastName() + { + return $this->lastName; + } + + /** + * @return Collection + */ + public function getAltNames(): Collection + { + return $this->altNames; + } + + /** + * @param Collection $altNames + * @return $this + */ + public function setAltNames(Collection $altNames) + { + $this->altNames = $altNames; + + return $this; + } + + /** + * @param PersonAltName $altName + * @return $this + */ + public function addAltName(PersonAltName $altName) + { + if (FALSE === $this->altNames->contains($altName)) { + $this->altNames->add($altName); + $altName->setPerson($this); + } + + return $this; + } + + /** + * @param PersonAltName $altName + * @return $this + */ + public function removeAltName(PersonAltName $altName) + { + if ($this->altNames->contains($altName)) { + $altName->setPerson(null); + $this->altNames->removeElement($altName); + } + + return $this; + } + + /** + * Set birthdate + * + * @param \DateTime $birthdate + * @return Person + */ + public function setBirthdate($birthdate) + { + $this->birthdate = $birthdate; + + return $this; + } + + /** + * Get birthdate + * + * @return \DateTime + */ + public function getBirthdate() + { + return $this->birthdate; + } + + /** + * Set placeOfBirth + * + * @param string $placeOfBirth + * @return Person + */ + public function setPlaceOfBirth($placeOfBirth) + { + if ($placeOfBirth === null) { + $placeOfBirth = ''; + } + + $this->placeOfBirth = $placeOfBirth; + + return $this; + } + + /** + * Get placeOfBirth + * + * @return string + */ + public function getPlaceOfBirth() + { + return $this->placeOfBirth; + } + + /** + * Set gender + * + * @param string $gender + * @return Person + */ + public function setGender($gender) + { + $this->gender = $gender; + + return $this; + } + + /** + * Get gender + * + * @return string + */ + public function getGender() + { + return $this->gender; + } + + /** + * return gender as a Numeric form. + * This is used for translations + * @return int + */ + public function getGenderNumeric() + { + if ($this->getGender() == self::FEMALE_GENDER) { + return 1; + } else { + return 0; + } + } + + /** + * Set memo + * + * @param string $memo + * @return Person + */ + public function setMemo($memo) + { + if ($memo === null) { + $memo = ''; + } + + if ($this->memo !== $memo) { + $this->memo = $memo; + } + + return $this; + } + + /** + * Get memo + * + * @return string + */ + public function getMemo() + { + return $this->memo; + } + + /** + * Set maritalStatus + * + * @param MaritalStatus $maritalStatus + * @return Person + */ + public function setMaritalStatus(MaritalStatus $maritalStatus = null) + { + $this->maritalStatus = $maritalStatus; + return $this; + } + + /** + * Get maritalStatus + * + * @return MaritalStatus + */ + public function getMaritalStatus() + { + return $this->maritalStatus; + } + + /** + * Set contactInfo + * + * @param string $contactInfo + * @return Person + */ + public function setcontactInfo($contactInfo) + { + if ($contactInfo === null) { + $contactInfo = ''; + } + + $this->contactInfo = $contactInfo; + + return $this; + } + + /** + * Get contactInfo + * + * @return string + */ + public function getcontactInfo() + { + return $this->contactInfo; + } + + /** + * Set email + * + * @param string $email + * @return Person + */ + public function setEmail($email) + { + if ($email === null) { + $email = ''; + } + + $this->email = $email; + + return $this; + } + + /** + * Get email + * + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * Set countryOfBirth + * + * @param Chill\MainBundle\Entity\Country $countryOfBirth + * @return Person + */ + public function setCountryOfBirth(Country $countryOfBirth = null) + { + $this->countryOfBirth = $countryOfBirth; + return $this; + } + + /** + * Get countryOfBirth + * + * @return Chill\MainBundle\Entity\Country + */ + public function getCountryOfBirth() + { + return $this->countryOfBirth; + } + + /** + * Set nationality + * + * @param Chill\MainBundle\Entity\Country $nationality + * @return Person + */ + public function setNationality(Country $nationality = null) + { + $this->nationality = $nationality; + + return $this; + } + + /** + * Get nationality + * + * @return Chill\MainBundle\Entity\Country + */ + public function getNationality() + { + return $this->nationality; + } + + /** + * @return string + */ + public function getLabel() + { + return $this->getFirstName()." ".$this->getLastName(); + } + + /** + * Get center + * + * @return Center + */ + public function getCenter() + { + return $this->center; + } + + /** + * Set the center + * + * @param Center $center + * @return \Chill\PersonBundle\Entity\Person + */ + public function setCenter(Center $center) + { + $this->center = $center; + return $this; + } + + /** + * Set cFData + * + * @param array $cFData + * + * @return Report + */ + public function setCFData($cFData) + { + $this->cFData = $cFData; + + return $this; + } + + /** + * Get cFData + * + * @return array + */ + public function getCFData() + { + if ($this->cFData === null) { + $this->cFData = []; + } + return $this->cFData; + } + + /** + * Set phonenumber + * + * @param string $phonenumber + * @return Person + */ + public function setPhonenumber($phonenumber = '') + { + $this->phonenumber = $phonenumber; + + return $this; + } + + /** + * Get phonenumber + * + * @return string + */ + public function getPhonenumber() + { + return $this->phonenumber; + } + + /** + * Set mobilenumber + * + * @param string $mobilenumber + * @return Person + */ + public function setMobilenumber($mobilenumber = '') + { + $this->mobilenumber = $mobilenumber; + + return $this; + } + + /** + * Get mobilenumber + * + * @return string + */ + public function getMobilenumber() + { + return $this->mobilenumber; + } + + /** + * @return string + */ + public function __toString() + { + return $this->getLabel(); + } + + /** + * Set spokenLanguages + * + * @param type $spokenLanguages + * @return Person + */ + public function setSpokenLanguages($spokenLanguages) + { + $this->spokenLanguages = $spokenLanguages; + + return $this; + } + + /** + * Get spokenLanguages + * + * @return ArrayCollection + */ + public function getSpokenLanguages() + { + return $this->spokenLanguages; + } + + /** + * @param Address $address + * @return $this + */ + public function addAddress(Address $address) + { + $this->addresses[] = $address; + + return $this; + } + + /** + * @param Address $address + */ + public function removeAddress(Address $address) + { + $this->addresses->removeElement($address); + } + + /** + * By default, the addresses are ordered by date, descending (the most + * recent first) + * + * @return \Chill\MainBundle\Entity\Address[] + */ + public function getAddresses() + { + return $this->addresses; + } + + /** + * @param \DateTime|null $date + * @return null + */ + public function getLastAddress(\DateTime $date = null) + { + if ($date === null) { + $date = new \DateTime('now'); + } + + $addresses = $this->getAddresses(); + + if ($addresses == null) { + + return null; + } + + return $addresses->first(); + } + + /** + * Validation callback that checks if the accompanying periods are valid + * + * This method add violation errors. + */ + public function isAccompanyingPeriodValid(ExecutionContextInterface $context) + { + $r = $this->checkAccompanyingPeriodsAreNotCollapsing(); + + if ($r !== true) { + if ($r['result'] === self::ERROR_PERIODS_ARE_COLLAPSING) { + $context->buildViolation('Two accompanying periods have days in commun') + ->atPath('accompanyingPeriods') + ->addViolation(); + } + + if ($r['result'] === self::ERROR_ADDIND_PERIOD_AFTER_AN_OPEN_PERIOD) { + $context->buildViolation('A period is opened and a period is added after it') + ->atPath('accompanyingPeriods') + ->addViolation(); + } + } + } + + /** + * Return true if the person has two addresses with the + * same validFrom date (in format 'Y-m-d') + */ + public function hasTwoAdressWithSameValidFromDate() + { + $validYMDDates = array(); + + foreach ($this->addresses as $ad) { + $validDate = $ad->getValidFrom()->format('Y-m-d'); + + if (in_array($validDate, $validYMDDates)) { + return true; + } + $validYMDDates[] = $validDate; + } + + return false; + } + + /** + * Validation callback that checks if the addresses are valid (do not have + * two addresses with the same validFrom date) + * + * This method add violation errors. + */ + public function isAddressesValid(ExecutionContextInterface $context) + { + if ($this->hasTwoAdressWithSameValidFromDate()) { + $context + ->buildViolation('Two addresses has the same validFrom date') + ->atPath('addresses') + ->addViolation() + ; + } + } + + + const ERROR_PERIODS_ARE_COLLAPSING = 1; // when two different periods + // have days in commun + const ERROR_ADDIND_PERIOD_AFTER_AN_OPEN_PERIOD = 2; // where there exist + // a period opened and another one after it + + /** + * Function used for validation that check if the accompanying periods of + * the person are not collapsing (i.e. have not shared days) or having + * a period after an open period. + * + * @return true | array True if the accompanying periods are not collapsing, + * an array with data for displaying the error + */ + public function checkAccompanyingPeriodsAreNotCollapsing() + { + $periods = $this->getAccompanyingPeriodsOrdered(); + $periodsNbr = sizeof($periods); + $i = 0; + + while($i < $periodsNbr - 1) { + $periodI = $periods[$i]; + $periodAfterI = $periods[$i + 1]; + + if($periodI->isOpen()) { + return array( + 'result' => self::ERROR_ADDIND_PERIOD_AFTER_AN_OPEN_PERIOD, + 'dateOpening' => $periodAfterI->getOpeningDate(), + 'dateClosing' => $periodAfterI->getClosingDate(), + 'date' => $periodI->getOpeningDate() + ); + } elseif ($periodI->getClosingDate() >= $periodAfterI->getOpeningDate()) { + return array( + 'result' => self::ERROR_PERIODS_ARE_COLLAPSING, + 'dateOpening' => $periodI->getOpeningDate(), + + 'dateClosing' => $periodI->getClosingDate(), + 'date' => $periodAfterI->getOpeningDate() + ); + } + $i++; + } + + return true; + } +} diff --git a/src/Bundle/ChillPerson/Entity/PersonAltName.php b/src/Bundle/ChillPerson/Entity/PersonAltName.php new file mode 100644 index 000000000..c5295487e --- /dev/null +++ b/src/Bundle/ChillPerson/Entity/PersonAltName.php @@ -0,0 +1,125 @@ +id; + } + + /** + * Set key. + * + * @param string $key + * + * @return PersonAltName + */ + public function setKey($key) + { + $this->key = $key; + + return $this; + } + + /** + * Get key. + * + * @return string + */ + public function getKey() + { + return $this->key; + } + + /** + * Set label. + * + * @param string $label + * + * @return PersonAltName + */ + public function setLabel($label) + { + $this->label = $label; + + return $this; + } + + /** + * Get label. + * + * @return string + */ + public function getLabel() + { + return $this->label; + } + + /** + * @return Person + */ + public function getPerson(): Person + { + return $this->person; + } + + /** + * @param Person|null $person + * @return $this + */ + public function setPerson(?Person $person = null) + { + $this->person = $person; + + return $this; + } +} diff --git a/src/Bundle/ChillPerson/Export/AbstractAccompanyingPeriodExportElement.php b/src/Bundle/ChillPerson/Export/AbstractAccompanyingPeriodExportElement.php new file mode 100644 index 000000000..c21d5733a --- /dev/null +++ b/src/Bundle/ChillPerson/Export/AbstractAccompanyingPeriodExportElement.php @@ -0,0 +1,59 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Export; + +use Doctrine\ORM\QueryBuilder; + +/** + * + * + */ +class AbstractAccompanyingPeriodExportElement +{ + /** + * Return true if "accompanying_period" alias is present in the query alises. + * + * @param QueryBuilder $query + * @return bool + */ + protected function havingAccompanyingPeriodInJoin(QueryBuilder $query): bool + { + $joins = $query->getDQLPart('join') ?? []; + + return (\in_array('accompanying_period', $query->getAllAliases())); + } + + /** + * Add the accompanying period alias to the query + * + * @param QueryBuilder $query + * @return void + * @throws \LogicException if the "person" alias is not present and attaching accompanying period is not possible + */ + protected function addJoinAccompanyingPeriod(QueryBuilder $query): void + { + if (FALSE === $this->havingAccompanyingPeriodInJoin($query)) { + if (FALSE === \in_array('person', $query->getAllAliases())) { + throw new \LogicException("the alias 'person' does not exists in " + . "query builder"); + } + + $query->join('person.accompanyingPeriods', 'accompanying_period'); + } + } +} diff --git a/src/Bundle/ChillPerson/Export/Aggregator/AgeAggregator.php b/src/Bundle/ChillPerson/Export/Aggregator/AgeAggregator.php new file mode 100644 index 000000000..7b2e6c1b4 --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Aggregator/AgeAggregator.php @@ -0,0 +1,112 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Export\Aggregator; + +use Chill\MainBundle\Export\AggregatorInterface; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * + * + * @author Julien Fastré + */ +class AgeAggregator implements AggregatorInterface, + ExportElementValidatedInterface +{ + /** + * + * @var + */ + protected $translator; + + public function __construct($translator) + { + $this->translator = $translator; + } + + + public function addRole() + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $qb->addSelect('DATE_DIFF(:date_age_calculation, person.birthdate)/365 as person_age'); + $qb->setParameter('date_age_calculation', $data['date_age_calculation']); + $qb->addGroupBy('person_age'); + } + + public function applyOn() + { + return 'person'; + } + + public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder) + { + $builder->add('date_age_calculation', DateType::class, array( + 'label' => "Calculate age in relation to this date", + 'data' => new \DateTime(), + 'attr' => array('class' => 'datepicker'), + 'widget'=> 'single_text', + 'format' => 'dd-MM-yyyy' + )); + } + + public function validateForm($data, ExecutionContextInterface $context) + { + if ($data['date_age_calculation'] === null) { + $context->buildViolation("The date should not be empty") + ->addViolation(); + } + } + + public function getLabels($key, array $values, $data) + { + return function($value) { + if ($value === '_header') { + return "Age"; + } + + if ($value === NULL) { + return $this->translator->trans("without data"); + } + + return $value; + }; + } + + public function getQueryKeys($data) + { + return array( + 'person_age' + ); + } + + public function getTitle() + { + return "Aggregate by age"; + } + +} diff --git a/src/Bundle/ChillPerson/Export/Aggregator/CountryOfBirthAggregator.php b/src/Bundle/ChillPerson/Export/Aggregator/CountryOfBirthAggregator.php new file mode 100644 index 000000000..5bb08af60 --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Aggregator/CountryOfBirthAggregator.php @@ -0,0 +1,202 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Export\Aggregator; + +use Chill\MainBundle\Export\AggregatorInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\EntityRepository; +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Symfony\Component\Translation\TranslatorInterface; +use Chill\MainBundle\Util\CountriesInfo; +use Symfony\Component\Security\Core\Role\Role; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; + + +/** + * + * + * @author Julien Fastré + */ +class CountryOfBirthAggregator implements AggregatorInterface, + ExportElementValidatedInterface +{ + /** + * + * @var EntityRepository + */ + protected $countriesRepository; + + /** + * + * @var TranslatableStringHelper + */ + protected $translatableStringHelper; + + /** + * + * @var TranslatorInterface + */ + protected $translator; + + public function __construct(EntityRepository $countriesRepository, + TranslatableStringHelper $translatableStringHelper, + TranslatorInterface $translator) + { + $this->countriesRepository = $countriesRepository; + $this->translatableStringHelper = $translatableStringHelper; + $this->translator = $translator; + } + + public function applyOn() + { + return 'person'; + } + + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('group_by_level', ChoiceType::class, array( + 'choices' => array( + 'Group by continents' => 'continent', + 'Group by country' => 'country' + ), + 'expanded' => true, + 'multiple' => false + )); + + } + + public function validateForm($data, ExecutionContextInterface $context) + { + if ($data['group_by_level'] === null) { + $context->buildViolation("You should select an option") + ->addViolation(); + } + } + + public function alterQuery(QueryBuilder $qb, $data) + { + // add a clause in select part + if ($data['group_by_level'] === 'country') { + $qb->addSelect('countryOfBirth.countryCode as country_of_birth_aggregator'); + } elseif ($data['group_by_level'] === 'continent') { + $clause = 'CASE ' + . 'WHEN countryOfBirth.countryCode IN(:cob_africa_codes) THEN \'AF\' ' + . 'WHEN countryOfBirth.countryCode IN(:cob_asia_codes) THEN \'AS\' ' + . 'WHEN countryOfBirth.countryCode IN(:cob_europe_codes) THEN \'EU\' ' + . 'WHEN countryOfBirth.countryCode IN(:cob_north_america_codes) THEN \'NA\' ' + . 'WHEN countryOfBirth.countryCode IN(:cob_south_america_codes) THEN \'SA\' ' + . 'WHEN countryOfBirth.countryCode IN(:cob_oceania_codes) THEN \'OC\' ' + . 'WHEN countryOfBirth.countryCode IN(:cob_antartica_codes) THEN \'AN\' ' + . 'ELSE \'\' ' + . 'END as country_of_birth_aggregator '; + $qb->addSelect($clause); + $params = + array( + 'cob_africa_codes' => CountriesInfo::getCountriesCodeByContinent('AF'), + 'cob_asia_codes' => CountriesInfo::getCountriesCodeByContinent('AS'), + 'cob_europe_codes' => CountriesInfo::getCountriesCodeByContinent('EU'), + 'cob_north_america_codes' => CountriesInfo::getCountriesCodeByContinent('NA'), + 'cob_south_america_codes' => CountriesInfo::getCountriesCodeByContinent('SA'), + 'cob_oceania_codes' => CountriesInfo::getCountriesCodeByContinent('OC'), + 'cob_antartica_codes' => CountriesInfo::getCountriesCodeByContinent('AN') + ); + foreach ($params as $k => $v) { + $qb->setParameter($k, $v); + } + } else { + throw new \LogicException("The group_by_level '".$data['group_by_level'] + ." is not known."); + } + + + $qb->leftJoin('person.countryOfBirth', 'countryOfBirth'); + + // add group by + $groupBy = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('country_of_birth_aggregator'); + } else { + $qb->groupBy('country_of_birth_aggregator'); + } + + } + + public function getTitle() + { + return "Group people by country of birth"; + } + + public function getQueryKeys($data) + { + return array('country_of_birth_aggregator'); + } + + public function addRole() + { + return NULL; + } + + public function getLabels($key, array $values, $data) + { + if ($data['group_by_level'] === 'country') { + $qb = $this->countriesRepository->createQueryBuilder('c'); + + $countries = $qb + ->andWhere($qb->expr()->in('c.countryCode', ':countries')) + ->setParameter('countries', $values) + ->getQuery() + ->getResult(\Doctrine\ORM\Query::HYDRATE_SCALAR); + + // initialize array and add blank key for null values + $labels[''] = $this->translator->trans('without data'); + $labels['_header'] = $this->translator->trans('Country of birth'); + foreach($countries as $row) { + $labels[$row['c_countryCode']] = $this->translatableStringHelper->localize($row['c_name']); + } + + + } elseif ($data['group_by_level'] === 'continent') { + + $labels = array( + 'EU' => $this->translator->trans('Europe'), + 'AS' => $this->translator->trans('Asia'), + 'AN' => $this->translator->trans('Antartica'), + 'AF' => $this->translator->trans('Africa'), + 'SA' => $this->translator->trans('South America'), + 'NA' => $this->translator->trans('North America'), + 'OC' => $this->translator->trans('Oceania'), + '' => $this->translator->trans('without data'), + '_header' => $this->translator->trans('Continent of birth') + ); + } + + + return function($value) use ($labels) { + return $labels[$value]; + }; + + } +} diff --git a/src/Bundle/ChillPerson/Export/Aggregator/GenderAggregator.php b/src/Bundle/ChillPerson/Export/Aggregator/GenderAggregator.php new file mode 100644 index 000000000..6f4314a76 --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Aggregator/GenderAggregator.php @@ -0,0 +1,103 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Export\Aggregator; + +use Chill\MainBundle\Export\AggregatorInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Translation\TranslatorInterface; +use Chill\PersonBundle\Entity\Person; +use Chill\PersonBundle\Export\Declarations; + +/** + * + * + * @author Julien Fastré + */ +class GenderAggregator implements AggregatorInterface +{ + + /** + * + * @var TranslatorInterface + */ + protected $translator; + + public function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + public function applyOn() + { + return Declarations::PERSON_TYPE; + } + + + public function buildForm(FormBuilderInterface $builder) + { + + } + + public function alterQuery(QueryBuilder $qb, $data) + { + + $qb->addSelect('person.gender as gender'); + + $qb->addGroupBy('gender'); + + } + + public function getTitle() + { + return "Group people by gender"; + } + + public function getQueryKeys($data) + { + return array('gender'); + } + + public function getLabels($key, array $values, $data) + { + return function($value) { + switch ($value) { + case Person::FEMALE_GENDER : + return $this->translator->trans('woman'); + case Person::MALE_GENDER : + return $this->translator->trans('man'); + case Person::BOTH_GENDER: + return $this->translator->trans('both'); + case null: + return $this->translator->trans('Not given'); + case '_header' : + return $this->translator->trans('Gender'); + default: + throw new \LogicException(sprintf("The value %s is not valid", $value)); + } + }; + } + + public function addRole() + { + return NULL; + } + +} diff --git a/src/Bundle/ChillPerson/Export/Aggregator/NationalityAggregator.php b/src/Bundle/ChillPerson/Export/Aggregator/NationalityAggregator.php new file mode 100644 index 000000000..d708957f7 --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Aggregator/NationalityAggregator.php @@ -0,0 +1,201 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Export\Aggregator; + +use Chill\MainBundle\Export\AggregatorInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\EntityRepository; +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Symfony\Component\Translation\TranslatorInterface; +use Chill\MainBundle\Util\CountriesInfo; +use Symfony\Component\Security\Core\Role\Role; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; + +/** + * + * + * @author Julien Fastré + */ +class NationalityAggregator implements AggregatorInterface, + ExportElementValidatedInterface +{ + /** + * + * @var EntityRepository + */ + protected $countriesRepository; + + /** + * + * @var TranslatableStringHelper + */ + protected $translatableStringHelper; + + /** + * + * @var TranslatorInterface + */ + protected $translator; + + public function __construct(EntityRepository $countriesRepository, + TranslatableStringHelper $translatableStringHelper, + TranslatorInterface $translator) + { + $this->countriesRepository = $countriesRepository; + $this->translatableStringHelper = $translatableStringHelper; + $this->translator = $translator; + } + + public function applyOn() + { + return 'person'; + } + + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('group_by_level', ChoiceType::class, array( + 'choices' => array( + 'Group by continents' => 'continent', + 'Group by country' => 'country' + ), + 'expanded' => true, + 'multiple' => false + )); + + } + + public function validateForm($data, ExecutionContextInterface $context) + { + if ($data['group_by_level'] === null) { + $context->buildViolation("You should select an option") + ->addViolation(); + } + } + + public function alterQuery(QueryBuilder $qb, $data) + { + // add a clause in select part + if ($data['group_by_level'] === 'country') { + $qb->addSelect('nationality.countryCode as nationality_aggregator'); + } elseif ($data['group_by_level'] === 'continent') { + $clause = 'CASE ' + . 'WHEN nationality.countryCode IN(:africa_codes) THEN \'AF\' ' + . 'WHEN nationality.countryCode IN(:asia_codes) THEN \'AS\' ' + . 'WHEN nationality.countryCode IN(:europe_codes) THEN \'EU\' ' + . 'WHEN nationality.countryCode IN(:north_america_codes) THEN \'NA\' ' + . 'WHEN nationality.countryCode IN(:south_america_codes) THEN \'SA\' ' + . 'WHEN nationality.countryCode IN(:oceania_codes) THEN \'OC\' ' + . 'WHEN nationality.countryCode IN(:antartica_codes) THEN \'AN\' ' + . 'ELSE \'\' ' + . 'END as nationality_aggregator '; + $qb->addSelect($clause); + $params = + array( + 'africa_codes' => CountriesInfo::getCountriesCodeByContinent('AF'), + 'asia_codes' => CountriesInfo::getCountriesCodeByContinent('AS'), + 'europe_codes' => CountriesInfo::getCountriesCodeByContinent('EU'), + 'north_america_codes' => CountriesInfo::getCountriesCodeByContinent('NA'), + 'south_america_codes' => CountriesInfo::getCountriesCodeByContinent('SA'), + 'oceania_codes' => CountriesInfo::getCountriesCodeByContinent('OC'), + 'antartica_codes' => CountriesInfo::getCountriesCodeByContinent('AN') + ); + foreach ($params as $k => $v) { + $qb->setParameter($k, $v); + } + } else { + throw new \LogicException("The group_by_level '".$data['group_by_level'] + ." is not known."); + } + + + $qb->leftJoin('person.nationality', 'nationality'); + + // add group by + $groupBy = $qb->getDQLPart('groupBy'); + + if (!empty($groupBy)) { + $qb->addGroupBy('nationality_aggregator'); + } else { + $qb->groupBy('nationality_aggregator'); + } + + } + + public function getTitle() + { + return "Group people by nationality"; + } + + public function getQueryKeys($data) + { + return array('nationality_aggregator'); + } + + public function addRole() + { + return NULL; + } + + public function getLabels($key, array $values, $data) + { + if ($data['group_by_level'] === 'country') { + $qb = $this->countriesRepository->createQueryBuilder('c'); + + $countries = $qb + ->andWhere($qb->expr()->in('c.countryCode', ':countries')) + ->setParameter('countries', $values) + ->getQuery() + ->getResult(\Doctrine\ORM\Query::HYDRATE_SCALAR); + + // initialize array and add blank key for null values + $labels[''] = $this->translator->trans('without data'); + $labels['_header'] = $this->translator->trans('Nationality'); + foreach($countries as $row) { + $labels[$row['c_countryCode']] = $this->translatableStringHelper->localize($row['c_name']); + } + + + } elseif ($data['group_by_level'] === 'continent') { + + $labels = array( + 'EU' => $this->translator->trans('Europe'), + 'AS' => $this->translator->trans('Asia'), + 'AN' => $this->translator->trans('Antartica'), + 'AF' => $this->translator->trans('Africa'), + 'SA' => $this->translator->trans('South America'), + 'NA' => $this->translator->trans('North America'), + 'OC' => $this->translator->trans('Oceania'), + '' => $this->translator->trans('without data'), + '_header' => $this->translator->trans('Continent') + ); + } + + + return function($value) use ($labels) { + return $labels[$value]; + }; + + } +} diff --git a/src/Bundle/ChillPerson/Export/Declarations.php b/src/Bundle/ChillPerson/Export/Declarations.php new file mode 100644 index 000000000..bbdf3e8e4 --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Declarations.php @@ -0,0 +1,32 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Export; + +/** + * This class declare constants used for the export framework. + * + * + * @author Julien Fastré + */ +abstract class Declarations +{ + CONST PERSON_TYPE = 'person'; + CONST PERSON_IMPLIED_IN = 'person_implied_in'; +} diff --git a/src/Bundle/ChillPerson/Export/Export/CountPerson.php b/src/Bundle/ChillPerson/Export/Export/CountPerson.php new file mode 100644 index 000000000..0431095ec --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Export/CountPerson.php @@ -0,0 +1,136 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Export\Export; + +use Chill\MainBundle\Export\ExportInterface; +use Doctrine\ORM\QueryBuilder; +use Symfony\Component\Form\FormBuilderInterface; +use Doctrine\ORM\Query; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Symfony\Component\Security\Core\Role\Role; +use Chill\PersonBundle\Export\Declarations; +use Chill\MainBundle\Export\FormatterInterface; +use Doctrine\ORM\EntityManagerInterface; + +/** + * + * + * @author Julien Fastré + */ +class CountPerson implements ExportInterface +{ + /** + * + * @var EntityManagerInterface + */ + protected $entityManager; + + public function __construct( + EntityManagerInterface $em + ) + { + $this->entityManager = $em; + } + + /** + * + */ + public function getType() + { + return Declarations::PERSON_TYPE; + } + + public function getDescription() + { + return "Count peoples by various parameters."; + } + + public function getTitle() + { + return "Count peoples"; + } + + public function requiredRole() + { + return new Role(PersonVoter::STATS); + } + + /** + * Initiate the query + * + * @param QueryBuilder $qb + * @return QueryBuilder + */ + public function initiateQuery(array $requiredModifiers, array $acl, array $data = array()) + { + $centers = array_map(function($el) { return $el['center']; }, $acl); + + $qb = $this->entityManager->createQueryBuilder(); + + $qb->select('COUNT(person.id) AS export_result') + ->from('ChillPersonBundle:Person', 'person') + ->join('person.center', 'center') + ->andWhere('center IN (:authorized_centers)') + ->setParameter('authorized_centers', $centers); + ; + + + return $qb; + } + + public function getResult($qb, $data) + { + return $qb->getQuery()->getResult(Query::HYDRATE_SCALAR); + } + + public function getQueryKeys($data) + { + return array('export_result'); + } + + public function getLabels($key, array $values, $data) + { + if ($key !== 'export_result') { + throw new \LogicException("the key $key is not used by this export"); + } + + $labels = array_combine($values, $values); + $labels['_header'] = $this->getTitle(); + + return function($value) use ($labels) { + return $labels[$value]; + }; + } + + public function getAllowedFormattersTypes() + { + return array(FormatterInterface::TYPE_TABULAR); + } + + public function buildForm(FormBuilderInterface $builder) { + + } + + public function supportsModifiers() + { + return array(Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN); + } + +} diff --git a/src/Bundle/ChillPerson/Export/Export/ListPerson.php b/src/Bundle/ChillPerson/Export/Export/ListPerson.php new file mode 100644 index 000000000..dc36c8c57 --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Export/ListPerson.php @@ -0,0 +1,503 @@ +entityManager = $em; + $this->translator = $translator; + $this->translatableStringHelper = $translatableStringHelper; + $this->customFieldProvider = $customFieldProvider; + } + + /** + * {@inheritDoc} + * + * @param FormBuilderInterface $builder + */ + public function buildForm(FormBuilderInterface $builder) + { + $choices = array_combine($this->fields, $this->fields); + + foreach ($this->getCustomFields() as $cf) { + $choices + [$this->translatableStringHelper->localize($cf->getName())] + = + $cf->getSlug(); + } + + // Add a checkbox to select fields + $builder->add('fields', ChoiceType::class, array( + 'multiple' => true, + 'expanded' => true, + 'choices' => $choices, + 'label' => 'Fields to include in export', + 'choice_attr' => function($val, $key, $index) { + // add a 'data-display-target' for address fields + if (substr($val, 0, 8) === 'address_') { + return ['data-display-target' => 'address_date']; + } else { + return []; + } + }, + 'constraints' => [new Callback(array( + 'callback' => function($selected, ExecutionContextInterface $context) { + if (count($selected) === 0) { + $context->buildViolation('You must select at least one element') + ->atPath('fields') + ->addViolation(); + } + } + ))] + )); + + // add a date field for addresses + $builder->add('address_date', DateType::class, array( + 'label' => "Address valid at this date", + 'data' => new \DateTime(), + 'attr' => array( 'class' => 'datepicker'), + 'widget'=> 'single_text', + 'format' => 'dd-MM-yyyy', + 'required' => false, + 'block_name' => 'list_export_form_address_date' + )); + } + + public function validateForm($data, ExecutionContextInterface $context) + { + // get the field starting with address_ + $addressFields = array_filter(function($el) { + return substr($el, 0, 8) === 'address_'; + }, $this->fields); + + // check if there is one field starting with address in data + if (count(array_intersect($data['fields'], $addressFields)) > 0) { + // if a field address is checked, the date must not be empty + if (empty($data['address_date'])) { + $context + ->buildViolation("You must set this date if an address is checked") + ->atPath('address_date') + ->addViolation(); + } + } + } + + /** + * Get custom fields associated with person + * + * @return CustomField[] + */ + private function getCustomFields() + { + return $this->entityManager + ->createQuery("SELECT cf " + . "FROM ChillCustomFieldsBundle:CustomField cf " + . "JOIN cf.customFieldGroup g " + . "WHERE cf.type != :title AND g.entity LIKE :entity") + ->setParameters(array( + 'title' => 'title', + 'entity' => \addcslashes(Person::class, "\\") + )) + ->getResult(); + } + + /** + * {@inheritDoc} + * + * @return type + */ + public function getAllowedFormattersTypes() + { + return array(FormatterInterface::TYPE_LIST); + } + + /** + * {@inheritDoc} + * + * @return string + */ + public function getDescription() + { + return "Create a list of people according to various filters."; + } + + /** + * {@inheritDoc} + * + * @param type $key + * @param array $values + * @param type $data + * @return type + */ + public function getLabels($key, array $values, $data) + { + switch ($key) { + case 'birthdate': + // for birthdate, we have to transform the string into a date + // to format the date correctly. + return function($value) { + if ($value === '_header') { return 'birthdate'; } + + if (empty($value)) + { + return ""; + } + + $date = \DateTime::createFromFormat('Y-m-d', $value); + // check that the creation could occurs. + if ($date === false) { + throw new \Exception(sprintf("The value %s could " + . "not be converted to %s", $value, \DateTime::class)); + } + + return $date->format('d-m-Y'); + }; + case 'gender' : + // for gender, we have to translate men/women statement + return function($value) { + if ($value === '_header') { return 'gender'; } + + return $this->translator->trans($value); + }; + case 'countryOfBirth': + case 'nationality': + $countryRepository = $this->entityManager + ->getRepository('ChillMainBundle:Country'); + + // load all countries in a single query + $countryRepository->findBy(array('countryCode' => $values)); + + return function($value) use ($key, $countryRepository) { + if ($value === '_header') { return \strtolower($key); } + + if ($value === NULL) { + return $this->translator->trans('no data'); + } + + $country = $countryRepository->find($value); + + return $this->translatableStringHelper->localize( + $country->getName()); + }; + case 'address_country_name': + return function($value) use ($key) { + if ($value === '_header') { return \strtolower($key); } + + if ($value === NULL) { + return ''; + } + + return $this->translatableStringHelper->localize(json_decode($value, true)); + }; + case 'address_isnoaddress': + return function($value) use ($key) { + if ($value === '_header') { return 'address.address_homeless'; } + + if ($value) { + return 'X'; + } else { + return ''; + } + }; + default: + // for fields which are associated with person + if (in_array($key, $this->fields)) { + return function($value) use ($key) { + if ($value === '_header') { return \strtolower($key); } + + return $value; + + }; + } else { + return $this->getLabelForCustomField($key, $values, $data); + } + } + + } + + private function getLabelForCustomField($key, array $values, $data) + { + // for fields which are custom fields + /* @var $cf CustomField */ + $cf = $this->entityManager + ->getRepository(CustomField::class) + ->findOneBy(array('slug' => $this->DQLToSlug($key))); + $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); + $defaultFunction = function($value) use ($cf) { + if ($value === '_header') { + return $this->translatableStringHelper->localize($cf->getName()); + } + + return $this->customFieldProvider + ->getCustomFieldByType($cf->getType()) + ->render(json_decode($value, true), $cf, 'csv'); + }; + + if ($cfType instanceof CustomFieldChoice and $cfType->isMultiple($cf)) { + return function($value) use ($cf, $cfType, $key) { + $slugChoice = $this->extractInfosFromSlug($key)['additionnalInfos']['choiceSlug']; + $decoded = \json_decode($value, true); + + if ($value === '_header') { + + $label = $cfType->getChoices($cf)[$slugChoice]; + + return $this->translatableStringHelper->localize($cf->getName()) + .' | '.$label; + } + + if ($slugChoice === '_other' and $cfType->isChecked($cf, $choiceSlug, $decoded)) { + return $cfType->extractOtherValue($cf, $decoded); + } else { + return $cfType->isChecked($cf, $slugChoice, $decoded); + } + }; + + } else { + return $defaultFunction; + } + } + + /** + * {@inheritDoc} + * + * @param type $data + * @return type + */ + public function getQueryKeys($data) + { + $fields = array(); + + foreach ($data['fields'] as $key) { + if (in_array($key, $this->fields)) { + $fields[] = $key; + } + } + + // add the key from slugs and return + return \array_merge($fields, \array_keys($this->slugs)); + } + + /** + * clean a slug to be usable by DQL + * + * @param string $slugsanitize + * @param string $type the type of the customfield, if required (currently only for choices) + * @return string + */ + private function slugToDQL($slug, $type = "default", array $additionalInfos = []) + { + $uid = 'slug_'.\uniqid(); + + $this->slugs[$uid] = [ + 'slug' => $slug, + 'type' => $type, + 'additionnalInfos' => $additionalInfos + ]; + + return $uid; + } + + private function DQLToSlug($cleanedSlug) + { + return $this->slugs[$cleanedSlug]['slug']; + } + + /** + * + * @param type $cleanedSlug + * @return an array with keys = 'slug', 'type', 'additionnalInfo' + */ + private function extractInfosFromSlug($slug) + { + return $this->slugs[$slug]; + } + + /** + * {@inheritDoc} + * + */ + public function getResult($query, $data) + { + return $query->getQuery()->getResult(Query::HYDRATE_SCALAR); + } + + /** + * {@inheritDoc} + * + * @return string + */ + public function getTitle() + { + return "List peoples"; + } + + /** + * {@inheritDoc} + * + */ + public function getType() + { + return Declarations::PERSON_TYPE; + } + + /** + * {@inheritDoc} + * + */ + public function initiateQuery(array $requiredModifiers, array $acl, array $data = array()) + { + $centers = array_map(function($el) { return $el['center']; }, $acl); + + // throw an error if any fields are present + if (!\array_key_exists('fields', $data)) { + throw new \Doctrine\DBAL\Exception\InvalidArgumentException("any fields " + . "have been checked"); + } + + $qb = $this->entityManager->createQueryBuilder(); + + foreach ($this->fields as $f) { + if (in_array($f, $data['fields'])) { + switch ($f) { + case 'countryOfBirth': + case 'nationality': + $qb->addSelect(sprintf('IDENTITY(person.%s) as %s', $f, $f)); + break; + case 'address_street_address_1': + case 'address_street_address_2': + case 'address_valid_from': + case 'address_postcode_label': + case 'address_postcode_code': + case 'address_country_name': + case 'address_country_code': + case 'address_isnoaddress': + + $qb->addSelect(sprintf( + 'GET_PERSON_ADDRESS_%s(person.id, :address_date) AS %s', + // get the part after address_ + strtoupper(substr($f, 8)), + $f)); + $qb->setParameter('address_date', $data['address_date']); + break; + default: + $qb->addSelect(sprintf('person.%s as %s', $f, $f)); + } + } + } + + foreach ($this->getCustomFields() as $cf) { + $cfType = $this->customFieldProvider->getCustomFieldByType($cf->getType()); + if ($cfType instanceof CustomFieldChoice and $cfType->isMultiple($cf)) { + foreach($cfType->getChoices($cf) as $choiceSlug => $label) { + $slug = $this->slugToDQL($cf->getSlug(), 'choice', [ 'choiceSlug' => $choiceSlug ]); + $qb->addSelect( + sprintf('GET_JSON_FIELD_BY_KEY(person.cFData, :slug%s) AS %s', + $slug, $slug)); + $qb->setParameter(sprintf('slug%s', $slug), $cf->getSlug()); + } + } else { + $slug = $this->slugToDQL($cf->getSlug()); + $qb->addSelect( + sprintf('GET_JSON_FIELD_BY_KEY(person.cFData, :slug%s) AS %s', + $slug, $slug)); + $qb->setParameter(sprintf('slug%s', $slug), $cf->getSlug()); + } + } + + $qb + ->from('ChillPersonBundle:Person', 'person') + ->join('person.center', 'center') + ->andWhere('center IN (:authorized_centers)') + ->setParameter('authorized_centers', $centers); + ; + + + return $qb; + } + + /** + * + * {@inheritDoc} + */ + public function requiredRole() + { + return new Role(PersonVoter::LISTS); + } + + /** + * + * {@inheritDoc} + */ + public function supportsModifiers() + { + return array(Declarations::PERSON_TYPE, Declarations::PERSON_IMPLIED_IN); + } +} diff --git a/src/Bundle/ChillPerson/Export/Filter/AccompanyingPeriodClosingFilter.php b/src/Bundle/ChillPerson/Export/Filter/AccompanyingPeriodClosingFilter.php new file mode 100644 index 000000000..7179de4d3 --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Filter/AccompanyingPeriodClosingFilter.php @@ -0,0 +1,85 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Export\Filter; + +use Chill\MainBundle\Export\FilterInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Doctrine\ORM\QueryBuilder; +use Chill\MainBundle\Form\Type\ChillDateType; +use Doctrine\DBAL\Types\Type; +use Chill\PersonBundle\Export\AbstractAccompanyingPeriodExportElement; + +/** + * + * + */ +class AccompanyingPeriodClosingFilter extends AbstractAccompanyingPeriodExportElement implements FilterInterface +{ + public function addRole() + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $this->addJoinAccompanyingPeriod($qb); + + $clause = $qb->expr()->andX( + $qb->expr()->lte('accompanying_period.closingDate', ':date_to'), + $qb->expr()->gte('accompanying_period.closingDate', ':date_from')); + + $qb->andWhere($clause); + $qb->setParameter('date_from', $data['date_from'], Type::DATE); + $qb->setParameter('date_to', $data['date_to'], Type::DATE); + } + + public function applyOn(): string + { + return 'person'; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('date_from', ChillDateType::class, array( + 'label' => "Having an accompanying period closed after this date", + 'data' => new \DateTime("-1 month"), + )); + + $builder->add('date_to', ChillDateType::class, array( + 'label' => "Having an accompanying period closed before this date", + 'data' => new \DateTime(), + )); + } + + public function describeAction($data, $format = 'string') + { + return [ + "Filtered by accompanying period: persons having an accompanying period" + . " closed between the %date_from% and %date_to%", + [ + '%date_from%' => $data['date_from']->format('d-m-Y'), + '%date_to%' => $data['date_to']->format('d-m-Y') + ] + ]; + } + + public function getTitle(): string + { + return "Filter by accompanying period: closed between two dates"; + } +} diff --git a/src/Bundle/ChillPerson/Export/Filter/AccompanyingPeriodFilter.php b/src/Bundle/ChillPerson/Export/Filter/AccompanyingPeriodFilter.php new file mode 100644 index 000000000..40f7dd501 --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Filter/AccompanyingPeriodFilter.php @@ -0,0 +1,95 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Export\Filter; + +use Chill\MainBundle\Export\FilterInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Doctrine\ORM\QueryBuilder; +use Chill\MainBundle\Form\Type\ChillDateType; +use Doctrine\DBAL\Types\Type; +use Chill\PersonBundle\Export\AbstractAccompanyingPeriodExportElement; + +/** + * + * + */ +class AccompanyingPeriodFilter extends AbstractAccompanyingPeriodExportElement implements FilterInterface +{ + public function addRole() + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $this->addJoinAccompanyingPeriod($qb); + + $clause = $qb->expr()->andX(); + + $clause->add( + $qb->expr()->lte('accompanying_period.openingDate', ':date_to') + ); + $clause->add( + $qb->expr()->orX( + $qb->expr()->gte('accompanying_period.closingDate', ':date_from'), + $qb->expr()->isNull('accompanying_period.closingDate') + ) + ); + + $qb->andWhere($clause); + $qb->setParameter('date_from', $data['date_from'], Type::DATE); + $qb->setParameter('date_to', $data['date_to'], Type::DATE); + } + + public function applyOn(): string + { + return 'person'; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('date_from', ChillDateType::class, array( + 'label' => "Having an accompanying period opened after this date", + 'data' => new \DateTime("-1 month"), + )); + + $builder->add('date_to', ChillDateType::class, array( + 'label' => "Having an accompanying period ending before this date, or " + . "still opened at this date", + 'data' => new \DateTime(), + )); + } + + public function describeAction($data, $format = 'string') + { + return [ + "Filtered by accompanying period: persons having an accompanying period" + . " opened after the %date_from% and closed before the %date_to% (or still opened " + . "at the %date_to%)", + [ + '%date_from%' => $data['date_from']->format('d-m-Y'), + '%date_to%' => $data['date_to']->format('d-m-Y') + ] + ]; + } + + public function getTitle(): string + { + return "Filter by accompanying period: active period"; + } +} diff --git a/src/Bundle/ChillPerson/Export/Filter/AccompanyingPeriodOpeningFilter.php b/src/Bundle/ChillPerson/Export/Filter/AccompanyingPeriodOpeningFilter.php new file mode 100644 index 000000000..26a8818df --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Filter/AccompanyingPeriodOpeningFilter.php @@ -0,0 +1,85 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Export\Filter; + +use Chill\MainBundle\Export\FilterInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Doctrine\ORM\QueryBuilder; +use Chill\MainBundle\Form\Type\ChillDateType; +use Doctrine\DBAL\Types\Type; +use Chill\PersonBundle\Export\AbstractAccompanyingPeriodExportElement; + +/** + * + * + */ +class AccompanyingPeriodOpeningFilter extends AbstractAccompanyingPeriodExportElement implements FilterInterface +{ + public function addRole() + { + return null; + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $this->addJoinAccompanyingPeriod($qb); + + $clause = $qb->expr()->andX( + $qb->expr()->lte('accompanying_period.openingDate', ':date_to'), + $qb->expr()->gte('accompanying_period.openingDate', ':date_from')); + + $qb->andWhere($clause); + $qb->setParameter('date_from', $data['date_from'], Type::DATE); + $qb->setParameter('date_to', $data['date_to'], Type::DATE); + } + + public function applyOn(): string + { + return 'person'; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('date_from', ChillDateType::class, array( + 'label' => "Having an accompanying period opened after this date", + 'data' => new \DateTime("-1 month"), + )); + + $builder->add('date_to', ChillDateType::class, array( + 'label' => "Having an accompanying period opened before this date", + 'data' => new \DateTime(), + )); + } + + public function describeAction($data, $format = 'string') + { + return [ + "Filtered by accompanying period: persons having an accompanying period" + . " opened between the %date_from% and %date_to%", + [ + '%date_from%' => $data['date_from']->format('d-m-Y'), + '%date_to%' => $data['date_to']->format('d-m-Y') + ] + ]; + } + + public function getTitle(): string + { + return "Filter by accompanying period: starting between two dates"; + } +} diff --git a/src/Bundle/ChillPerson/Export/Filter/BirthdateFilter.php b/src/Bundle/ChillPerson/Export/Filter/BirthdateFilter.php new file mode 100644 index 000000000..234d6bd3e --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Filter/BirthdateFilter.php @@ -0,0 +1,130 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Export\Filter; + +use Chill\MainBundle\Export\FilterInterface; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Validator\Constraints; +use Symfony\Component\Form\FormEvent; +use Symfony\Component\Form\FormEvents; +use Doctrine\ORM\Query\Expr; +use Chill\MainBundle\Form\Type\Export\FilterType; +use Symfony\Component\Form\FormError; +use Chill\MainBundle\Export\ExportElementValidatedInterface; + +/** + * + * + * @author Julien Fastré + */ +class BirthdateFilter implements FilterInterface, ExportElementValidatedInterface +{ + + public function addRole() + { + return null; + } + + public function alterQuery(\Doctrine\ORM\QueryBuilder $qb, $data) + { + $where = $qb->getDQLPart('where'); + $clause = $qb->expr()->between('person.birthdate', ':date_from', + ':date_to'); + + if ($where instanceof Expr\Andx) { + $where->add($clause); + } else { + $where = $qb->expr()->andX($clause); + } + + $qb->add('where', $where); + $qb->setParameter('date_from', $data['date_from']); + $qb->setParameter('date_to', $data['date_to']); + } + + public function applyOn() + { + return 'person'; + } + + public function buildForm(\Symfony\Component\Form\FormBuilderInterface $builder) + { + $builder->add('date_from', DateType::class, array( + 'label' => "Born after this date", + 'data' => new \DateTime(), + 'attr' => array('class' => 'datepicker'), + 'widget'=> 'single_text', + 'format' => 'dd-MM-yyyy', + )); + + $builder->add('date_to', DateType::class, array( + 'label' => "Born before this date", + 'data' => new \DateTime(), + 'attr' => array('class' => 'datepicker'), + 'widget'=> 'single_text', + 'format' => 'dd-MM-yyyy', + )); + + } + + public function validateForm($data, ExecutionContextInterface $context) + { + $date_from = $data['date_from']; + $date_to = $data['date_to']; + + if ($date_from === null) { + $context->buildViolation('The "date from" should not be empty') + //->atPath('date_from') + ->addViolation(); + } + + if ($date_to === null) { + $context->buildViolation('The "date to" should not be empty') + //->atPath('date_to') + ->addViolation(); + } + + if ( + ($date_from !== null && $date_to !== null) + && + $date_from >= $date_to + ) { + $context->buildViolation('The date "date to" should be after the ' + . 'date given in "date from" field') + ->addViolation(); + } + } + + public function describeAction($data, $format = 'string') + { + return array('Filtered by person\'s birtdate: ' + . 'between %date_from% and %date_to%', array( + '%date_from%' => $data['date_from']->format('d-m-Y'), + '%date_to%' => $data['date_to']->format('d-m-Y') + )); + } + + public function getTitle() + { + return 'Filter by person\'s birthdate'; + } + +} diff --git a/src/Bundle/ChillPerson/Export/Filter/GenderFilter.php b/src/Bundle/ChillPerson/Export/Filter/GenderFilter.php new file mode 100644 index 000000000..7db76f6a7 --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Filter/GenderFilter.php @@ -0,0 +1,139 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Export\Filter; + +use Chill\MainBundle\Export\FilterInterface; +use Symfony\Component\Form\FormBuilderInterface; +use Chill\PersonBundle\Entity\Person; +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\Query\Expr; +use Symfony\Component\Security\Core\Role\Role; +use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Symfony\Component\Validator\Context\ExecutionContextInterface; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * + * + * @author Julien Fastré + */ +class GenderFilter implements FilterInterface, + ExportElementValidatedInterface +{ + /** + * + * @var TranslatorInterface + */ + protected $translator; + + function __construct(TranslatorInterface $translator) + { + $this->translator = $translator; + } + + public function applyOn() + { + return 'person'; + } + + /** + * + */ + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('accepted_genders', ChoiceType::class, array( + 'choices' => array( + 'Woman' => Person::FEMALE_GENDER, + 'Man' => Person::MALE_GENDER, + 'Both' => Person::BOTH_GENDER, + 'Not given' => 'null' + ), + 'multiple' => true, + 'expanded' => true + )); + } + + public function validateForm($data, ExecutionContextInterface $context) + { + if (!is_array($data['accepted_genders']) || count($data['accepted_genders']) === 0 ) { + $context->buildViolation("You should select an option") + ->addViolation(); + } + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $where = $qb->getDQLPart('where'); + $isIn = $qb->expr()->in('person.gender', ':person_gender'); + + if (!\in_array('null', $data['accepted_genders'])) { + $clause = $isIn; + } else { + $clause = $qb->expr()->orX($isIn, $qb->expr()->isNull('person.gender')); + } + + if ($where instanceof Expr\Andx) { + $where->add($clause); + } else { + $where = $qb->expr()->andX($clause); + } + + $qb->add('where', $where); + $qb->setParameter('person_gender', \array_filter( + $data['accepted_genders'], + function($el) { + return $el !== 'null'; + })); + } + + /** + * A title which will be used in the label for the form + * + * @return string + */ + public function getTitle() + { + return 'Filter by person gender'; + } + + public function addRole() + { + return NULL; + } + + public function describeAction($data, $format = 'string') + { + $genders = []; + + foreach ($data['accepted_genders'] as $g) { + if ('null' === $g) { + $genders[] = $this->translator->trans('Not given'); + } else { + $genders[] = $this->translator->trans($g); + } + } + + return [ + "Filtering by genders: only %genders%", + [ "%genders%" => \implode(", ", $genders)] + ]; + } +} diff --git a/src/Bundle/ChillPerson/Export/Filter/NationalityFilter.php b/src/Bundle/ChillPerson/Export/Filter/NationalityFilter.php new file mode 100644 index 000000000..d6dc52af8 --- /dev/null +++ b/src/Bundle/ChillPerson/Export/Filter/NationalityFilter.php @@ -0,0 +1,111 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Export\Filter; + +use Symfony\Component\Form\FormBuilderInterface; +use Doctrine\ORM\QueryBuilder; +use Chill\MainBundle\Export\FilterInterface; +use Doctrine\ORM\Query\Expr; +use Chill\MainBundle\Templating\TranslatableStringHelper; +use Doctrine\ORM\EntityRepository; +use Chill\MainBundle\Entity\Country; +use Chill\MainBundle\Export\ExportElementValidatedInterface; +use Chill\MainBundle\Form\Type\Select2CountryType; +use Symfony\Component\Validator\Context\ExecutionContextInterface; + +/** + * + * + * @author Julien Fastré + */ +class NationalityFilter implements FilterInterface, + ExportElementValidatedInterface +{ + /** + * + * @var TranslatableStringHelper + */ + private $translatableStringHelper; + + public function __construct(TranslatableStringHelper $helper) + { + $this->translatableStringHelper = $helper; + } + + public function applyOn() + { + return 'person'; + } + + public function buildForm(FormBuilderInterface $builder) + { + $builder->add('nationalities', Select2CountryType::class, array( + 'placeholder' => 'Choose countries' + )); + + } + + public function validateForm($data, ExecutionContextInterface $context) + { + if ($data['nationalities'] === null) { + $context->buildViolation("A nationality must be selected") + ->addViolation(); + } + } + + public function alterQuery(QueryBuilder $qb, $data) + { + $where = $qb->getDQLPart('where'); + $clause = $qb->expr()->in('person.nationality', ':person_nationality'); + + if ($where instanceof Expr\Andx) { + $where->add($clause); + } else { + $where = $qb->expr()->andX($clause); + } + + $qb->add('where', $where); + $qb->setParameter('person_nationality', array($data['nationalities'])); + } + + public function getTitle() + { + return "Filter by person's nationality"; + } + + public function addRole() + { + return NULL; + } + + public function describeAction($data, $format = 'string') + { + $countries = $data['nationalities']; + + $names = array_map(function(Country $c) { + return $this->translatableStringHelper->localize($c->getName()); + }, array($countries)); + + return array( + "Filtered by nationality : %nationalities%", + array('%nationalities%' => implode(", ", $names)) + ); + } +} diff --git a/src/Bundle/ChillPerson/Form/AccompanyingPeriodType.php b/src/Bundle/ChillPerson/Form/AccompanyingPeriodType.php new file mode 100644 index 000000000..2de2011a1 --- /dev/null +++ b/src/Bundle/ChillPerson/Form/AccompanyingPeriodType.php @@ -0,0 +1,132 @@ +config = $config; + } + + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + //if the period_action is close, date opening should not be seen + if ($options['period_action'] !== 'close') { + $builder + ->add('openingDate', DateType::class, [ + "required" => true, + 'widget' => 'single_text', + 'format' => 'dd-MM-yyyy' + ]) + ; + } + + // closingDate should be seen only if + // period_action = close + // OR ( period_action = update AND accompanying period is already closed ) + $accompanyingPeriod = $options['data']; + + if ( + ($options['period_action'] === 'close') + OR + ($options['period_action'] === 'create') + OR + ($options['period_action'] === 'update' AND !$accompanyingPeriod->isOpen()) + ) { + + $builder->add('closingDate', DateType::class, [ + 'required' => true, + 'widget' => 'single_text', + 'format' => 'dd-MM-yyyy' + ]); + + $builder->add('closingMotive', ClosingMotivePickerType::class); + } + + if ($this->config['user'] === 'visible') { + $builder->add('user', UserPickerType::class, [ + 'center' => $options['center'], + 'role' => new Role(PersonVoter::SEE), + ]); + } + + $builder->add('remark', TextareaType::class, [ + 'required' => false + ]); + } + + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => 'Chill\PersonBundle\Entity\AccompanyingPeriod' + ]); + + $resolver + ->setRequired(['period_action']) + ->addAllowedTypes('period_action', 'string') + ->addAllowedValues('period_action', ['update', 'open', 'close', 'create']) + ->setRequired('center') + ->setAllowedTypes('center', Center::class) + ; + } + + /** + * @param FormView $view + * @param FormInterface $form + * @param array $options + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars['action'] = $options['period_action']; + } + + /** + * @return string + */ + public function getBlockPrefix() + { + return 'chill_personbundle_accompanyingperiod'; + } +} diff --git a/src/Bundle/ChillPerson/Form/ChoiceLoader/PersonChoiceLoader.php b/src/Bundle/ChillPerson/Form/ChoiceLoader/PersonChoiceLoader.php new file mode 100644 index 000000000..b1cdfee7a --- /dev/null +++ b/src/Bundle/ChillPerson/Form/ChoiceLoader/PersonChoiceLoader.php @@ -0,0 +1,138 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Form\ChoiceLoader; + +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\ChoiceListInterface; +use Doctrine\ORM\EntityRepository; +use Chill\PersonBundle\Entity\Person; + +/** + * Class PersonChoiceLoader + * + * @package Chill\PersonBundle\Form\ChoiceLoader + * @author Julien Fastré + */ +class PersonChoiceLoader implements ChoiceLoaderInterface +{ + /** + * @var EntityRepository + */ + protected $personRepository; + + /** + * @var array + */ + protected $lazyLoadedPersons = []; + + /** + * @var array + */ + protected $centers = []; + + /** + * PersonChoiceLoader constructor. + * + * @param EntityRepository $personRepository + * @param array|null $centers + */ + public function __construct( + EntityRepository $personRepository, + array $centers = null + ) { + $this->personRepository = $personRepository; + if (NULL !== $centers) { + $this->centers = $centers; + } + } + + /** + * @return bool + */ + protected function hasCenterFilter() + { + return count($this->centers) > 0; + } + + /** + * @param null $value + * @return ChoiceListInterface + */ + public function loadChoiceList($value = null): ChoiceListInterface + { + $list = new \Symfony\Component\Form\ChoiceList\ArrayChoiceList( + $this->lazyLoadedPersons, + function(Person $p) use ($value) { + return \call_user_func($value, $p); + }); + + return $list; + } + + /** + * @param array $values + * @param null $value + * @return array + */ + public function loadChoicesForValues(array $values, $value = null) + { + $choices = []; + + foreach($values as $value) { + if (empty($value)) { + continue; + } + + $person = $this->personRepository->find($value); + + if ($this->hasCenterFilter() && + !\in_array($person->getCenter(), $this->centers)) { + throw new \RuntimeException("chosen a person not in correct center"); + } + + $choices[] = $person; + } + + return $choices; + } + + /** + * @param array $choices + * @param null $value + * @return array|string[] + */ + 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->lazyLoadedPersons[$id] = $choice; + } + + return $values; + } +} diff --git a/src/Bundle/ChillPerson/Form/ClosingMotiveType.php b/src/Bundle/ChillPerson/Form/ClosingMotiveType.php new file mode 100644 index 000000000..672fc4f19 --- /dev/null +++ b/src/Bundle/ChillPerson/Form/ClosingMotiveType.php @@ -0,0 +1,76 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Form; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Chill\PersonBundle\Entity\AccompanyingPeriod\ClosingMotive; +use Chill\PersonBundle\Form\Type\ClosingMotivePickerType; +use Chill\MainBundle\Form\Type\TranslatableStringFormType; +use Symfony\Component\Form\Extension\Core\Type\CheckboxType; +use Symfony\Component\Form\Extension\Core\Type\NumberType; + +/** + * Class ClosingMotiveType + * + * @package Chill\PersonBundle\Form + */ +class ClosingMotiveType extends AbstractType +{ + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('name', TranslatableStringFormType::class, [ + 'label' => 'Nom' + ]) + ->add('active', CheckboxType::class, [ + 'label' => 'Actif ?', + 'required' => false + ]) + ->add('ordering', NumberType::class, [ + 'label' => 'Ordre d\'apparition', + 'required' => true, + 'scale' => 5 + ]) + ->add('parent', ClosingMotivePickerType::class, [ + 'label' => 'Parent', + 'required' => false, + 'placeholder' => 'closing_motive.any parent', + 'multiple' => false, + 'only_leaf' => false + ]) + ; + } + + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefault('class', ClosingMotive::class) + ; + } +} + \ No newline at end of file diff --git a/src/Bundle/ChillPerson/Form/CreationPersonType.php b/src/Bundle/ChillPerson/Form/CreationPersonType.php new file mode 100644 index 000000000..ec3ddbf82 --- /dev/null +++ b/src/Bundle/ChillPerson/Form/CreationPersonType.php @@ -0,0 +1,159 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Form; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer; +use Symfony\Component\Form\Extension\Core\Type\HiddenType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Chill\MainBundle\Form\Type\CenterType; +use Chill\PersonBundle\Form\Type\GenderType; +use Chill\MainBundle\Form\Type\DataTransformer\CenterTransformer; +use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; +use Chill\PersonBundle\Form\Type\PersonAltNameType; + +class CreationPersonType extends AbstractType +{ + + const NAME = 'chill_personbundle_person_creation'; + + const FORM_NOT_REVIEWED = 'not_reviewed'; + const FORM_REVIEWED = 'reviewed' ; + const FORM_BEING_REVIEWED = 'being_reviewed'; + + /** + * + * @var CenterTransformer + */ + private $centerTransformer; + + /** + * + * @var ConfigPersonAltNamesHelper + */ + protected $configPersonAltNamesHelper; + + public function __construct( + CenterTransformer $centerTransformer, + ConfigPersonAltNamesHelper $configPersonAltNamesHelper + ) { + $this->centerTransformer = $centerTransformer; + $this->configPersonAltNamesHelper = $configPersonAltNamesHelper; + } + + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + if ($options['form_status'] === self::FORM_BEING_REVIEWED) { + + $dateToStringTransformer = new DateTimeToStringTransformer( + null, null, 'd-m-Y', false); + + $builder->add('firstName', HiddenType::class) + ->add('lastName', HiddenType::class) + ->add('birthdate', HiddenType::class, array( + 'property_path' => 'birthdate' + )) + ->add('gender', HiddenType::class) + ->add('creation_date', HiddenType::class, array( + 'mapped' => false + )) + ->add('form_status', HiddenType::class, array( + 'mapped' => false, + 'data' => $options['form_status'] + )) + ->add('center', HiddenType::class) + ; + + if ($this->configPersonAltNamesHelper->hasAltNames()) { + $builder->add('altNames', PersonAltNameType::class, [ + 'by_reference' => false, + 'force_hidden' => true + ]); + } + + $builder->get('birthdate') + ->addModelTransformer($dateToStringTransformer); + $builder->get('creation_date') + ->addModelTransformer($dateToStringTransformer); + $builder->get('center') + ->addModelTransformer($this->centerTransformer); + } else { + $builder + ->add('firstName') + ->add('lastName') + ->add('birthdate', DateType::class, array('required' => false, + 'widget' => 'single_text', 'format' => 'dd-MM-yyyy')) + ->add('gender', GenderType::class, array( + 'required' => true, 'placeholder' => null + )) + ->add('creation_date', DateType::class, array( + 'required' => true, + 'widget' => 'single_text', + 'format' => 'dd-MM-yyyy', + 'mapped' => false, + 'data' => new \DateTime())) + ->add('form_status', HiddenType::class, array( + 'data' => $options['form_status'], + 'mapped' => false + )) + ->add('center', CenterType::class) + ; + + if ($this->configPersonAltNamesHelper->hasAltNames()) { + $builder->add('altNames', PersonAltNameType::class, [ + 'by_reference' => false + ]); + } + } + } + + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => 'Chill\PersonBundle\Entity\Person' + )); + + $resolver->setRequired('form_status') + ->setAllowedValues('form_status', array( + self::FORM_BEING_REVIEWED, + self::FORM_NOT_REVIEWED, + self::FORM_REVIEWED + )); + } + + /** + * @return string + */ + public function getBlockPrefix() + { + return self::NAME; + } +} diff --git a/src/Bundle/ChillPerson/Form/DataMapper/PersonAltNameDataMapper.php b/src/Bundle/ChillPerson/Form/DataMapper/PersonAltNameDataMapper.php new file mode 100644 index 000000000..d1f50404b --- /dev/null +++ b/src/Bundle/ChillPerson/Form/DataMapper/PersonAltNameDataMapper.php @@ -0,0 +1,87 @@ +getIterator() as $key => $altName) { + /** @var PersonAltName $altName */ + $mapIndexToKey[$altName->getKey()] = $key; + } + + foreach ($forms as $key => $form) { + if (\array_key_exists($key, $mapIndexToKey)) { + $form->setData($viewData->get($mapIndexToKey[$key])->getLabel()); + } + } + } + + /** + * + * @param FormInterface[] $forms + * @param Collection $viewData + */ + public function mapFormsToData($forms, &$viewData) + { + $mapIndexToKey = []; + + if (is_array($viewData)) { + $dataIterator = $viewData; + } else { + $dataIterator = $viewData instanceof ArrayCollection ? + $viewData->toArray() : $viewData->getIterator(); + } + + foreach ($dataIterator as $key => $altName) { + /** @var PersonAltName $altName */ + $mapIndexToKey[$altName->getKey()] = $key; + } + + foreach ($forms as $key => $form) { + $isEmpty = empty($form->getData()); + + if (\array_key_exists($key, $mapIndexToKey)) { + if ($isEmpty) { + $viewData->remove($mapIndexToKey[$key]); + } else { + $viewData->get($mapIndexToKey[$key])->setLabel($form->getData()); + } + } else { + if (!$isEmpty) { + $altName = (new PersonAltName()) + ->setKey($key) + ->setLabel($form->getData()) + ; + + if (is_array($viewData)) { + $viewData[] = $altName; + } else { + $viewData->add($altName); + } + } + } + } + + } +} diff --git a/src/Bundle/ChillPerson/Form/DataTransformer/PersonToIdTransformer.php b/src/Bundle/ChillPerson/Form/DataTransformer/PersonToIdTransformer.php new file mode 100644 index 000000000..2561019d5 --- /dev/null +++ b/src/Bundle/ChillPerson/Form/DataTransformer/PersonToIdTransformer.php @@ -0,0 +1,70 @@ +om = $om; + } + + /** + * Transforms an object (issue) to a string (id). + * + * @param Person|null $issue + * @return string + */ + public function transform($issue) + { + if (null === $issue) { + return ""; + } + + return $issue->getId(); + } + + /** + * Transforms a string (id) to an object (issue). + * + * @param string $id + * + * @return Person|null + * + * @throws TransformationFailedException if object (issue) is not found. + */ + public function reverseTransform($id) + { + if (!$id) { + return null; + } + + $issue = $this->om + ->getRepository('ChillPersonBundle:Person') + ->findOneBy(array('id' => $id)) + ; + + if (null === $issue) { + throw new TransformationFailedException(sprintf( + 'An issue with id "%s" does not exist!', + $id + )); + } + + return $issue; + } +} +?> \ No newline at end of file diff --git a/src/Bundle/ChillPerson/Form/MaritalStatusType.php b/src/Bundle/ChillPerson/Form/MaritalStatusType.php new file mode 100644 index 000000000..1897c7dc4 --- /dev/null +++ b/src/Bundle/ChillPerson/Form/MaritalStatusType.php @@ -0,0 +1,45 @@ +add('id', TextType::class, [ + 'label' => 'Identifiant' + ]) + ->add('name', TranslatableStringFormType::class, [ + 'label' => 'Nom' + ]) + ; + } + + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefault('class', MaritalStatus::class) + ; + } +} \ No newline at end of file diff --git a/src/Bundle/ChillPerson/Form/PersonType.php b/src/Bundle/ChillPerson/Form/PersonType.php new file mode 100644 index 000000000..7cf24a836 --- /dev/null +++ b/src/Bundle/ChillPerson/Form/PersonType.php @@ -0,0 +1,175 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Form; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Form\Extension\Core\Type\TextareaType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\DateType; +use Symfony\Component\Form\Extension\Core\Type\EmailType; +use Symfony\Component\Form\Extension\Core\Type\TelType; +use Chill\PersonBundle\Form\Type\GenderType; +use Chill\MainBundle\Form\Type\Select2CountryType; +use Chill\MainBundle\Form\Type\Select2LanguageType; +use Chill\CustomFieldsBundle\Form\Type\CustomFieldType; +use Chill\PersonBundle\Form\Type\Select2MaritalStatusType; +use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; +use Chill\PersonBundle\Form\Type\PersonAltNameType; + +class PersonType extends AbstractType +{ + /** + * array of configuration for person_fields. + * + * Contains whether we should add fields some optional fields (optional per + * instance) + * + * @var string[] + */ + protected $config = array(); + + /** + * + * @var ConfigPersonAltNamesHelper + */ + protected $configAltNamesHelper; + + /** + * + * @param string[] $personFieldsConfiguration configuration of visibility of some fields + */ + public function __construct( + array $personFieldsConfiguration, + ConfigPersonAltNamesHelper $configAltNamesHelper + ) { + $this->config = $personFieldsConfiguration; + $this->configAltNamesHelper = $configAltNamesHelper; + } + + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder + ->add('firstName') + ->add('lastName') + ->add('birthdate', DateType::class, array('required' => false, 'widget' => 'single_text', 'format' => 'dd-MM-yyyy')) + ->add('gender', GenderType::class, array( + 'required' => true + )); + + if ($this->configAltNamesHelper->hasAltNames()) { + $builder->add('altNames', PersonAltNameType::class, [ + 'by_reference' => false + ]); + } + + if ($this->config['memo'] === 'visible') { + $builder + ->add('memo', TextareaType::class, array('required' => false)) + ; + } + + if ($this->config['place_of_birth'] === 'visible') { + $builder->add('placeOfBirth', TextType::class, array('required' => false)); + } + + if ($this->config['contact_info'] === 'visible') { + $builder->add('contactInfo', TextareaType::class, array('required' => false)); + } + + if ($this->config['phonenumber'] === 'visible') { + $builder->add('phonenumber', TelType::class, array('required' => false)); + } + + if ($this->config['mobilenumber'] === 'visible') { + $builder->add('mobilenumber', TelType::class, array('required' => false)); + } + + if ($this->config['email'] === 'visible') { + $builder->add('email', EmailType::class, array('required' => false)); + } + + if ($this->config['country_of_birth'] === 'visible') { + $builder->add('countryOfBirth', Select2CountryType::class, array( + 'required' => false + )); + } + + if ($this->config['nationality'] === 'visible') { + $builder->add('nationality', Select2CountryType::class, array( + 'required' => false + )); + } + + if ($this->config['spoken_languages'] === 'visible') { + $builder->add('spokenLanguages', Select2LanguageType::class, array( + 'required' => false, + 'multiple' => true + )); + } + + if ($this->config['marital_status'] === 'visible'){ + $builder->add('maritalStatus', Select2MaritalStatusType::class, array( + 'required' => false + )); + } + + if($options['cFGroup']) { + $builder + ->add('cFData', CustomFieldType::class, + array('attr' => array('class' => 'cf-fields'), 'group' => $options['cFGroup'])) + ; + } + } + + /** + * @param OptionsResolverInterface $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults(array( + 'data_class' => 'Chill\PersonBundle\Entity\Person', + 'validation_groups' => array('general', 'creation') + )); + + $resolver->setRequired(array( + 'cFGroup' + )); + + $resolver->setAllowedTypes( + 'cFGroup', array('null', 'Chill\CustomFieldsBundle\Entity\CustomFieldsGroup') + ); + } + + /** + * @return string + */ + public function getBlockPrefix() + { + return 'chill_personbundle_person'; + } +} diff --git a/src/Bundle/ChillPerson/Form/Type/ClosingMotivePickerType.php b/src/Bundle/ChillPerson/Form/Type/ClosingMotivePickerType.php new file mode 100644 index 000000000..aa7e47583 --- /dev/null +++ b/src/Bundle/ChillPerson/Form/Type/ClosingMotivePickerType.php @@ -0,0 +1,98 @@ +translatableStringHelper = $translatableStringHelper; + $this->entityRenderExtension = $chillEntityRenderExtension; + $this->repository = $closingMotiveRepository; + } + + /** + * @return string + */ + public function getBlockPrefix() + { + return 'closing_motive'; + } + + /** + * @return null|string + */ + public function getParent() + { + return EntityType::class; + } + + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + + $resolver->setDefaults([ + 'class' => ClosingMotive::class, + 'empty_data' => null, + 'placeholder' => 'Choose a motive', + 'choice_label' => function(ClosingMotive $cm) { + return $this->entityRenderExtension->renderString($cm); + }, + 'only_leaf' => true + ]); + + $resolver + ->setAllowedTypes('only_leaf', 'bool') + ->setNormalizer('choices', function (Options $options) { + return $this->repository + ->getActiveClosingMotive($options['only_leaf']); + }) + ; + } + +} diff --git a/src/Bundle/ChillPerson/Form/Type/GenderType.php b/src/Bundle/ChillPerson/Form/Type/GenderType.php new file mode 100644 index 000000000..bdd31e899 --- /dev/null +++ b/src/Bundle/ChillPerson/Form/Type/GenderType.php @@ -0,0 +1,38 @@ + Person::MALE_GENDER, + Person::FEMALE_GENDER => Person::FEMALE_GENDER, + Person::BOTH_GENDER => Person::BOTH_GENDER + ); + + $resolver->setDefaults(array( + 'choices' => $a, + 'expanded' => true, + 'multiple' => false, + 'placeholder' => null + )); + } + +} diff --git a/src/Bundle/ChillPerson/Form/Type/PersonAltNameType.php b/src/Bundle/ChillPerson/Form/Type/PersonAltNameType.php new file mode 100644 index 000000000..1ba58e86e --- /dev/null +++ b/src/Bundle/ChillPerson/Form/Type/PersonAltNameType.php @@ -0,0 +1,76 @@ +configHelper = $configHelper; + $this->translatableStringHelper = $translatableStringHelper; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + foreach ($this->getKeyChoices() as $label => $key) { + $builder->add( + $key, + $options['force_hidden'] ? HiddenType::class : TextType::class, [ + 'label' => $label, + 'required' => false + ]); + } + + $builder->setDataMapper(new \Chill\PersonBundle\Form\DataMapper\PersonAltNameDataMapper()); + } + + protected function getKeyChoices() + { + $choices = $this->configHelper->getChoices(); + $translatedChoices = []; + + foreach ($choices as $key => $labels) { + $label = $this->translatableStringHelper->localize($labels); + $translatedChoices[$label] = $key; + } + + return $translatedChoices; + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver + ->setDefault('class', \Chill\PersonBundle\Entity\PersonAltName::class) + ->setDefined('force_hidden') + ->setAllowedTypes('force_hidden', 'bool') + ->setDefault('force_hidden', false) + ; + } + +} diff --git a/src/Bundle/ChillPerson/Form/Type/PickPersonType.php b/src/Bundle/ChillPerson/Form/Type/PickPersonType.php new file mode 100644 index 000000000..839c84b69 --- /dev/null +++ b/src/Bundle/ChillPerson/Form/Type/PickPersonType.php @@ -0,0 +1,191 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Form\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\Role\Role; +use Symfony\Bridge\Doctrine\Form\Type\EntityType; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Chill\MainBundle\Entity\GroupCenter; +use Chill\PersonBundle\Entity\Person; +use Chill\MainBundle\Security\Authorization\AuthorizationHelper; +use Chill\MainBundle\Entity\Center; +use Chill\PersonBundle\Repository\PersonRepository; +use Chill\PersonBundle\Search\PersonSearch; +use Symfony\Component\Translation\TranslatorInterface; +use Chill\PersonBundle\Form\ChoiceLoader\PersonChoiceLoader; +use Symfony\Component\OptionsResolver\Options; + +/** + * This type allow to pick a person. + * + * The form is embedded in a select2 input. + * + * The people may be filtered : + * + * - with the `centers` option, only the people associated with the given center(s) + * are seen. May be an instance of `Chill\MainBundle\Entity\Center`, or an array of + * `Chill\MainBundle\Entity\Center`. By default, all the reachable centers as selected. + * - with the `role` option, only the people belonging to the reachable center for the + * given role are displayed. + * + * + * @author Julien Fastré + */ +class PickPersonType extends AbstractType +{ + /** + * @var PersonRepository + */ + protected $personRepository; + + /** + * + * @var \Chill\MainBundle\Entity\User + */ + protected $user; + + /** + * + * @var AuthorizationHelper + */ + protected $authorizationHelper; + + /** + * + * @var UrlGeneratorInterface + */ + protected $urlGenerator; + + /** + * + * @var TranslatorInterface + */ + protected $translator; + + public function __construct( + PersonRepository $personRepository, + TokenStorageInterface $tokenStorage, + AuthorizationHelper $authorizationHelper, + UrlGeneratorInterface $urlGenerator, + TranslatorInterface $translator + ) + { + $this->personRepository = $personRepository; + $this->user = $tokenStorage->getToken()->getUser(); + $this->authorizationHelper = $authorizationHelper; + $this->urlGenerator = $urlGenerator; + $this->translator = $translator; + } + + protected function filterCentersfom(Options $options) + { + if ($options['role'] === NULL) { + $centers = array_map(function (GroupCenter $g) { + + return $g->getCenter(); + }, $this->user->getGroupCenters()->toArray()); + } else { + $centers = $this->authorizationHelper + ->getReachableCenters($this->user, $options['role']); + } + + if ($options['centers'] === NULL) { + // we select all selected centers + $selectedCenters = $centers; + } else { + $selectedCenters = array(); + $optionsCenters = is_array($options['centers']) ? + $options['centers'] : array($options['centers']); + + foreach ($optionsCenters as $c) { + // check that every member of the array is a center + if (!$c instanceof Center) { + throw new \RuntimeException('Every member of the "centers" ' + . 'option must be an instance of '.Center::class); + } + if (!in_array($c->getId(), array_map( + function(Center $c) { return $c->getId();}, + $centers))) { + throw new AccessDeniedException('The given center is not reachable'); + } + $selectedCenters[] = $c; + } + } + + return $selectedCenters; + } + + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + + // add the possibles options for this type + $resolver->setDefined('centers') + ->addAllowedTypes('centers', array('array', Center::class, 'null')) + ->setDefault('centers', null) + ->setDefined('role') + ->addAllowedTypes('role', array(Role::class, 'null')) + ->setDefault('role', null) + ; + + // add the default options + $resolver->setDefaults(array( + 'class' => Person::class, + 'choice_label' => function(Person $p) { + return $p->getFirstname().' '.$p->getLastname(); + }, + 'placeholder' => 'Pick a person', + 'choice_attr' => function(Person $p) { + return array( + 'data-center' => $p->getCenter()->getId() + ); + }, + 'attr' => array('class' => 'select2 '), + 'choice_loader' => function(Options $options) { + $centers = $this->filterCentersfom($options); + + return new PersonChoiceLoader($this->personRepository, $centers); + } + )); + } + + public function getParent() + { + return EntityType::class; + } + + public function buildView(\Symfony\Component\Form\FormView $view, \Symfony\Component\Form\FormInterface $form, array $options) + { + $view->vars['attr']['data-person-picker'] = true; + $view->vars['attr']['data-select-interactive-loading'] = true; + $view->vars['attr']['data-search-url'] = $this->urlGenerator + ->generate('chill_main_search', [ 'name' => PersonSearch::NAME, '_format' => 'json' ]); + $view->vars['attr']['data-placeholder'] = $this->translator->trans($options['placeholder']); + $view->vars['attr']['data-no-results-label'] = $this->translator->trans('select2.no_results'); + $view->vars['attr']['data-error-load-label'] = $this->translator->trans('select2.error_loading'); + $view->vars['attr']['data-searching-label'] = $this->translator->trans('select2.searching'); + } + +} diff --git a/src/Bundle/ChillPerson/Form/Type/Select2MaritalStatusType.php b/src/Bundle/ChillPerson/Form/Type/Select2MaritalStatusType.php new file mode 100644 index 000000000..0cff8ee51 --- /dev/null +++ b/src/Bundle/ChillPerson/Form/Type/Select2MaritalStatusType.php @@ -0,0 +1,81 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Form\Type; + +use Symfony\Component\Form\AbstractType; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Chill\MainBundle\Form\Type\DataTransformer\ObjectToIdTransformer; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Doctrine\Persistence\ObjectManager; +use Chill\MainBundle\Form\Type\Select2ChoiceType; + +/** + * A type to select the marital status + * + * @author Champs-Libres COOP + */ +class Select2MaritalStatusType extends AbstractType +{ + /** @var RequestStack */ + private $requestStack; + + /** @var ObjectManager */ + private $em; + + public function __construct(RequestStack $requestStack,ObjectManager $em) + { + $this->requestStack = $requestStack; + $this->em = $em; + } + + public function getBlockPrefix() { + return 'select2_chill_marital_status'; + } + + public function getParent() { + return Select2ChoiceType::class; + } + + public function buildForm(FormBuilderInterface $builder, array $options) + { + $transformer = new ObjectToIdTransformer($this->em,'Chill\PersonBundle\Entity\MaritalStatus'); + $builder->addModelTransformer($transformer); + } + + public function configureOptions(OptionsResolver $resolver) + { + $locale = $this->requestStack->getCurrentRequest()->getLocale(); + $maritalStatuses = $this->em->getRepository('Chill\PersonBundle\Entity\MaritalStatus')->findAll(); + $choices = array(); + + foreach ($maritalStatuses as $ms) { + $choices[$ms->getId()] = $ms->getName()[$locale]; + } + + asort($choices, SORT_STRING | SORT_FLAG_CASE); + + $resolver->setDefaults(array( + 'class' => 'Chill\PersonBundle\Entity\MaritalStatus', + 'choices' => array_combine(array_values($choices),array_keys($choices)) + )); + } +} diff --git a/src/Bundle/ChillPerson/LICENSE b/src/Bundle/ChillPerson/LICENSE new file mode 100644 index 000000000..2def0e883 --- /dev/null +++ b/src/Bundle/ChillPerson/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/src/Bundle/ChillPerson/Menu/AdminMenuBuilder.php b/src/Bundle/ChillPerson/Menu/AdminMenuBuilder.php new file mode 100644 index 000000000..e8a73d2ab --- /dev/null +++ b/src/Bundle/ChillPerson/Menu/AdminMenuBuilder.php @@ -0,0 +1,61 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Menu; + +use Chill\MainBundle\Routing\LocalMenuBuilderInterface; +use Knp\Menu\MenuItem; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Chill\PersonBundle\Security\Authorization\PersonVoter; + +/** + * + * + */ +class AdminMenuBuilder implements LocalMenuBuilderInterface +{ + /** + * + * @var AuthorizationCheckerInterface + */ + protected $authorizationChecker; + + public function __construct(AuthorizationCheckerInterface $authorizationChecker) + { + $this->authorizationChecker = $authorizationChecker; + } + + + public function buildMenu($menuId, MenuItem $menu, array $parameters) + { + if (!$this->authorizationChecker->isGranted('ROLE_ADMIN')) { + return; + } + + $menu->addChild('Person', [ + 'route' => 'chill_person_admin' + ]) + ->setExtras([ + 'order' => 20 + ]); + } + + public static function getMenuIds(): array + { + return [ 'admin_section' ]; + } +} diff --git a/src/Bundle/ChillPerson/Menu/PersonMenuBuilder.php b/src/Bundle/ChillPerson/Menu/PersonMenuBuilder.php new file mode 100644 index 000000000..3a757d898 --- /dev/null +++ b/src/Bundle/ChillPerson/Menu/PersonMenuBuilder.php @@ -0,0 +1,84 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Menu; + +use Chill\MainBundle\Routing\LocalMenuBuilderInterface; +use Knp\Menu\MenuItem; +use Symfony\Contracts\Translation\TranslatorInterface; + +/** + * Add menu entrie to person menu. + * + * Menu entries added : + * + * - person details ; + * - accompanying period (if `visible`) + * + */ +class PersonMenuBuilder implements LocalMenuBuilderInterface +{ + /** + * + * @var string 'visible' or 'hidden' + */ + protected $showAccompanyingPeriod; + + /** + * + * @var TranslatorInterface + */ + protected $translator; + + public function __construct( + $showAccompanyingPeriod, + TranslatorInterface $translator + ) { + $this->showAccompanyingPeriod = $showAccompanyingPeriod; + $this->translator = $translator; + } + + public function buildMenu($menuId, MenuItem $menu, array $parameters) + { + $menu->addChild($this->translator->trans('Person details'), [ + 'route' => 'chill_person_view', + 'routeParameters' => [ + 'person_id' => $parameters['person']->getId() + ] + ]) + ->setExtras([ + 'order' => 50 + ]); + + if ($this->showAccompanyingPeriod === 'visible') { + $menu->addChild($this->translator->trans('Accompanying period list'), [ + 'route' => 'chill_person_accompanying_period_list', + 'routeParameters' => [ + 'person_id' => $parameters['person']->getId() + ] + ]) + ->setExtras([ + 'order' => 100 + ]); + } + } + + public static function getMenuIds(): array + { + return [ 'person' ]; + } +} diff --git a/src/Bundle/ChillPerson/Menu/SectionMenuBuilder.php b/src/Bundle/ChillPerson/Menu/SectionMenuBuilder.php new file mode 100644 index 000000000..ea6a1d060 --- /dev/null +++ b/src/Bundle/ChillPerson/Menu/SectionMenuBuilder.php @@ -0,0 +1,83 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Menu; + +use Chill\MainBundle\Routing\LocalMenuBuilderInterface; +use Knp\Menu\MenuItem; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Chill\PersonBundle\Security\Authorization\PersonVoter; +use Symfony\Component\Translation\TranslatorInterface; + +/** + * Class SectionMenuBuilder + * + * @package Chill\PersonBundle\Menu + * @author Julien Fastré + */ +class SectionMenuBuilder implements LocalMenuBuilderInterface +{ + /** + * @var AuthorizationCheckerInterface + */ + protected $authorizationChecker; + + /** + * @var TranslatorInterface + */ + protected $translator; + + /** + * SectionMenuBuilder constructor. + * + * @param AuthorizationCheckerInterface $authorizationChecker + * @param TranslatorInterface $translator + */ + public function __construct(AuthorizationCheckerInterface $authorizationChecker, TranslatorInterface $translator) + { + $this->authorizationChecker = $authorizationChecker; + $this->translator = $translator; + } + + /** + * @param $menuId + * @param MenuItem $menu + * @param array $parameters + */ + public function buildMenu($menuId, MenuItem $menu, array $parameters) + { + if ($this->authorizationChecker->isGranted(PersonVoter::CREATE)) { + $menu->addChild($this->translator->trans('Add a person'), [ + 'route' => 'chill_person_new' + ]) + ->setExtras([ + 'order' => 10, + 'icons' => [ 'plus' ] + ]); + } + } + + /** + * @return array + */ + public static function getMenuIds(): array + { + return [ 'section' ]; + } +} diff --git a/src/Bundle/ChillPerson/Privacy/PrivacyEvent.php b/src/Bundle/ChillPerson/Privacy/PrivacyEvent.php new file mode 100644 index 000000000..65304f96a --- /dev/null +++ b/src/Bundle/ChillPerson/Privacy/PrivacyEvent.php @@ -0,0 +1,110 @@ +, + * + * 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 . + */ + +use Symfony\Component\EventDispatcher\Event; +use Chill\PersonBundle\Entity\Person; + +/** + * Class PrivacyEvent + * + * Array $args expects arguments with the following keys: 'element_class', 'element_id', 'action' + * By default, action is set to 'show' + * + * @package Chill\PersonBundle\Privacy + */ +class PrivacyEvent extends Event +{ + const PERSON_PRIVACY_EVENT = 'chill_person.privacy_event'; + + /** + * @var Person + */ + private $person; + + /** + * @var array + */ + private $args; + + /** + * @var array + */ + private $persons; + + /** + * PrivacyEvent constructor. + * + * @param Person $person + * @param array $args + */ + public function __construct(Person $person, array $args = array('action' => 'show')) + { + $this->person = $person; + $this->args = $args; + $this->persons = array(); + } + + /** + * @return Person + */ + public function getPerson() + { + return $this->person; + } + + /** + * @param Person $person + */ + public function addPerson(Person $person) + { + $this->persons[] = $person; + + return $this; + } + + /** + * @return array $persons + */ + public function getPersons() + { + return $this->persons; + } + + /** + * @return bool + */ + public function hasPersons() + { + return count($this->persons) >= 1; + } + + /** + * @return array + */ + public function getArgs() + { + return $this->args; + } + +} \ No newline at end of file diff --git a/src/Bundle/ChillPerson/Privacy/PrivacyEventSubscriber.php b/src/Bundle/ChillPerson/Privacy/PrivacyEventSubscriber.php new file mode 100644 index 000000000..b6cc50e54 --- /dev/null +++ b/src/Bundle/ChillPerson/Privacy/PrivacyEventSubscriber.php @@ -0,0 +1,89 @@ +, + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +use Psr\Log\LoggerInterface; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Chill\PersonBundle\Entity\Person; + +class PrivacyEventSubscriber implements EventSubscriberInterface +{ + + /** + * @var LoggerInterface + */ + protected $logger; + + /** + * @var TokenStorageInterface + */ + protected $token; + + /** + * PrivacyEventSubscriber constructor. + * + * @param LoggerInterface $logger + */ + public function __construct(LoggerInterface $logger, TokenStorageInterface $token) + { + $this->logger = $logger; + $this->token = $token; + } + + public static function getSubscribedEvents() + { + return array(PrivacyEvent::PERSON_PRIVACY_EVENT => array( + array('onPrivacyEvent') + )); + } + + public function onPrivacyEvent(PrivacyEvent $event) + { + $persons = array(); + + if ($event->hasPersons() === true) { + foreach ($event->getPersons() as $person) { + $persons[] = $person->getId(); + } + } + + $involved = array( + 'by_user' => $this->token->getToken()->getUser()->getUsername(), + 'by_user_id' => $this->token->getToken()->getUser()->getId(), + 'person_id' => $event->getPerson()->getId(), + ); + + if ($event->hasPersons()) { + $involved['persons'] = \array_map( + function(Person $p) { return $p->getId(); }, + $event->getPersons() + ); + } + + $this->logger->notice( + "[Privacy Event] A Person Folder has been viewed", + array_merge($involved, $event->getArgs()) + ); + } +} \ No newline at end of file diff --git a/src/Bundle/ChillPerson/README.md b/src/Bundle/ChillPerson/README.md new file mode 100644 index 000000000..4412c2d2b --- /dev/null +++ b/src/Bundle/ChillPerson/README.md @@ -0,0 +1,9 @@ +ChillPersonBundle +================= + +The chill bundle for dealing with persons + +Documentation & installation +---------------------------- + +Read documentation here : http://chill.readthedocs.org diff --git a/src/Bundle/ChillPerson/Repository/ClosingMotiveRepository.php b/src/Bundle/ChillPerson/Repository/ClosingMotiveRepository.php new file mode 100644 index 000000000..c3519796d --- /dev/null +++ b/src/Bundle/ChillPerson/Repository/ClosingMotiveRepository.php @@ -0,0 +1,59 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace Chill\PersonBundle\Repository; + +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\Query\ResultSetMappingBuilder; + +/** + * Class ClosingMotiveRepository + * Entity repository for closing motives + * + * @package Chill\PersonBundle\Repository + */ +class ClosingMotiveRepository extends EntityRepository +{ + /** + * @param bool $onlyLeaf + * @return mixed + */ + public function getActiveClosingMotive(bool $onlyLeaf = true) + { + $rsm = new ResultSetMappingBuilder($this->getEntityManager()); + $rsm->addRootEntityFromClassMetadata($this->getClassName(), 'cm'); + + $sql = "SELECT ".(string) $rsm." + FROM chill_person_closingmotive AS cm + WHERE + active IS TRUE "; + + if ($onlyLeaf) { + $sql .= "AND cm.id NOT IN ( + SELECT DISTINCT parent_id FROM chill_person_closingmotive WHERE parent_id IS NOT NULL + )"; + } + + $sql .= " ORDER BY cm.ordering ASC"; + + return $this->_em + ->createNativeQuery($sql, $rsm) + ->getResult() + ; + } +} diff --git a/src/Bundle/ChillPerson/Repository/PersonAltNameRepository.php b/src/Bundle/ChillPerson/Repository/PersonAltNameRepository.php new file mode 100644 index 000000000..315cee94f --- /dev/null +++ b/src/Bundle/ChillPerson/Repository/PersonAltNameRepository.php @@ -0,0 +1,13 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Chill\PersonBundle\Repository; + +use Doctrine\ORM\EntityRepository; +use Doctrine\ORM\QueryBuilder; + +/** + * Class PersonRepository + * + * @package Chill\PersonBundle\Repository + */ +class PersonRepository extends EntityRepository +{ + /** + * @param string $phonenumber + * @param $centers + * @param $firstResult + * @param $maxResults + * @param array $only + * @return mixed + * @throws \Exception + */ + public function findByPhone( + string $phonenumber, + $centers, + $firstResult, + $maxResults, + array $only = ['mobile', 'phone'] + ) { + $qb = $this->createQueryBuilder('p'); + $qb->select('p'); + + $this->addByCenters($qb, $centers); + $this->addPhoneNumber($qb, $phonenumber, $only); + + $qb->setFirstResult($firstResult) + ->setMaxResults($maxResults) + ; + + return $qb->getQuery()->getResult(); + } + + /** + * @param string $phonenumber + * @param $centers + * @param array $only + * @return int + * @throws \Doctrine\ORM\NoResultException + * @throws \Doctrine\ORM\NonUniqueResultException + */ + public function countByPhone( + string $phonenumber, + $centers, + array $only = ['mobile', 'phone'] + ): int + { + $qb = $this->createQueryBuilder('p'); + $qb->select('COUNT(p)'); + + $this->addByCenters($qb, $centers); + $this->addPhoneNumber($qb, $phonenumber, $only); + + return $qb->getQuery()->getSingleScalarResult(); + } + + /** + * @param QueryBuilder $qb + * @param string $phonenumber + * @param array $only + * @throws \Exception + */ + protected function addPhoneNumber(QueryBuilder $qb, string $phonenumber, array $only) + { + if (count($only) === 0) { + throw new \Exception("No array field to search"); + } + + $phonenumber = $this->parsePhoneNumber($phonenumber); + + $orX = $qb->expr()->orX(); + + if (\in_array('mobile', $only)) { + $orX->add($qb->expr()->like("REPLACE(p.mobilenumber, ' ', '')", ':phonenumber')); + } + if (\in_array('phone', $only)) { + $orX->add($qb->expr()->like("REPLACE(p.phonenumber, ' ', '')", ':phonenumber')); + } + + $qb->andWhere($orX); + + $qb->setParameter('phonenumber', '%'.$phonenumber.'%'); + } + + /** + * @param $phonenumber + * @return string + */ + protected function parsePhoneNumber($phonenumber): string + { + return \str_replace(' ', '', $phonenumber); + } + + /** + * @param QueryBuilder $qb + * @param array $centers + */ + protected function addByCenters(QueryBuilder $qb, array $centers) + { + if (count($centers) > 0) { + $qb->andWhere($qb->expr()->in('p.center', ':centers')); + $qb->setParameter('centers', $centers); + } + } +} diff --git a/src/Bundle/ChillPerson/Resources/Gruntfile.js b/src/Bundle/ChillPerson/Resources/Gruntfile.js new file mode 100644 index 000000000..1e3fea92f --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/Gruntfile.js @@ -0,0 +1,46 @@ +module.exports = function(grunt) { + grunt.initConfig({ + pkg: grunt.file.readJSON('package.json'), + + chillperson: { + folders: { + pub: './public', + css: '<%= chillperson.folders.pub %>/css/', + sass: '<%= chillperson.folders.pub %>/sass/', + } + }, + sass: { + dist: { + options: { + debugInfo: false, + }, + files: [{ + expand: true, + cwd: '<%= chillperson.folders.sass.src %>', + src: ['*.scss'], + dest: '<%= chillperson.folders.css %>', + ext: '.css' + }] + } + }, + watch: { + css: { + files: [ '<%= chillperson.folders.sass %>/*.scss', '<%= chillperson.folders.sass %>/**/*.scss' ], + tasks: ['generatecss'], + /* + options: { + spawn: false, + interrupt: true, + } + */ + } + }, + }); + + grunt.loadNpmTasks('grunt-contrib-sass'); + grunt.loadNpmTasks('grunt-contrib-watch'); + + grunt.registerTask('generatecss', 'sass'); + + grunt.registerTask('default', ['generatecss']); +}; \ No newline at end of file diff --git a/src/Bundle/ChillPerson/Resources/doc/index.rst b/src/Bundle/ChillPerson/Resources/doc/index.rst new file mode 100644 index 000000000..e69de29bb diff --git a/src/Bundle/ChillPerson/Resources/package.json b/src/Bundle/ChillPerson/Resources/package.json new file mode 100644 index 000000000..3db5780e7 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/package.json @@ -0,0 +1,16 @@ +{ + "name": "chill-person-js-css", + "version": "0.0.0", + "description": "JS-CSS files for ChillPerson Bundle, a software for social workers", + "directories": { + }, + "author": "Champs-Libres ", + "devDependencies": { + "grunt": "^0.4.5", + "grunt-contrib-copy": "^0.7.0", + "grunt-contrib-sass": "^0.8.1", + "grunt-contrib-watch": "^0.6.1", + "grunt-contrib-clean": "^0.6.0" + }, + "dependencies": {} +} diff --git a/src/Bundle/ChillPerson/Resources/public/css/person.css b/src/Bundle/ChillPerson/Resources/public/css/person.css new file mode 100644 index 000000000..61fca71e5 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/public/css/person.css @@ -0,0 +1,78 @@ +.chill-blue, div.person-view div.custom-fields figure.person-details div.cf_title_box:nth-child(4n+4) h2 { + color: #334d5c; } + +.chill-green, div.person-view div.custom-fields figure.person-details div.cf_title_box:nth-child(4n+2) h2 { + color: #43b29d; } + +.chill-green-dark { + color: #328474; } + +.chill-yellow { + color: #eec84a; } + +.chill-orange, div.person-view div.custom-fields figure.person-details div.cf_title_box:nth-child(4n+3) h2 { + color: #e2793d; } + +.chill-red, div.person-view div.custom-fields figure.person-details div.cf_title_box:nth-child(4n+1) h2 { + color: #df4949; } + +.chill-gray { + color: #ececec; } + +.chill-beige { + color: #cabb9f; } + +.chill-pink { + color: #dd506d; } + +.chill-dark-gray { + color: #333333; } + +.chill-light-gray { + color: #b2b2b2; } + +div#header-person-name { + background: none repeat scroll 0 0 #328474; + color: #FFF; + padding-top: 1em; + padding-bottom: 1em; } + +div#header-person-details { + background: none repeat scroll 0 0 #43b29d; + color: #FFF; + padding-top: 1em; + padding-bottom: 1em; } + +div#person_details_container { + padding-top: 20px; + padding-bottom: 20px; } + +div.person-view { + /* custom fields on the home page */ } + div.person-view figure.person-details { + /* background-color: $black; + padding-top: 0.2em; + padding-bottom: 0.2em; + }*/ } + div.person-view figure.person-details h2 { + font-family: 'Open Sans'; + font-weight: 600; + margin-bottom: 0.3em; + font-variant: small-caps; } + div.person-view figure.person-details dl { + margin-top: 0.3em; } + div.person-view figure.person-details dt { + font-family: 'Open Sans'; + font-weight: 600; } + div.person-view figure.person-details dd { + margin-left: 0; } + div.person-view div.custom-fields figure.person-details { + display: flex; + flex-flow: row wrap; } + div.person-view div.custom-fields figure.person-details div.cf_title_box:nth-child(2n+1) { + width: 50%; + margin-right: 40px; } + div.person-view div.custom-fields figure.person-details iv.cf_title_box:nth-child(2n+2) { + width: calc(50% - 40px); } + +/*# sourceMappingURL=person.css.map */ diff --git a/src/Bundle/ChillPerson/Resources/public/css/person.css.map b/src/Bundle/ChillPerson/Resources/public/css/person.css.map new file mode 100644 index 000000000..6593711e5 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/public/css/person.css.map @@ -0,0 +1,7 @@ +{ +"version": 3, +"mappings": "AAkBE,wGAA+B;EAC7B,KAAK,EAJI,OAAO;;AAGlB,yGAA+B;EAC7B,KAAK,EAJY,OAAO;;AAG1B,iBAA+B;EAC7B,KAAK,EAJoB,OAAO;;AAGlC,aAA+B;EAC7B,KAAK,EAJ4B,OAAO;;AAG1C,0GAA+B;EAC7B,KAAK,EAJoC,OAAO;;AAGlD,uGAA+B;EAC7B,KAAK,EAJ4C,OAAO;;AAG1D,WAA+B;EAC7B,KAAK,EAJoD,OAAO;;AAGlE,YAA+B;EAC7B,KAAK,EAJ4D,OAAO;;AAG1E,WAA+B;EAC7B,KAAK,EAJoE,OAAO;;AAGlF,gBAA+B;EAC7B,KAAK,EAJ4E,OAAO;;AAG1F,iBAA+B;EAC7B,KAAK,EAJoF,OAAO;;ACZpG,sBAAuB;EACnB,UAAU,EAAE,8BAAwC;EACpD,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,GAAG;EAChB,cAAc,EAAE,GAAG;;AAGvB,yBAA0B;EACtB,UAAU,EAAE,8BAAmC;EAC/C,KAAK,EAAE,IAAI;EACX,WAAW,EAAE,GAAG;EAChB,cAAc,EAAE,GAAG;;AAGvB,4BAA6B;EAC5B,WAAW,EAAE,IAAI;EACjB,cAAc,EAAE,IAAI;;AAGrB,eAAgB;EA6BZ,oCAAoC;EA5BpC,qCAAsB;IAsB1B;;;mBAGe;IAxBH,wCAAG;MACC,WAAW,EAAE,WAAW;MACxB,WAAW,EAAE,GAAG;MAChB,aAAa,EAAE,KAAK;MACpB,YAAY,EAAE,UAAU;IAG5B,wCAAG;MACK,UAAU,EAAE,KAAK;IAGzB,wCAAG;MACH,WAAW,EAAE,WAAW;MACxB,WAAW,EAAE,GAAG;IAGhB,wCAAG;MACK,WAAW,EAAE,CAAC;EAY1B,uDAAsB;IAClB,OAAO,EAAE,IAAI;IACb,SAAS,EAAE,QAAQ;IAkBnB,wFAAgC;MAC5B,KAAK,EAAE,GAAG;MACV,YAAY,EAAE,IAAI;IAGtB,uFAAgC;MAC5B,KAAK,EAAE,gBAAgB", +"sources": ["../../../../main/Resources/public/sass/custom/config/_colors.scss","../sass/person.scss"], +"names": [], +"file": "person.css" +} diff --git a/src/Bundle/ChillPerson/Resources/public/index.js b/src/Bundle/ChillPerson/Resources/public/index.js new file mode 100644 index 000000000..f4bcba7fb --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/public/index.js @@ -0,0 +1 @@ +require('./sass/person.scss'); \ No newline at end of file diff --git a/src/Bundle/ChillPerson/Resources/public/sass/index.js b/src/Bundle/ChillPerson/Resources/public/sass/index.js new file mode 100644 index 000000000..35945c7ff --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/public/sass/index.js @@ -0,0 +1,5 @@ +require('./phone-alt-solid.svg'); +require('./mobile-alt-solid.svg'); +require('./person_by_phonenumber.scss'); + + diff --git a/src/Bundle/ChillPerson/Resources/public/sass/mobile-alt-solid.svg b/src/Bundle/ChillPerson/Resources/public/sass/mobile-alt-solid.svg new file mode 100644 index 000000000..ae8b81bb1 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/public/sass/mobile-alt-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Bundle/ChillPerson/Resources/public/sass/person.scss b/src/Bundle/ChillPerson/Resources/public/sass/person.scss new file mode 100644 index 000000000..78e3d5b40 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/public/sass/person.scss @@ -0,0 +1,88 @@ + +@import '../../../../main/Resources/public/sass/custom/config/colors'; + +div#header-person-name { + background: none repeat scroll 0 0 $chill-green-dark; + color: #FFF; + padding-top: 1em; + padding-bottom: 1em; +} + +div#header-person-details { + background: none repeat scroll 0 0 $chill-green; + color: #FFF; + padding-top: 1em; + padding-bottom: 1em; +} + +div#person_details_container { + padding-top: 20px; + padding-bottom: 20px; +} + +div.person-view { + figure.person-details { + h2 { + font-family: 'Open Sans'; + font-weight: 600; + margin-bottom: 0.3em; + font-variant: small-caps; + } + + dl { + margin-top: 0.3em; + } + + dt { + font-family: 'Open Sans'; + font-weight: 600; + } + + dd { + margin-left: 0; + } + +// a.sc-button { +/* background-color: $black; + padding-top: 0.2em; + padding-bottom: 0.2em; + }*/ + } + + /* custom fields on the home page */ + div.custom-fields { + figure.person-details { + display: flex; + flex-flow: row wrap; + + div.cf_title_box:nth-child(4n+1) h2 { + @extend .chill-red; + } + + div.cf_title_box:nth-child(4n+2) h2 { + @extend .chill-green; + } + + div.cf_title_box:nth-child(4n+3) h2 { + @extend .chill-orange; + } + + div.cf_title_box:nth-child(4n+4) h2 { + @extend .chill-blue; + } + + div.cf_title_box:nth-child(2n+1){ + width: 50%; + margin-right: 40px; + } + + iv.cf_title_box:nth-child(2n+2) { + width: calc(50% - 40px); + } + + } + + + } + +} diff --git a/src/Bundle/ChillPerson/Resources/public/sass/person_by_phonenumber.scss b/src/Bundle/ChillPerson/Resources/public/sass/person_by_phonenumber.scss new file mode 100644 index 000000000..a1066c08f --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/public/sass/person_by_phonenumber.scss @@ -0,0 +1,25 @@ +.person-list__--by-phonenumber { + .person-list__--by-phonenumber__phones { + ul { + list-style: none inside; + padding: 0; + margin: 0; + + li { + margin: 0.80rem; + + img { + vertical-align: baseline; + height: 0.90rem; + margin-right: 0.20rem; + } + pre { + display: inline; + } + } + } + + + } +} +; diff --git a/src/Bundle/ChillPerson/Resources/public/sass/phone-alt-solid.svg b/src/Bundle/ChillPerson/Resources/public/sass/phone-alt-solid.svg new file mode 100644 index 000000000..6460d2d9e --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/public/sass/phone-alt-solid.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/AppKernel.php b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/AppKernel.php new file mode 100644 index 000000000..29c257406 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/AppKernel.php @@ -0,0 +1,47 @@ +load(__DIR__.'/config/config_'.$this->getEnvironment().'.yml'); + } + + /** + * @return string + */ + public function getCacheDir() + { + return sys_get_temp_dir().'/PersonBundle/cache'; + } + + /** + * @return string + */ + public function getLogDir() + { + return sys_get_temp_dir().'/PersonBundle/logs'; + } +} + diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/DoctrineMigrations/.gitignore b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/DoctrineMigrations/.gitignore new file mode 100644 index 000000000..d6b7ef32c --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/DoctrineMigrations/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/Resources/views/base.html.twig b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/Resources/views/base.html.twig new file mode 100644 index 000000000..bafd28d3b --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/Resources/views/base.html.twig @@ -0,0 +1,13 @@ + + + + + {% block title %}Welcome!{% endblock %} + {% block stylesheets %}{% endblock %} + + + + {% block body %}{% endblock %} + {% block javascripts %}{% endblock %} + + diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/autoload.php b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/autoload.php new file mode 100644 index 000000000..39dc0e2ba --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/app/autoload.php @@ -0,0 +1,11 @@ +getParameterOption(array('--env', '-e'), getenv('SYMFONY_ENV') ?: 'dev'); +$debug = getenv('SYMFONY_DEBUG') !== '0' && !$input->hasParameterOption(array('--no-debug', '')) && $env !== 'prod'; + +if ($debug) { + Debug::enable(); +} + +$kernel = new AppKernel($env, $debug); +$application = new Application($kernel); +$application->run($input); diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/logs/empty b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/logs/empty new file mode 100644 index 000000000..e69de29bb diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/app_dev.php b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/app_dev.php new file mode 100644 index 000000000..e0279c2ae --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/app_dev.php @@ -0,0 +1,30 @@ +loadClassCache(); +$request = Request::createFromGlobals(); +$response = $kernel->handle($request); +$response->send(); +$kernel->terminate($request, $response); diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/CONTRIBUTING.md b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/CONTRIBUTING.md new file mode 100644 index 000000000..bca4baf61 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/CONTRIBUTING.md @@ -0,0 +1,107 @@ +Contributing to Select2 +======================= +Looking to contribute something to Select2? **Here's how you can help.** + +Please take a moment to review this document in order to make the contribution +process easy and effective for everyone involved. + +Following these guidelines helps to communicate that you respect the time of +the developers managing and developing this open source project. In return, +they should reciprocate that respect in addressing your issue or assessing +patches and features. + +Using the issue tracker +----------------------- +When [reporting bugs][reporting-bugs] or +[requesting features][requesting-features], the +[issue tracker on GitHub][issue-tracker] is the recommended channel to use. + +The issue tracker **is not** a place for support requests. The +[mailing list][mailing-list] or [IRC channel][irc-channel] are better places to +get help. + +Reporting bugs with Select2 +--------------------------- +We really appreciate clear bug reports that _consistently_ show an issue +_within Select2_. + +The ideal bug report follows these guidelines: + +1. **Use the [GitHub issue search][issue-search]** — Check if the issue + has already been reported. +2. **Check if the issue has been fixed** — Try to reproduce the problem + using the code in the `master` branch. +3. **Isolate the problem** — Try to create an + [isolated test case][isolated-case] that consistently reproduces the problem. + +Please try to be as detailed as possible in your bug report, especially if an +isolated test case cannot be made. Some useful questions to include the answer +to are: + +- What steps can be used to reproduce the issue? +- What is the bug and what is the expected outcome? +- What browser(s) and Operating System have you tested with? +- Does the bug happen consistently across all tested browsers? +- What version of jQuery are you using? And what version of Select2? +- Are you using Select2 with other plugins? + +All of these questions will help people fix and identify any potential bugs. + +Requesting features in Select2 +------------------------------ +Select2 is a large library that carries with it a lot of functionality. Because +of this, many feature requests will not be implemented in the core library. + +Before starting work on a major feature for Select2, **contact the +[community][community] first** or you may risk spending a considerable amount of +time on something which the project developers are not interested in bringing +into the project. + +### Select2 4.0 + +Many feature requests will be closed off until 4.0, where Select2 plans to adopt +a more flexible API. If you are interested in helping with the development of +the next major Select2 release, please send a message to the +[mailing list][mailing-list] or [irc channel][irc-channel] for more information. + +Triaging issues and pull requests +--------------------------------- +Anyone can help the project maintainers triage issues and review pull requests. + +### Handling new issues + +Select2 regularly receives new issues which need to be tested and organized. + +When a new issue that comes in that is similar to another existing issue, it +should be checked to make sure it is not a duplicate. Duplicates issues should +be marked by replying to the issue with "Duplicate of #[issue number]" where +`[issue number]` is the url or issue number for the existing issue. This will +allow the project maintainers to quickly close off additional issues and keep +the discussion focused within a single issue. + +If you can test issues that are reported to Select2 that contain test cases and +confirm under what conditions bugs happen, that will allow others to identify +what causes a bug quicker. + +### Reviewing pull requests + +It is very common for pull requests to be opened for issues that contain a clear +solution to the problem. These pull requests should be rigorously reviewed by +the community before being accepted. If you are not sure about a piece of +submitted code, or know of a better way to do something, do not hesitate to make +a comment on the pull request. + +It should also be made clear that **all code contributed to Select** must be +licensable under the [Apache 2 or GPL 2 licenses][licensing]. Code that cannot +be released under either of these licenses **cannot be accepted** into the +project. + +[community]: https://github.com/ivaynberg/select2#community +[reporting-bugs]: #reporting-bugs-with-select2 +[requesting-features]: #requesting-features-in-select2 +[issue-tracker]: https://github.com/ivaynberg/select2/issues +[mailing-list]: https://github.com/ivaynberg/select2#mailing-list +[irc-channel]: https://github.com/ivaynberg/select2#irc-channel +[issue-search]: https://github.com/ivaynberg/select2/search?q=&type=Issues +[isolated-case]: http://css-tricks.com/6263-reduced-test-cases/ +[licensing]: https://github.com/ivaynberg/select2#copyright-and-license diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/LICENSE b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/LICENSE new file mode 100644 index 000000000..0247cc762 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/LICENSE @@ -0,0 +1,18 @@ +Copyright 2014 Igor Vaynberg + +Version: @@ver@@ Timestamp: @@timestamp@@ + +This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU +General Public License version 2 (the "GPL License"). You may choose either license to govern your +use of this software only upon the condition that you accept all of the terms of either the Apache +License or the GPL License. + +You may obtain a copy of the Apache License and the GPL License at: + +http://www.apache.org/licenses/LICENSE-2.0 +http://www.gnu.org/licenses/gpl-2.0.html + +Unless required by applicable law or agreed to in writing, software distributed under the Apache License +or the GPL Licesnse is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the Apache License and the GPL License for the specific language governing +permissions and limitations under the Apache License and the GPL License. diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/README.md b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/README.md new file mode 100644 index 000000000..64380c451 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/README.md @@ -0,0 +1,114 @@ +Select2 +======= + +Select2 is a jQuery-based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results. + +To get started, checkout examples and documentation at http://ivaynberg.github.com/select2 + +Use cases +--------- + +* Enhancing native selects with search. +* Enhancing native selects with a better multi-select interface. +* Loading data from JavaScript: easily load items via ajax and have them searchable. +* Nesting optgroups: native selects only support one level of nested. Select2 does not have this restriction. +* Tagging: ability to add new items on the fly. +* Working with large, remote datasets: ability to partially load a dataset based on the search term. +* Paging of large datasets: easy support for loading more pages when the results are scrolled to the end. +* Templating: support for custom rendering of results and selections. + +Browser compatibility +--------------------- +* IE 8+ +* Chrome 8+ +* Firefox 10+ +* Safari 3+ +* Opera 10.6+ + +Usage +----- +You can source Select2 directly from a CDN like [JSDliver](http://www.jsdelivr.com/#!select2) or [CDNJS](http://www.cdnjs.com/libraries/select2), [download it from this GitHub repo](https://github.com/ivaynberg/select2/tags), or use one of the integrations below. + +Integrations +------------ + +* [Wicket-Select2](https://github.com/ivaynberg/wicket-select2) (Java / [Apache Wicket](http://wicket.apache.org)) +* [select2-rails](https://github.com/argerim/select2-rails) (Ruby on Rails) +* [AngularUI](http://angular-ui.github.io/#ui-select) ([AngularJS](https://angularjs.org/)) +* [Django](https://github.com/applegrew/django-select2) +* [Symfony](https://github.com/19Gerhard85/sfSelect2WidgetsPlugin) +* [Symfony2](https://github.com/avocode/FormExtensions) +* [Bootstrap 2](https://github.com/t0m/select2-bootstrap-css) and [Bootstrap 3](https://github.com/t0m/select2-bootstrap-css/tree/bootstrap3) (CSS skins) +* [Meteor](https://github.com/nate-strauser/meteor-select2) (modern reactive JavaScript framework; + [Bootstrap 3 skin](https://github.com/esperadomedia/meteor-select2-bootstrap3-css/)) +* [Meteor](https://jquery-select2.meteor.com) +* [Yii 2.x](http://demos.krajee.com/widgets#select2) +* [Yii 1.x](https://github.com/tonybolzan/yii-select2) +* [AtmosphereJS](https://atmospherejs.com/package/jquery-select2) + +### Example Integrations + +* [Knockout.js](https://github.com/ivaynberg/select2/wiki/Knockout.js-Integration) +* [Socket.IO](https://github.com/ivaynberg/select2/wiki/Socket.IO-Integration) +* [PHP](https://github.com/ivaynberg/select2/wiki/PHP-Example) +* [.Net MVC] (https://github.com/ivaynberg/select2/wiki/.Net-MVC-Example) + +Internationalization (i18n) +--------------------------- + +Select2 supports multiple languages by simply including the right language JS +file (`select2_locale_it.js`, `select2_locale_nl.js`, etc.) after `select2.js`. + +Missing a language? Just copy `select2_locale_en.js.template`, translate +it, and make a pull request back to Select2 here on GitHub. + +Documentation +------------- + +The documentation for Select2 is available [through GitHub Pages](https://ivaynberg.github.io/select2/) and is located within this repository in the [`gh-pages` branch](https://github.com/ivaynberg/select2/tree/gh-pages). + +Community +--------- + +### Bug tracker + +Have a bug? Please create an issue here on GitHub! + +https://github.com/ivaynberg/select2/issues + +### Mailing list + +Have a question? Ask on our mailing list! + +select2@googlegroups.com + +https://groups.google.com/d/forum/select2 + +### IRC channel + +Need help implementing Select2 in your project? Ask in our IRC channel! + +**Network:** [Freenode](https://freenode.net/) (`chat.freenode.net`) + +**Channel:** `#select2` + +**Web access:** https://webchat.freenode.net/?channels=select2 + +Copyright and license +--------------------- + +Copyright 2012 Igor Vaynberg + +This software is licensed under the Apache License, Version 2.0 (the "Apache License") or the GNU +General Public License version 2 (the "GPL License"). You may choose either license to govern your +use of this software only upon the condition that you accept all of the terms of either the Apache +License or the GPL License. + +You may obtain a copy of the Apache License and the GPL License in the LICENSE file, or at: + +http://www.apache.org/licenses/LICENSE-2.0 +http://www.gnu.org/licenses/gpl-2.0.html + +Unless required by applicable law or agreed to in writing, software distributed under the Apache License +or the GPL License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +either express or implied. See the Apache License and the GPL License for the specific language governing +permissions and limitations under the Apache License and the GPL License. diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/bower.json b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/bower.json new file mode 100644 index 000000000..54d44c459 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/bower.json @@ -0,0 +1,8 @@ +{ + "name": "select2", + "version": "3.5.2", + "main": ["select2.js", "select2.css", "select2.png", "select2x2.png", "select2-spinner.gif"], + "dependencies": { + "jquery": ">= 1.7.1" + } +} diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/component.json b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/component.json new file mode 100644 index 000000000..8bd3c020a --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/component.json @@ -0,0 +1,66 @@ +{ + "name": "select2", + "repo": "ivaynberg/select2", + "description": "Select2 is a jQuery based replacement for select boxes. It supports searching, remote data sets, and infinite scrolling of results.", + "version": "3.5.2", + "demo": "http://ivaynberg.github.io/select2/", + "keywords": [ + "jquery" + ], + "main": "select2.js", + "styles": [ + "select2.css", + "select2-bootstrap.css" + ], + "scripts": [ + "select2.js", + "select2_locale_ar.js", + "select2_locale_bg.js", + "select2_locale_ca.js", + "select2_locale_cs.js", + "select2_locale_da.js", + "select2_locale_de.js", + "select2_locale_el.js", + "select2_locale_es.js", + "select2_locale_et.js", + "select2_locale_eu.js", + "select2_locale_fa.js", + "select2_locale_fi.js", + "select2_locale_fr.js", + "select2_locale_gl.js", + "select2_locale_he.js", + "select2_locale_hr.js", + "select2_locale_hu.js", + "select2_locale_id.js", + "select2_locale_is.js", + "select2_locale_it.js", + "select2_locale_ja.js", + "select2_locale_ka.js", + "select2_locale_ko.js", + "select2_locale_lt.js", + "select2_locale_lv.js", + "select2_locale_mk.js", + "select2_locale_ms.js", + "select2_locale_nl.js", + "select2_locale_no.js", + "select2_locale_pl.js", + "select2_locale_pt-BR.js", + "select2_locale_pt-PT.js", + "select2_locale_ro.js", + "select2_locale_ru.js", + "select2_locale_sk.js", + "select2_locale_sv.js", + "select2_locale_th.js", + "select2_locale_tr.js", + "select2_locale_uk.js", + "select2_locale_vi.js", + "select2_locale_zh-CN.js", + "select2_locale_zh-TW.js" + ], + "images": [ + "select2-spinner.gif", + "select2.png", + "select2x2.png" + ], + "license": "MIT" +} diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/composer.json b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/composer.json new file mode 100644 index 000000000..cd2d26a2a --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/composer.json @@ -0,0 +1,29 @@ +{ + "name": + "ivaynberg/select2", + "description": "Select2 is a jQuery based replacement for select boxes.", + "version": "3.5.2", + "type": "component", + "homepage": "http://ivaynberg.github.io/select2/", + "license": "Apache-2.0", + "require": { + "robloach/component-installer": "*", + "components/jquery": ">=1.7.1" + }, + "extra": { + "component": { + "scripts": [ + "select2.js" + ], + "files": [ + "select2.js", + "select2_locale_*.js", + "select2.css", + "select2-bootstrap.css", + "select2-spinner.gif", + "select2.png", + "select2x2.png" + ] + } + } +} diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/css/admin.css b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/css/admin.css new file mode 100644 index 000000000..e4ef36d42 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/css/admin.css @@ -0,0 +1,2 @@ +ul.admin_tiles li { background: none repeat scroll 0 0 #E9E9E9; border: 1px solid #C9C9C9; padding: 1.5em; margin-left: 1.5em; margin-bottom: 1.5em; } +ul.admin_tiles li p, ul.admin_tiles li h1, ul.admin_tiles li h2, ul.admin_tiles li h3 { width: 350px; } diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/css/chillmain.css b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/css/chillmain.css new file mode 100644 index 000000000..85c247173 --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/css/chillmain.css @@ -0,0 +1,25 @@ +div#usefulbar { background-color: #fbba3a; z-index: 1000; padding-right: 15px; } +div#usefulbar form { margin: 0; } +div#usefulbar i.menu { font-size: 2em; } +div#usefulbar ul { display: flex; justify-content: flex-end; margin: 0; padding-top: 5px; padding-right: 10px; } +div#usefulbar li { color: white; margin-left: 10px; } +div#usefulbar li a { color: white; text-shadow: 0px 0px 1px #555; } +div#usefulbar li i.icon-user-add:before { vertical-align: -5px; } +div#usefulbar li#search_element { text-align: right; } +div#usefulbar li#search_element div#search_form { margin: 0; padding: 0; } +div#usefulbar li#search_element div#search_form div { margin: 0; } +div#usefulbar li#search_element div#search_form .field { margin: 0; } +div#usefulbar li#search_element div#search_form button { color: white; border: none; bottom: -2px; height: 35px; } + +div#flashMessages { margin-top: 20px; } +div#flashMessages .flash-notice { margin-top: 10px; margin-bottom: 10px; } + +.personName { font-variant: small-caps; text-transform: capitalize; } + +.personName { text-transform: capitalize; } + +input.belgian_national_number_inversed_date { width: 7em; margin-right: 1em; } + +input.belgian_national_number_daily_counter { width: 4em; margin-right: 1em; } + +input.belgian_national_number_control_digit { width: 3em; } \ No newline at end of file diff --git a/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/css/gumby.css b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/css/gumby.css new file mode 100644 index 000000000..24a87ec7a --- /dev/null +++ b/src/Bundle/ChillPerson/Resources/test/Fixtures/App/web/bundles/chillmain/css/gumby.css @@ -0,0 +1,2617 @@ +@charset "UTF-8"; +/** +* Gumby Framework +* --------------- +* +* Follow @gumbycss on twitter and spread the love. +* We worked super hard on making this awesome and released it to the web. +* All we ask is you leave this intact. #gumbyisawesome +* +* Gumby Framework +* http://gumbyframework.com +* +* Built with love by your friends @digitalsurgeons +* http://www.digitalsurgeons.com +* +* Free to use under the MIT license. +* http://www.opensource.org/licenses/mit-license.php +*/ +html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font: inherit; font-size: 100%; vertical-align: baseline; } + +html { line-height: 1; } + +ol, ul { list-style: none; } + +table { border-collapse: collapse; border-spacing: 0; } + +caption, th, td { text-align: left; font-weight: normal; vertical-align: middle; } + +q, blockquote { quotes: none; } +q:before, q:after, blockquote:before, blockquote:after { content: ""; content: none; } + +a img { border: none; } + +article, aside, details, figcaption, figure, footer, header, hgroup, menu, nav, section, summary { display: block; } + +.pull_right { float: right; } + +.pull_left { float: left; } + +* html { font-size: 100%; } + +html { font-size: 16px; line-height: 1.625em; } + +* { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } + +body { background: white; font-family: "Oxygen"; font-weight: 400; color: #555555; position: relative; -webkit-font-smoothing: antialiased; } +@media only screen and (max-width: 767px) { body { -webkit-text-size-adjust: none; -ms-text-size-adjust: none; width: 100%; min-width: 0; } } + +html, body { height: 100%; } + +.hide { display: none; } + +.hide.active, .show { display: block; } + +.icon-note.icon-left a:before, .icon-note.icon-right a:after { content: "\266a"; height: inherit; } + +i.icon-note:before { content: "\266a"; height: inherit; } + +.icon-note-beamed.icon-left a:before, .icon-note-beamed.icon-right a:after { content: "\266b"; height: inherit; } + +i.icon-note-beamed:before { content: "\266b"; height: inherit; } + +.icon-music.icon-left a:before, .icon-music.icon-right a:after { content: "🎵"; height: inherit; } + +i.icon-music:before { content: "🎵"; height: inherit; } + +.icon-search.icon-left a:before, .icon-search.icon-right a:after { content: "🔍"; height: inherit; } + +i.icon-search:before { content: "🔍"; height: inherit; } + +.icon-flashlight.icon-left a:before, .icon-flashlight.icon-right a:after { content: "🔦"; height: inherit; } + +i.icon-flashlight:before { content: "🔦"; height: inherit; } + +.icon-mail.icon-left a:before, .icon-mail.icon-right a:after { content: "\2709"; height: inherit; } + +i.icon-mail:before { content: "\2709"; height: inherit; } + +.icon-heart.icon-left a:before, .icon-heart.icon-right a:after { content: "\2665"; height: inherit; } + +i.icon-heart:before { content: "\2665"; height: inherit; } + +.icon-heart-empty.icon-left a:before, .icon-heart-empty.icon-right a:after { content: "\2661"; height: inherit; } + +i.icon-heart-empty:before { content: "\2661"; height: inherit; } + +.icon-star.icon-left a:before, .icon-star.icon-right a:after { content: "\2605"; height: inherit; } + +i.icon-star:before { content: "\2605"; height: inherit; } + +.icon-star-empty.icon-left a:before, .icon-star-empty.icon-right a:after { content: "\2606"; height: inherit; } + +i.icon-star-empty:before { content: "\2606"; height: inherit; } + +.icon-user.icon-left a:before, .icon-user.icon-right a:after { content: "👤"; height: inherit; } + +i.icon-user:before { content: "👤"; height: inherit; } + +.icon-users.icon-left a:before, .icon-users.icon-right a:after { content: "👥"; height: inherit; } + +i.icon-users:before { content: "👥"; height: inherit; } + +.icon-user-add.icon-left a:before, .icon-user-add.icon-right a:after { content: "\e700"; height: inherit; } + +i.icon-user-add:before { content: "\e700"; height: inherit; } + +.icon-video.icon-left a:before, .icon-video.icon-right a:after { content: "🎬"; height: inherit; } + +i.icon-video:before { content: "🎬"; height: inherit; } + +.icon-picture.icon-left a:before, .icon-picture.icon-right a:after { content: "🌄"; height: inherit; } + +i.icon-picture:before { content: "🌄"; height: inherit; } + +.icon-camera.icon-left a:before, .icon-camera.icon-right a:after { content: "📷"; height: inherit; } + +i.icon-camera:before { content: "📷"; height: inherit; } + +.icon-layout.icon-left a:before, .icon-layout.icon-right a:after { content: "\268f"; height: inherit; } + +i.icon-layout:before { content: "\268f"; height: inherit; } + +.icon-menu.icon-left a:before, .icon-menu.icon-right a:after { content: "\2630"; height: inherit; } + +i.icon-menu:before { content: "\2630"; height: inherit; } + +.icon-check.icon-left a:before, .icon-check.icon-right a:after { content: "\2713"; height: inherit; } + +i.icon-check:before { content: "\2713"; height: inherit; } + +.icon-cancel.icon-left a:before, .icon-cancel.icon-right a:after { content: "\2715"; height: inherit; } + +i.icon-cancel:before { content: "\2715"; height: inherit; } + +.icon-cancel-circled.icon-left a:before, .icon-cancel-circled.icon-right a:after { content: "\2716"; height: inherit; } + +i.icon-cancel-circled:before { content: "\2716"; height: inherit; } + +.icon-cancel-squared.icon-left a:before, .icon-cancel-squared.icon-right a:after { content: "\274e"; height: inherit; } + +i.icon-cancel-squared:before { content: "\274e"; height: inherit; } + +.icon-plus.icon-left a:before, .icon-plus.icon-right a:after { content: "\2b"; height: inherit; } + +i.icon-plus:before { content: "\2b"; height: inherit; } + +.icon-plus-circled.icon-left a:before, .icon-plus-circled.icon-right a:after { content: "\2795"; height: inherit; } + +i.icon-plus-circled:before { content: "\2795"; height: inherit; } + +.icon-plus-squared.icon-left a:before, .icon-plus-squared.icon-right a:after { content: "\229e"; height: inherit; } + +i.icon-plus-squared:before { content: "\229e"; height: inherit; } + +.icon-minus.icon-left a:before, .icon-minus.icon-right a:after { content: "\2d"; height: inherit; } + +i.icon-minus:before { content: "\2d"; height: inherit; } + +.icon-minus-circled.icon-left a:before, .icon-minus-circled.icon-right a:after { content: "\2796"; height: inherit; } + +i.icon-minus-circled:before { content: "\2796"; height: inherit; } + +.icon-minus-squared.icon-left a:before, .icon-minus-squared.icon-right a:after { content: "\229f"; height: inherit; } + +i.icon-minus-squared:before { content: "\229f"; height: inherit; } + +.icon-help.icon-left a:before, .icon-help.icon-right a:after { content: "\2753"; height: inherit; } + +i.icon-help:before { content: "\2753"; height: inherit; } + +.icon-help-circled.icon-left a:before, .icon-help-circled.icon-right a:after { content: "\e704"; height: inherit; } + +i.icon-help-circled:before { content: "\e704"; height: inherit; } + +.icon-info.icon-left a:before, .icon-info.icon-right a:after { content: "\2139"; height: inherit; } + +i.icon-info:before { content: "\2139"; height: inherit; } + +.icon-info-circled.icon-left a:before, .icon-info-circled.icon-right a:after { content: "\e705"; height: inherit; } + +i.icon-info-circled:before { content: "\e705"; height: inherit; } + +.icon-back.icon-left a:before, .icon-back.icon-right a:after { content: "🔙"; height: inherit; } + +i.icon-back:before { content: "🔙"; height: inherit; } + +.icon-home.icon-left a:before, .icon-home.icon-right a:after { content: "\2302"; height: inherit; } + +i.icon-home:before { content: "\2302"; height: inherit; } + +.icon-link.icon-left a:before, .icon-link.icon-right a:after { content: "🔗"; height: inherit; } + +i.icon-link:before { content: "🔗"; height: inherit; } + +.icon-attach.icon-left a:before, .icon-attach.icon-right a:after { content: "📎"; height: inherit; } + +i.icon-attach:before { content: "📎"; height: inherit; } + +.icon-lock.icon-left a:before, .icon-lock.icon-right a:after { content: "🔒"; height: inherit; } + +i.icon-lock:before { content: "🔒"; height: inherit; } + +.icon-lock-open.icon-left a:before, .icon-lock-open.icon-right a:after { content: "🔓"; height: inherit; } + +i.icon-lock-open:before { content: "🔓"; height: inherit; } + +.icon-eye.icon-left a:before, .icon-eye.icon-right a:after { content: "\e70a"; height: inherit; } + +i.icon-eye:before { content: "\e70a"; height: inherit; } + +.icon-tag.icon-left a:before, .icon-tag.icon-right a:after { content: "\e70c"; height: inherit; } + +i.icon-tag:before { content: "\e70c"; height: inherit; } + +.icon-bookmark.icon-left a:before, .icon-bookmark.icon-right a:after { content: "🔖"; height: inherit; } + +i.icon-bookmark:before { content: "🔖"; height: inherit; } + +.icon-bookmarks.icon-left a:before, .icon-bookmarks.icon-right a:after { content: "📑"; height: inherit; } + +i.icon-bookmarks:before { content: "📑"; height: inherit; } + +.icon-flag.icon-left a:before, .icon-flag.icon-right a:after { content: "\2691"; height: inherit; } + +i.icon-flag:before { content: "\2691"; height: inherit; } + +.icon-thumbs-up.icon-left a:before, .icon-thumbs-up.icon-right a:after { content: "👍"; height: inherit; } + +i.icon-thumbs-up:before { content: "👍"; height: inherit; } + +.icon-thumbs-down.icon-left a:before, .icon-thumbs-down.icon-right a:after { content: "👎"; height: inherit; } + +i.icon-thumbs-down:before { content: "👎"; height: inherit; } + +.icon-download.icon-left a:before, .icon-download.icon-right a:after { content: "📥"; height: inherit; } + +i.icon-download:before { content: "📥"; height: inherit; } + +.icon-upload.icon-left a:before, .icon-upload.icon-right a:after { content: "📤"; height: inherit; } + +i.icon-upload:before { content: "📤"; height: inherit; } + +.icon-upload-cloud.icon-left a:before, .icon-upload-cloud.icon-right a:after { content: "\e711"; height: inherit; } + +i.icon-upload-cloud:before { content: "\e711"; height: inherit; } + +.icon-reply.icon-left a:before, .icon-reply.icon-right a:after { content: "\e712"; height: inherit; } + +i.icon-reply:before { content: "\e712"; height: inherit; } + +.icon-reply-all.icon-left a:before, .icon-reply-all.icon-right a:after { content: "\e713"; height: inherit; } + +i.icon-reply-all:before { content: "\e713"; height: inherit; } + +.icon-forward.icon-left a:before, .icon-forward.icon-right a:after { content: "\27a6"; height: inherit; } + +i.icon-forward:before { content: "\27a6"; height: inherit; } + +.icon-quote.icon-left a:before, .icon-quote.icon-right a:after { content: "\275e"; height: inherit; } + +i.icon-quote:before { content: "\275e"; height: inherit; } + +.icon-code.icon-left a:before, .icon-code.icon-right a:after { content: "\e714"; height: inherit; } + +i.icon-code:before { content: "\e714"; height: inherit; } + +.icon-export.icon-left a:before, .icon-export.icon-right a:after { content: "\e715"; height: inherit; } + +i.icon-export:before { content: "\e715"; height: inherit; } + +.icon-pencil.icon-left a:before, .icon-pencil.icon-right a:after { content: "\270e"; height: inherit; } + +i.icon-pencil:before { content: "\270e"; height: inherit; } + +.icon-feather.icon-left a:before, .icon-feather.icon-right a:after { content: "\2712"; height: inherit; } + +i.icon-feather:before { content: "\2712"; height: inherit; } + +.icon-print.icon-left a:before, .icon-print.icon-right a:after { content: "\e716"; height: inherit; } + +i.icon-print:before { content: "\e716"; height: inherit; } + +.icon-retweet.icon-left a:before, .icon-retweet.icon-right a:after { content: "\e717"; height: inherit; } + +i.icon-retweet:before { content: "\e717"; height: inherit; } + +.icon-keyboard.icon-left a:before, .icon-keyboard.icon-right a:after { content: "\2328"; height: inherit; } + +i.icon-keyboard:before { content: "\2328"; height: inherit; } + +.icon-comment.icon-left a:before, .icon-comment.icon-right a:after { content: "\e718"; height: inherit; } + +i.icon-comment:before { content: "\e718"; height: inherit; } + +.icon-chat.icon-left a:before, .icon-chat.icon-right a:after { content: "\e720"; height: inherit; } + +i.icon-chat:before { content: "\e720"; height: inherit; } + +.icon-bell.icon-left a:before, .icon-bell.icon-right a:after { content: "🔔"; height: inherit; } + +i.icon-bell:before { content: "🔔"; height: inherit; } + +.icon-attention.icon-left a:before, .icon-attention.icon-right a:after { content: "\26a0"; height: inherit; } + +i.icon-attention:before { content: "\26a0"; height: inherit; } + +.icon-alert.icon-left a:before, .icon-alert.icon-right a:after { content: "💥"; height: inherit; } + +i.icon-alert:before { content: "💥"; height: inherit; } + +.icon-vcard.icon-left a:before, .icon-vcard.icon-right a:after { content: "\e722"; height: inherit; } + +i.icon-vcard:before { content: "\e722"; height: inherit; } + +.icon-address.icon-left a:before, .icon-address.icon-right a:after { content: "\e723"; height: inherit; } + +i.icon-address:before { content: "\e723"; height: inherit; } + +.icon-location.icon-left a:before, .icon-location.icon-right a:after { content: "\e724"; height: inherit; } + +i.icon-location:before { content: "\e724"; height: inherit; } + +.icon-map.icon-left a:before, .icon-map.icon-right a:after { content: "\e727"; height: inherit; } + +i.icon-map:before { content: "\e727"; height: inherit; } + +.icon-direction.icon-left a:before, .icon-direction.icon-right a:after { content: "\27a2"; height: inherit; } + +i.icon-direction:before { content: "\27a2"; height: inherit; } + +.icon-compass.icon-left a:before, .icon-compass.icon-right a:after { content: "\e728"; height: inherit; } + +i.icon-compass:before { content: "\e728"; height: inherit; } + +.icon-cup.icon-left a:before, .icon-cup.icon-right a:after { content: "\2615"; height: inherit; } + +i.icon-cup:before { content: "\2615"; height: inherit; } + +.icon-trash.icon-left a:before, .icon-trash.icon-right a:after { content: "\e729"; height: inherit; } + +i.icon-trash:before { content: "\e729"; height: inherit; } + +.icon-doc.icon-left a:before, .icon-doc.icon-right a:after { content: "\e730"; height: inherit; } + +i.icon-doc:before { content: "\e730"; height: inherit; } + +.icon-docs.icon-left a:before, .icon-docs.icon-right a:after { content: "\e736"; height: inherit; } + +i.icon-docs:before { content: "\e736"; height: inherit; } + +.icon-doc-landscape.icon-left a:before, .icon-doc-landscape.icon-right a:after { content: "\e737"; height: inherit; } + +i.icon-doc-landscape:before { content: "\e737"; height: inherit; } + +.icon-doc-text.icon-left a:before, .icon-doc-text.icon-right a:after { content: "📄"; height: inherit; } + +i.icon-doc-text:before { content: "📄"; height: inherit; } + +.icon-doc-text-inv.icon-left a:before, .icon-doc-text-inv.icon-right a:after { content: "\e731"; height: inherit; } + +i.icon-doc-text-inv:before { content: "\e731"; height: inherit; } + +.icon-newspaper.icon-left a:before, .icon-newspaper.icon-right a:after { content: "📰"; height: inherit; } + +i.icon-newspaper:before { content: "📰"; height: inherit; } + +.icon-book-open.icon-left a:before, .icon-book-open.icon-right a:after { content: "📖"; height: inherit; } + +i.icon-book-open:before { content: "📖"; height: inherit; } + +.icon-book.icon-left a:before, .icon-book.icon-right a:after { content: "📕"; height: inherit; } + +i.icon-book:before { content: "📕"; height: inherit; } + +.icon-folder.icon-left a:before, .icon-folder.icon-right a:after { content: "📁"; height: inherit; } + +i.icon-folder:before { content: "📁"; height: inherit; } + +.icon-archive.icon-left a:before, .icon-archive.icon-right a:after { content: "\e738"; height: inherit; } + +i.icon-archive:before { content: "\e738"; height: inherit; } + +.icon-box.icon-left a:before, .icon-box.icon-right a:after { content: "📦"; height: inherit; } + +i.icon-box:before { content: "📦"; height: inherit; } + +.icon-rss.icon-left a:before, .icon-rss.icon-right a:after { content: "\e73a"; height: inherit; } + +i.icon-rss:before { content: "\e73a"; height: inherit; } + +.icon-phone.icon-left a:before, .icon-phone.icon-right a:after { content: "📞"; height: inherit; } + +i.icon-phone:before { content: "📞"; height: inherit; } + +.icon-cog.icon-left a:before, .icon-cog.icon-right a:after { content: "\2699"; height: inherit; } + +i.icon-cog:before { content: "\2699"; height: inherit; } + +.icon-tools.icon-left a:before, .icon-tools.icon-right a:after { content: "\2692"; height: inherit; } + +i.icon-tools:before { content: "\2692"; height: inherit; } + +.icon-share.icon-left a:before, .icon-share.icon-right a:after { content: "\e73c"; height: inherit; } + +i.icon-share:before { content: "\e73c"; height: inherit; } + +.icon-shareable.icon-left a:before, .icon-shareable.icon-right a:after { content: "\e73e"; height: inherit; } + +i.icon-shareable:before { content: "\e73e"; height: inherit; } + +.icon-basket.icon-left a:before, .icon-basket.icon-right a:after { content: "\e73d"; height: inherit; } + +i.icon-basket:before { content: "\e73d"; height: inherit; } + +.icon-bag.icon-left a:before, .icon-bag.icon-right a:after { content: "👜"; height: inherit; } + +i.icon-bag:before { content: "👜"; height: inherit; } + +.icon-calendar.icon-left a:before, .icon-calendar.icon-right a:after { content: "📅"; height: inherit; } + +i.icon-calendar:before { content: "📅"; height: inherit; } + +.icon-login.icon-left a:before, .icon-login.icon-right a:after { content: "\e740"; height: inherit; } + +i.icon-login:before { content: "\e740"; height: inherit; } + +.icon-logout.icon-left a:before, .icon-logout.icon-right a:after { content: "\e741"; height: inherit; } + +i.icon-logout:before { content: "\e741"; height: inherit; } + +.icon-mic.icon-left a:before, .icon-mic.icon-right a:after { content: "🎤"; height: inherit; } + +i.icon-mic:before { content: "🎤"; height: inherit; } + +.icon-mute.icon-left a:before, .icon-mute.icon-right a:after { content: "🔇"; height: inherit; } + +i.icon-mute:before { content: "🔇"; height: inherit; } + +.icon-sound.icon-left a:before, .icon-sound.icon-right a:after { content: "🔊"; height: inherit; } + +i.icon-sound:before { content: "🔊"; height: inherit; } + +.icon-volume.icon-left a:before, .icon-volume.icon-right a:after { content: "\e742"; height: inherit; } + +i.icon-volume:before { content: "\e742"; height: inherit; } + +.icon-clock.icon-left a:before, .icon-clock.icon-right a:after { content: "🕔"; height: inherit; } + +i.icon-clock:before { content: "🕔"; height: inherit; } + +.icon-hourglass.icon-left a:before, .icon-hourglass.icon-right a:after { content: "\23f3"; height: inherit; } + +i.icon-hourglass:before { content: "\23f3"; height: inherit; } + +.icon-lamp.icon-left a:before, .icon-lamp.icon-right a:after { content: "💡"; height: inherit; } + +i.icon-lamp:before { content: "💡"; height: inherit; } + +.icon-light-down.icon-left a:before, .icon-light-down.icon-right a:after { content: "🔅"; height: inherit; } + +i.icon-light-down:before { content: "🔅"; height: inherit; } + +.icon-light-up.icon-left a:before, .icon-light-up.icon-right a:after { content: "🔆"; height: inherit; } + +i.icon-light-up:before { content: "🔆"; height: inherit; } + +.icon-adjust.icon-left a:before, .icon-adjust.icon-right a:after { content: "\25d1"; height: inherit; } + +i.icon-adjust:before { content: "\25d1"; height: inherit; } + +.icon-block.icon-left a:before, .icon-block.icon-right a:after { content: "🚫"; height: inherit; } + +i.icon-block:before { content: "🚫"; height: inherit; } + +.icon-resize-full.icon-left a:before, .icon-resize-full.icon-right a:after { content: "\e744"; height: inherit; } + +i.icon-resize-full:before { content: "\e744"; height: inherit; } + +.icon-resize-small.icon-left a:before, .icon-resize-small.icon-right a:after { content: "\e746"; height: inherit; } + +i.icon-resize-small:before { content: "\e746"; height: inherit; } + +.icon-popup.icon-left a:before, .icon-popup.icon-right a:after { content: "\e74c"; height: inherit; } + +i.icon-popup:before { content: "\e74c"; height: inherit; } + +.icon-publish.icon-left a:before, .icon-publish.icon-right a:after { content: "\e74d"; height: inherit; } + +i.icon-publish:before { content: "\e74d"; height: inherit; } + +.icon-window.icon-left a:before, .icon-window.icon-right a:after { content: "\e74e"; height: inherit; } + +i.icon-window:before { content: "\e74e"; height: inherit; } + +.icon-arrow-combo.icon-left a:before, .icon-arrow-combo.icon-right a:after { content: "\e74f"; height: inherit; } + +i.icon-arrow-combo:before { content: "\e74f"; height: inherit; } + +.icon-down-circled.icon-left a:before, .icon-down-circled.icon-right a:after { content: "\e758"; height: inherit; } + +i.icon-down-circled:before { content: "\e758"; height: inherit; } + +.icon-left-circled.icon-left a:before, .icon-left-circled.icon-right a:after { content: "\e759"; height: inherit; } + +i.icon-left-circled:before { content: "\e759"; height: inherit; } + +.icon-right-circled.icon-left a:before, .icon-right-circled.icon-right a:after { content: "\e75a"; height: inherit; } + +i.icon-right-circled:before { content: "\e75a"; height: inherit; } + +.icon-up-circled.icon-left a:before, .icon-up-circled.icon-right a:after { content: "\e75b"; height: inherit; } + +i.icon-up-circled:before { content: "\e75b"; height: inherit; } + +.icon-down-open.icon-left a:before, .icon-down-open.icon-right a:after { content: "\e75c"; height: inherit; } + +i.icon-down-open:before { content: "\e75c"; height: inherit; } + +.icon-left-open.icon-left a:before, .icon-left-open.icon-right a:after { content: "\e75d"; height: inherit; } + +i.icon-left-open:before { content: "\e75d"; height: inherit; } + +.icon-right-open.icon-left a:before, .icon-right-open.icon-right a:after { content: "\e75e"; height: inherit; } + +i.icon-right-open:before { content: "\e75e"; height: inherit; } + +.icon-up-open.icon-left a:before, .icon-up-open.icon-right a:after { content: "\e75f"; height: inherit; } + +i.icon-up-open:before { content: "\e75f"; height: inherit; } + +.icon-down-open-mini.icon-left a:before, .icon-down-open-mini.icon-right a:after { content: "\e760"; height: inherit; } + +i.icon-down-open-mini:before { content: "\e760"; height: inherit; } + +.icon-left-open-mini.icon-left a:before, .icon-left-open-mini.icon-right a:after { content: "\e761"; height: inherit; } + +i.icon-left-open-mini:before { content: "\e761"; height: inherit; } + +.icon-right-open-mini.icon-left a:before, .icon-right-open-mini.icon-right a:after { content: "\e762"; height: inherit; } + +i.icon-right-open-mini:before { content: "\e762"; height: inherit; } + +.icon-up-open-mini.icon-left a:before, .icon-up-open-mini.icon-right a:after { content: "\e763"; height: inherit; } + +i.icon-up-open-mini:before { content: "\e763"; height: inherit; } + +.icon-down-open-big.icon-left a:before, .icon-down-open-big.icon-right a:after { content: "\e764"; height: inherit; } + +i.icon-down-open-big:before { content: "\e764"; height: inherit; } + +.icon-left-open-big.icon-left a:before, .icon-left-open-big.icon-right a:after { content: "\e765"; height: inherit; } + +i.icon-left-open-big:before { content: "\e765"; height: inherit; } + +.icon-right-open-big.icon-left a:before, .icon-right-open-big.icon-right a:after { content: "\e766"; height: inherit; } + +i.icon-right-open-big:before { content: "\e766"; height: inherit; } + +.icon-up-open-big.icon-left a:before, .icon-up-open-big.icon-right a:after { content: "\e767"; height: inherit; } + +i.icon-up-open-big:before { content: "\e767"; height: inherit; } + +.icon-down.icon-left a:before, .icon-down.icon-right a:after { content: "\2b07"; height: inherit; } + +i.icon-down:before { content: "\2b07"; height: inherit; } + +.icon-arrow-left.icon-left a:before, .icon-arrow-left.icon-right a:after { content: "\2b05"; height: inherit; } + +i.icon-arrow-left:before { content: "\2b05"; height: inherit; } + +.icon-arrow-right.icon-left a:before, .icon-arrow-right.icon-right a:after { content: "\27a1"; height: inherit; } + +i.icon-arrow-right:before { content: "\27a1"; height: inherit; } + +.icon-up.icon-left a:before, .icon-up.icon-right a:after { content: "\2b06"; height: inherit; } + +i.icon-up:before { content: "\2b06"; height: inherit; } + +.icon-down-dir.icon-left a:before, .icon-down-dir.icon-right a:after { content: "\25be"; height: inherit; } + +i.icon-down-dir:before { content: "\25be"; height: inherit; } + +.icon-left-dir.icon-left a:before, .icon-left-dir.icon-right a:after { content: "\25c2"; height: inherit; } + +i.icon-left-dir:before { content: "\25c2"; height: inherit; } + +.icon-right-dir.icon-left a:before, .icon-right-dir.icon-right a:after { content: "\25b8"; height: inherit; } + +i.icon-right-dir:before { content: "\25b8"; height: inherit; } + +.icon-up-dir.icon-left a:before, .icon-up-dir.icon-right a:after { content: "\25b4"; height: inherit; } + +i.icon-up-dir:before { content: "\25b4"; height: inherit; } + +.icon-down-bold.icon-left a:before, .icon-down-bold.icon-right a:after { content: "\e4b0"; height: inherit; } + +i.icon-down-bold:before { content: "\e4b0"; height: inherit; } + +.icon-left-bold.icon-left a:before, .icon-left-bold.icon-right a:after { content: "\e4ad"; height: inherit; } + +i.icon-left-bold:before { content: "\e4ad"; height: inherit; } + +.icon-right-bold.icon-left a:before, .icon-right-bold.icon-right a:after { content: "\e4ae"; height: inherit; } + +i.icon-right-bold:before { content: "\e4ae"; height: inherit; } + +.icon-up-bold.icon-left a:before, .icon-up-bold.icon-right a:after { content: "\e4af"; height: inherit; } + +i.icon-up-bold:before { content: "\e4af"; height: inherit; } + +.icon-down-thin.icon-left a:before, .icon-down-thin.icon-right a:after { content: "\2193"; height: inherit; } + +i.icon-down-thin:before { content: "\2193"; height: inherit; } + +.icon-left-thin.icon-left a:before, .icon-left-thin.icon-right a:after { content: "\2190"; height: inherit; } + +i.icon-left-thin:before { content: "\2190"; height: inherit; } + +.icon-right-thin.icon-left a:before, .icon-right-thin.icon-right a:after { content: "\2192"; height: inherit; } + +i.icon-right-thin:before { content: "\2192"; height: inherit; } + +.icon-up-thin.icon-left a:before, .icon-up-thin.icon-right a:after { content: "\2191"; height: inherit; } + +i.icon-up-thin:before { content: "\2191"; height: inherit; } + +.icon-ccw.icon-left a:before, .icon-ccw.icon-right a:after { content: "\27f2"; height: inherit; } + +i.icon-ccw:before { content: "\27f2"; height: inherit; } + +.icon-cw.icon-left a:before, .icon-cw.icon-right a:after { content: "\27f3"; height: inherit; } + +i.icon-cw:before { content: "\27f3"; height: inherit; } + +.icon-arrows-ccw.icon-left a:before, .icon-arrows-ccw.icon-right a:after { content: "🔄"; height: inherit; } + +i.icon-arrows-ccw:before { content: "🔄"; height: inherit; } + +.icon-level-down.icon-left a:before, .icon-level-down.icon-right a:after { content: "\21b3"; height: inherit; } + +i.icon-level-down:before { content: "\21b3"; height: inherit; } + +.icon-level-up.icon-left a:before, .icon-level-up.icon-right a:after { content: "\21b0"; height: inherit; } + +i.icon-level-up:before { content: "\21b0"; height: inherit; } + +.icon-shuffle.icon-left a:before, .icon-shuffle.icon-right a:after { content: "🔀"; height: inherit; } + +i.icon-shuffle:before { content: "🔀"; height: inherit; } + +.icon-loop.icon-left a:before, .icon-loop.icon-right a:after { content: "🔁"; height: inherit; } + +i.icon-loop:before { content: "🔁"; height: inherit; } + +.icon-switch.icon-left a:before, .icon-switch.icon-right a:after { content: "\21c6"; height: inherit; } + +i.icon-switch:before { content: "\21c6"; height: inherit; } + +.icon-play.icon-left a:before, .icon-play.icon-right a:after { content: "\25b6"; height: inherit; } + +i.icon-play:before { content: "\25b6"; height: inherit; } + +.icon-stop.icon-left a:before, .icon-stop.icon-right a:after { content: "\25a0"; height: inherit; } + +i.icon-stop:before { content: "\25a0"; height: inherit; } + +.icon-pause.icon-left a:before, .icon-pause.icon-right a:after { content: "\2389"; height: inherit; } + +i.icon-pause:before { content: "\2389"; height: inherit; } + +.icon-record.icon-left a:before, .icon-record.icon-right a:after { content: "\26ab"; height: inherit; } + +i.icon-record:before { content: "\26ab"; height: inherit; } + +.icon-to-end.icon-left a:before, .icon-to-end.icon-right a:after { content: "\23ed"; height: inherit; } + +i.icon-to-end:before { content: "\23ed"; height: inherit; } + +.icon-to-start.icon-left a:before, .icon-to-start.icon-right a:after { content: "\23ee"; height: inherit; } + +i.icon-to-start:before { content: "\23ee"; height: inherit; } + +.icon-fast-forward.icon-left a:before, .icon-fast-forward.icon-right a:after { content: "\23e9"; height: inherit; } + +i.icon-fast-forward:before { content: "\23e9"; height: inherit; } + +.icon-fast-backward.icon-left a:before, .icon-fast-backward.icon-right a:after { content: "\23ea"; height: inherit; } + +i.icon-fast-backward:before { content: "\23ea"; height: inherit; } + +.icon-progress-0.icon-left a:before, .icon-progress-0.icon-right a:after { content: "\e768"; height: inherit; } + +i.icon-progress-0:before { content: "\e768"; height: inherit; } + +.icon-progress-1.icon-left a:before, .icon-progress-1.icon-right a:after { content: "\e769"; height: inherit; } + +i.icon-progress-1:before { content: "\e769"; height: inherit; } + +.icon-progress-2.icon-left a:before, .icon-progress-2.icon-right a:after { content: "\e76a"; height: inherit; } + +i.icon-progress-2:before { content: "\e76a"; height: inherit; } + +.icon-progress-3.icon-left a:before, .icon-progress-3.icon-right a:after { content: "\e76b"; height: inherit; } + +i.icon-progress-3:before { content: "\e76b"; height: inherit; } + +.icon-target.icon-left a:before, .icon-target.icon-right a:after { content: "🎯"; height: inherit; } + +i.icon-target:before { content: "🎯"; height: inherit; } + +.icon-palette.icon-left a:before, .icon-palette.icon-right a:after { content: "🎨"; height: inherit; } + +i.icon-palette:before { content: "🎨"; height: inherit; } + +.icon-list.icon-left a:before, .icon-list.icon-right a:after { content: "\e005"; height: inherit; } + +i.icon-list:before { content: "\e005"; height: inherit; } + +.icon-list-add.icon-left a:before, .icon-list-add.icon-right a:after { content: "\e003"; height: inherit; } + +i.icon-list-add:before { content: "\e003"; height: inherit; } + +.icon-signal.icon-left a:before, .icon-signal.icon-right a:after { content: "📶"; height: inherit; } + +i.icon-signal:before { content: "📶"; height: inherit; } + +.icon-trophy.icon-left a:before, .icon-trophy.icon-right a:after { content: "🏆"; height: inherit; } + +i.icon-trophy:before { content: "🏆"; height: inherit; } + +.icon-battery.icon-left a:before, .icon-battery.icon-right a:after { content: "🔋"; height: inherit; } + +i.icon-battery:before { content: "🔋"; height: inherit; } + +.icon-back-in-time.icon-left a:before, .icon-back-in-time.icon-right a:after { content: "\e771"; height: inherit; } + +i.icon-back-in-time:before { content: "\e771"; height: inherit; } + +.icon-monitor.icon-left a:before, .icon-monitor.icon-right a:after { content: "💻"; height: inherit; } + +i.icon-monitor:before { content: "💻"; height: inherit; } + +.icon-mobile.icon-left a:before, .icon-mobile.icon-right a:after { content: "📱"; height: inherit; } + +i.icon-mobile:before { content: "📱"; height: inherit; } + +.icon-network.icon-left a:before, .icon-network.icon-right a:after { content: "\e776"; height: inherit; } + +i.icon-network:before { content: "\e776"; height: inherit; } + +.icon-cd.icon-left a:before, .icon-cd.icon-right a:after { content: "💿"; height: inherit; } + +i.icon-cd:before { content: "💿"; height: inherit; } + +.icon-inbox.icon-left a:before, .icon-inbox.icon-right a:after { content: "\e777"; height: inherit; } + +i.icon-inbox:before { content: "\e777"; height: inherit; } + +.icon-install.icon-left a:before, .icon-install.icon-right a:after { content: "\e778"; height: inherit; } + +i.icon-install:before { content: "\e778"; height: inherit; } + +.icon-globe.icon-left a:before, .icon-globe.icon-right a:after { content: "🌎"; height: inherit; } + +i.icon-globe:before { content: "🌎"; height: inherit; } + +.icon-cloud.icon-left a:before, .icon-cloud.icon-right a:after { content: "\2601"; height: inherit; } + +i.icon-cloud:before { content: "\2601"; height: inherit; } + +.icon-cloud-thunder.icon-left a:before, .icon-cloud-thunder.icon-right a:after { content: "\26c8"; height: inherit; } + +i.icon-cloud-thunder:before { content: "\26c8"; height: inherit; } + +.icon-flash.icon-left a:before, .icon-flash.icon-right a:after { content: "\26a1"; height: inherit; } + +i.icon-flash:before { content: "\26a1"; height: inherit; } + +.icon-moon.icon-left a:before, .icon-moon.icon-right a:after { content: "\263d"; height: inherit; } + +i.icon-moon:before { content: "\263d"; height: inherit; } + +.icon-flight.icon-left a:before, .icon-flight.icon-right a:after { content: "\2708"; height: inherit; } + +i.icon-flight:before { content: "\2708"; height: inherit; } + +.icon-paper-plane.icon-left a:before, .icon-paper-plane.icon-right a:after { content: "\e79b"; height: inherit; } + +i.icon-paper-plane:before { content: "\e79b"; height: inherit; } + +.icon-leaf.icon-left a:before, .icon-leaf.icon-right a:after { content: "🍂"; height: inherit; } + +i.icon-leaf:before { content: "🍂"; height: inherit; } + +.icon-lifebuoy.icon-left a:before, .icon-lifebuoy.icon-right a:after { content: "\e788"; height: inherit; } + +i.icon-lifebuoy:before { content: "\e788"; height: inherit; } + +.icon-mouse.icon-left a:before, .icon-mouse.icon-right a:after { content: "\e789"; height: inherit; } + +i.icon-mouse:before { content: "\e789"; height: inherit; } + +.icon-briefcase.icon-left a:before, .icon-briefcase.icon-right a:after { content: "💼"; height: inherit; } + +i.icon-briefcase:before { content: "💼"; height: inherit; } + +.icon-suitcase.icon-left a:before, .icon-suitcase.icon-right a:after { content: "\e78e"; height: inherit; } + +i.icon-suitcase:before { content: "\e78e"; height: inherit; } + +.icon-dot.icon-left a:before, .icon-dot.icon-right a:after { content: "\e78b"; height: inherit; } + +i.icon-dot:before { content: "\e78b"; height: inherit; } + +.icon-dot-2.icon-left a:before, .icon-dot-2.icon-right a:after { content: "\e78c"; height: inherit; } + +i.icon-dot-2:before { content: "\e78c"; height: inherit; } + +.icon-dot-3.icon-left a:before, .icon-dot-3.icon-right a:after { content: "\e78d"; height: inherit; } + +i.icon-dot-3:before { content: "\e78d"; height: inherit; } + +.icon-brush.icon-left a:before, .icon-brush.icon-right a:after { content: "\e79a"; height: inherit; } + +i.icon-brush:before { content: "\e79a"; height: inherit; } + +.icon-magnet.icon-left a:before, .icon-magnet.icon-right a:after { content: "\e7a1"; height: inherit; } + +i.icon-magnet:before { content: "\e7a1"; height: inherit; } + +.icon-infinity.icon-left a:before, .icon-infinity.icon-right a:after { content: "\221e"; height: inherit; } + +i.icon-infinity:before { content: "\221e"; height: inherit; } + +.icon-erase.icon-left a:before, .icon-erase.icon-right a:after { content: "\232b"; height: inherit; } + +i.icon-erase:before { content: "\232b"; height: inherit; } + +.icon-chart-pie.icon-left a:before, .icon-chart-pie.icon-right a:after { content: "\e751"; height: inherit; } + +i.icon-chart-pie:before { content: "\e751"; height: inherit; } + +.icon-chart-line.icon-left a:before, .icon-chart-line.icon-right a:after { content: "📈"; height: inherit; } + +i.icon-chart-line:before { content: "📈"; height: inherit; } + +.icon-chart-bar.icon-left a:before, .icon-chart-bar.icon-right a:after { content: "📊"; height: inherit; } + +i.icon-chart-bar:before { content: "📊"; height: inherit; } + +.icon-chart-area.icon-left a:before, .icon-chart-area.icon-right a:after { content: "🔾"; height: inherit; } + +i.icon-chart-area:before { content: "🔾"; height: inherit; } + +.icon-tape.icon-left a:before, .icon-tape.icon-right a:after { content: "\2707"; height: inherit; } + +i.icon-tape:before { content: "\2707"; height: inherit; } + +.icon-graduation-cap.icon-left a:before, .icon-graduation-cap.icon-right a:after { content: "🎓"; height: inherit; } + +i.icon-graduation-cap:before { content: "🎓"; height: inherit; } + +.icon-language.icon-left a:before, .icon-language.icon-right a:after { content: "\e752"; height: inherit; } + +i.icon-language:before { content: "\e752"; height: inherit; } + +.icon-ticket.icon-left a:before, .icon-ticket.icon-right a:after { content: "🎫"; height: inherit; } + +i.icon-ticket:before { content: "🎫"; height: inherit; } + +.icon-water.icon-left a:before, .icon-water.icon-right a:after { content: "💦"; height: inherit; } + +i.icon-water:before { content: "💦"; height: inherit; } + +.icon-droplet.icon-left a:before, .icon-droplet.icon-right a:after { content: "💧"; height: inherit; } + +i.icon-droplet:before { content: "💧"; height: inherit; } + +.icon-air.icon-left a:before, .icon-air.icon-right a:after { content: "\e753"; height: inherit; } + +i.icon-air:before { content: "\e753"; height: inherit; } + +.icon-credit-card.icon-left a:before, .icon-credit-card.icon-right a:after { content: "💳"; height: inherit; } + +i.icon-credit-card:before { content: "💳"; height: inherit; } + +.icon-floppy.icon-left a:before, .icon-floppy.icon-right a:after { content: "💾"; height: inherit; } + +i.icon-floppy:before { content: "💾"; height: inherit; } + +.icon-clipboard.icon-left a:before, .icon-clipboard.icon-right a:after { content: "📋"; height: inherit; } + +i.icon-clipboard:before { content: "📋"; height: inherit; } + +.icon-megaphone.icon-left a:before, .icon-megaphone.icon-right a:after { content: "📣"; height: inherit; } + +i.icon-megaphone:before { content: "📣"; height: inherit; } + +.icon-database.icon-left a:before, .icon-database.icon-right a:after { content: "\e754"; height: inherit; } + +i.icon-database:before { content: "\e754"; height: inherit; } + +.icon-drive.icon-left a:before, .icon-drive.icon-right a:after { content: "\e755"; height: inherit; } + +i.icon-drive:before { content: "\e755"; height: inherit; } + +.icon-bucket.icon-left a:before, .icon-bucket.icon-right a:after { content: "\e756"; height: inherit; } + +i.icon-bucket:before { content: "\e756"; height: inherit; } + +.icon-thermometer.icon-left a:before, .icon-thermometer.icon-right a:after { content: "\e757"; height: inherit; } + +i.icon-thermometer:before { content: "\e757"; height: inherit; } + +.icon-key.icon-left a:before, .icon-key.icon-right a:after { content: "🔑"; height: inherit; } + +i.icon-key:before { content: "🔑"; height: inherit; } + +.icon-flow-cascade.icon-left a:before, .icon-flow-cascade.icon-right a:after { content: "\e790"; height: inherit; } + +i.icon-flow-cascade:before { content: "\e790"; height: inherit; } + +.icon-flow-branch.icon-left a:before, .icon-flow-branch.icon-right a:after { content: "\e791"; height: inherit; } + +i.icon-flow-branch:before { content: "\e791"; height: inherit; } + +.icon-flow-tree.icon-left a:before, .icon-flow-tree.icon-right a:after { content: "\e792"; height: inherit; } + +i.icon-flow-tree:before { content: "\e792"; height: inherit; } + +.icon-flow-line.icon-left a:before, .icon-flow-line.icon-right a:after { content: "\e793"; height: inherit; } + +i.icon-flow-line:before { content: "\e793"; height: inherit; } + +.icon-flow-parallel.icon-left a:before, .icon-flow-parallel.icon-right a:after { content: "\e794"; height: inherit; } + +i.icon-flow-parallel:before { content: "\e794"; height: inherit; } + +.icon-rocket.icon-left a:before, .icon-rocket.icon-right a:after { content: "🚀"; height: inherit; } + +i.icon-rocket:before { content: "🚀"; height: inherit; } + +.icon-gauge.icon-left a:before, .icon-gauge.icon-right a:after { content: "\e7a2"; height: inherit; } + +i.icon-gauge:before { content: "\e7a2"; height: inherit; } + +.icon-traffic-cone.icon-left a:before, .icon-traffic-cone.icon-right a:after { content: "\e7a3"; height: inherit; } + +i.icon-traffic-cone:before { content: "\e7a3"; height: inherit; } + +.icon-cc.icon-left a:before, .icon-cc.icon-right a:after { content: "\e7a5"; height: inherit; } + +i.icon-cc:before { content: "\e7a5"; height: inherit; } + +.icon-cc-by.icon-left a:before, .icon-cc-by.icon-right a:after { content: "\e7a6"; height: inherit; } + +i.icon-cc-by:before { content: "\e7a6"; height: inherit; } + +.icon-cc-nc.icon-left a:before, .icon-cc-nc.icon-right a:after { content: "\e7a7"; height: inherit; } + +i.icon-cc-nc:before { content: "\e7a7"; height: inherit; } + +.icon-cc-nc-eu.icon-left a:before, .icon-cc-nc-eu.icon-right a:after { content: "\e7a8"; height: inherit; } + +i.icon-cc-nc-eu:before { content: "\e7a8"; height: inherit; } + +.icon-cc-nc-jp.icon-left a:before, .icon-cc-nc-jp.icon-right a:after { content: "\e7a9"; height: inherit; } + +i.icon-cc-nc-jp:before { content: "\e7a9"; height: inherit; } + +.icon-cc-sa.icon-left a:before, .icon-cc-sa.icon-right a:after { content: "\e7aa"; height: inherit; } + +i.icon-cc-sa:before { content: "\e7aa"; height: inherit; } + +.icon-cc-nd.icon-left a:before, .icon-cc-nd.icon-right a:after { content: "\e7ab"; height: inherit; } + +i.icon-cc-nd:before { content: "\e7ab"; height: inherit; } + +.icon-cc-pd.icon-left a:before, .icon-cc-pd.icon-right a:after { content: "\e7ac"; height: inherit; } + +i.icon-cc-pd:before { content: "\e7ac"; height: inherit; } + +.icon-cc-zero.icon-left a:before, .icon-cc-zero.icon-right a:after { content: "\e7ad"; height: inherit; } + +i.icon-cc-zero:before { content: "\e7ad"; height: inherit; } + +.icon-cc-share.icon-left a:before, .icon-cc-share.icon-right a:after { content: "\e7ae"; height: inherit; } + +i.icon-cc-share:before { content: "\e7ae"; height: inherit; } + +.icon-cc-remix.icon-left a:before, .icon-cc-remix.icon-right a:after { content: "\e7af"; height: inherit; } + +i.icon-cc-remix:before { content: "\e7af"; height: inherit; } + +.icon-github.icon-left a:before, .icon-github.icon-right a:after { content: "\f300"; height: inherit; } + +i.icon-github:before { content: "\f300"; height: inherit; } + +.icon-github-circled.icon-left a:before, .icon-github-circled.icon-right a:after { content: "\f301"; height: inherit; } + +i.icon-github-circled:before { content: "\f301"; height: inherit; } + +.icon-flickr.icon-left a:before, .icon-flickr.icon-right a:after { content: "\f303"; height: inherit; } + +i.icon-flickr:before { content: "\f303"; height: inherit; } + +.icon-flickr-circled.icon-left a:before, .icon-flickr-circled.icon-right a:after { content: "\f304"; height: inherit; } + +i.icon-flickr-circled:before { content: "\f304"; height: inherit; } + +.icon-vimeo.icon-left a:before, .icon-vimeo.icon-right a:after { content: "\f306"; height: inherit; } + +i.icon-vimeo:before { content: "\f306"; height: inherit; } + +.icon-vimeo-circled.icon-left a:before, .icon-vimeo-circled.icon-right a:after { content: "\f307"; height: inherit; } + +i.icon-vimeo-circled:before { content: "\f307"; height: inherit; } + +.icon-twitter.icon-left a:before, .icon-twitter.icon-right a:after { content: "\f309"; height: inherit; } + +i.icon-twitter:before { content: "\f309"; height: inherit; } + +.icon-twitter-circled.icon-left a:before, .icon-twitter-circled.icon-right a:after { content: "\f30a"; height: inherit; } + +i.icon-twitter-circled:before { content: "\f30a"; height: inherit; } + +.icon-facebook.icon-left a:before, .icon-facebook.icon-right a:after { content: "\f30c"; height: inherit; } + +i.icon-facebook:before { content: "\f30c"; height: inherit; } + +.icon-facebook-circled.icon-left a:before, .icon-facebook-circled.icon-right a:after { content: "\f30d"; height: inherit; } + +i.icon-facebook-circled:before { content: "\f30d"; height: inherit; } + +.icon-facebook-squared.icon-left a:before, .icon-facebook-squared.icon-right a:after { content: "\f30e"; height: inherit; } + +i.icon-facebook-squared:before { content: "\f30e"; height: inherit; } + +.icon-gplus.icon-left a:before, .icon-gplus.icon-right a:after { content: "\f30f"; height: inherit; } + +i.icon-gplus:before { content: "\f30f"; height: inherit; } + +.icon-gplus-circled.icon-left a:before, .icon-gplus-circled.icon-right a:after { content: "\f310"; height: inherit; } + +i.icon-gplus-circled:before { content: "\f310"; height: inherit; } + +.icon-pinterest.icon-left a:before, .icon-pinterest.icon-right a:after { content: "\f312"; height: inherit; } + +i.icon-pinterest:before { content: "\f312"; height: inherit; } + +.icon-pinterest-circled.icon-left a:before, .icon-pinterest-circled.icon-right a:after { content: "\f313"; height: inherit; } + +i.icon-pinterest-circled:before { content: "\f313"; height: inherit; } + +.icon-tumblr.icon-left a:before, .icon-tumblr.icon-right a:after { content: "\f315"; height: inherit; } + +i.icon-tumblr:before { content: "\f315"; height: inherit; } + +.icon-tumblr-circled.icon-left a:before, .icon-tumblr-circled.icon-right a:after { content: "\f316"; height: inherit; } + +i.icon-tumblr-circled:before { content: "\f316"; height: inherit; } + +.icon-linkedin.icon-left a:before, .icon-linkedin.icon-right a:after { content: "\f318"; height: inherit; } + +i.icon-linkedin:before { content: "\f318"; height: inherit; } + +.icon-linkedin-circled.icon-left a:before, .icon-linkedin-circled.icon-right a:after { content: "\f319"; height: inherit; } + +i.icon-linkedin-circled:before { content: "\f319"; height: inherit; } + +.icon-dribbble.icon-left a:before, .icon-dribbble.icon-right a:after { content: "\f31b"; height: inherit; } + +i.icon-dribbble:before { content: "\f31b"; height: inherit; } + +.icon-dribbble-circled.icon-left a:before, .icon-dribbble-circled.icon-right a:after { content: "\f31c"; height: inherit; } + +i.icon-dribbble-circled:before { content: "\f31c"; height: inherit; } + +.icon-stumbleupon.icon-left a:before, .icon-stumbleupon.icon-right a:after { content: "\f31e"; height: inherit; } + +i.icon-stumbleupon:before { content: "\f31e"; height: inherit; } + +.icon-stumbleupon-circled.icon-left a:before, .icon-stumbleupon-circled.icon-right a:after { content: "\f31f"; height: inherit; } + +i.icon-stumbleupon-circled:before { content: "\f31f"; height: inherit; } + +.icon-lastfm.icon-left a:before, .icon-lastfm.icon-right a:after { content: "\f321"; height: inherit; } + +i.icon-lastfm:before { content: "\f321"; height: inherit; } + +.icon-lastfm-circled.icon-left a:before, .icon-lastfm-circled.icon-right a:after { content: "\f322"; height: inherit; } + +i.icon-lastfm-circled:before { content: "\f322"; height: inherit; } + +.icon-rdio.icon-left a:before, .icon-rdio.icon-right a:after { content: "\f324"; height: inherit; } + +i.icon-rdio:before { content: "\f324"; height: inherit; } + +.icon-rdio-circled.icon-left a:before, .icon-rdio-circled.icon-right a:after { content: "\f325"; height: inherit; } + +i.icon-rdio-circled:before { content: "\f325"; height: inherit; } + +.icon-spotify.icon-left a:before, .icon-spotify.icon-right a:after { content: "\f327"; height: inherit; } + +i.icon-spotify:before { content: "\f327"; height: inherit; } + +.icon-spotify-circled.icon-left a:before, .icon-spotify-circled.icon-right a:after { content: "\f328"; height: inherit; } + +i.icon-spotify-circled:before { content: "\f328"; height: inherit; } + +.icon-qq.icon-left a:before, .icon-qq.icon-right a:after { content: "\f32a"; height: inherit; } + +i.icon-qq:before { content: "\f32a"; height: inherit; } + +.icon-instagram.icon-left a:before, .icon-instagram.icon-right a:after { content: "\f32d"; height: inherit; } + +i.icon-instagram:before { content: "\f32d"; height: inherit; } + +.icon-dropbox.icon-left a:before, .icon-dropbox.icon-right a:after { content: "\f330"; height: inherit; } + +i.icon-dropbox:before { content: "\f330"; height: inherit; } + +.icon-evernote.icon-left a:before, .icon-evernote.icon-right a:after { content: "\f333"; height: inherit; } + +i.icon-evernote:before { content: "\f333"; height: inherit; } + +.icon-flattr.icon-left a:before, .icon-flattr.icon-right a:after { content: "\f336"; height: inherit; } + +i.icon-flattr:before { content: "\f336"; height: inherit; } + +.icon-skype.icon-left a:before, .icon-skype.icon-right a:after { content: "\f339"; height: inherit; } + +i.icon-skype:before { content: "\f339"; height: inherit; } + +.icon-skype-circled.icon-left a:before, .icon-skype-circled.icon-right a:after { content: "\f33a"; height: inherit; } + +i.icon-skype-circled:before { content: "\f33a"; height: inherit; } + +.icon-renren.icon-left a:before, .icon-renren.icon-right a:after { content: "\f33c"; height: inherit; } + +i.icon-renren:before { content: "\f33c"; height: inherit; } + +.icon-sina-weibo.icon-left a:before, .icon-sina-weibo.icon-right a:after { content: "\f33f"; height: inherit; } + +i.icon-sina-weibo:before { content: "\f33f"; height: inherit; } + +.icon-paypal.icon-left a:before, .icon-paypal.icon-right a:after { content: "\f342"; height: inherit; } + +i.icon-paypal:before { content: "\f342"; height: inherit; } + +.icon-picasa.icon-left a:before, .icon-picasa.icon-right a:after { content: "\f345"; height: inherit; } + +i.icon-picasa:before { content: "\f345"; height: inherit; } + +.icon-soundcloud.icon-left a:before, .icon-soundcloud.icon-right a:after { content: "\f348"; height: inherit; } + +i.icon-soundcloud:before { content: "\f348"; height: inherit; } + +.icon-mixi.icon-left a:before, .icon-mixi.icon-right a:after { content: "\f34b"; height: inherit; } + +i.icon-mixi:before { content: "\f34b"; height: inherit; } + +.icon-behance.icon-left a:before, .icon-behance.icon-right a:after { content: "\f34e"; height: inherit; } + +i.icon-behance:before { content: "\f34e"; height: inherit; } + +.icon-google-circles.icon-left a:before, .icon-google-circles.icon-right a:after { content: "\f351"; height: inherit; } + +i.icon-google-circles:before { content: "\f351"; height: inherit; } + +.icon-vkontakte.icon-left a:before, .icon-vkontakte.icon-right a:after { content: "\f354"; height: inherit; } + +i.icon-vkontakte:before { content: "\f354"; height: inherit; } + +.icon-smashing.icon-left a:before, .icon-smashing.icon-right a:after { content: "\f357"; height: inherit; } + +i.icon-smashing:before { content: "\f357"; height: inherit; } + +.icon-sweden.icon-left a:before, .icon-sweden.icon-right a:after { content: "\f601"; height: inherit; } + +i.icon-sweden:before { content: "\f601"; height: inherit; } + +.icon-db-shape.icon-left a:before, .icon-db-shape.icon-right a:after { content: "\f600"; height: inherit; } + +i.icon-db-shape:before { content: "\f600"; height: inherit; } + +.icon-logo-db.icon-left a:before, .icon-logo-db.icon-right a:after { content: "\f603"; height: inherit; } + +i.icon-logo-db:before { content: "\f603"; height: inherit; } + +.fixed { position: fixed; } +.fixed.pinned { position: absolute; } +@media only screen and (max-width: 768px) { .fixed { position: relative !important; top: auto !important; left: auto !important; } } + +.unfixed { position: relative !important; top: auto !important; left: auto !important; } + +.text-center { text-align: center; } + +.text-left { text-align: left; } + +.text-right { text-align: right; } + +/* Fonts */ +@font-face { font-family: "entypo"; font-style: normal; font-weight: 400; src: url(../fonts/icons/entypo.eot); src: url("../fonts/icons/entypo.eot?#iefix") format("ie9-skip-eot"), url("../fonts/icons/entypo.woff") format("woff"), url("../fonts/icons/entypo.ttf") format("truetype"); } + +@font-face { font-family: "entypo"; font-style: normal; font-weight: 400; src: url("./../../fonts/icons/entypo.eot"); src: url("./../../fonts/icons/entypo.eot?#iefix") format("ie9-skip-eot"), url("./../../fonts/icons/entypo.woff") format("woff"), url("./../../fonts/icons/entypo.ttf") format("truetype"); } + +@font-face { font-family: "Merriweather Sans"; font-style: normal; font-weight: 400; src: url("./../../fonts/font_1_pathRegular.ttf") format("truetype"); } + +@font-face { font-family: "Merriweather Sans"; font-style: italic; font-weight: 400; src: url("./../../fonts/Merriweather_Sans/MerriweatherSans-Italic.ttf") format("truetype"); } + +@font-face { font-family: "Merriweather Sans"; font-style: normal; font-weight: 300; src: url("./../../fonts/Merriweather_Sans/MerriweatherSans-Light.ttf") format("truetype"); } + +@font-face { font-family: "Merriweather Sans"; font-style: italic; font-weight: 300; src: url("./../../fonts/Merriweather_Sans/MerriweatherSans-LigthItalic.ttf") format("truetype"); } + +@font-face { font-family: "Merriweather Sans"; font-style: normal; font-weight: 700; src: url("./../../fonts/Merriweather_Sans/MerriweatherSans-Bold.ttf") format("truetype"); } + +@font-face { font-family: "Merriweather Sans"; font-style: italic; font-weight: 700; src: url("./../../fonts/Merriweather_Sans/MerriweatherSans-BoldItalic.ttf") format("truetype"); } + +@font-face { font-family: "Merriweather Sans"; font-style: normal; font-weight: 800; src: url("./../../fonts/Merriweather_Sans/MerriweatherSans-ExtraBold.ttf") format("truetype"); } + +@font-face { font-family: "Merriweather Sans"; font-style: italic; font-weight: 800; src: url("./../../fonts/Merriweather_Sans/MerriweatherSans-ExtraBoldItalic.ttf") format("truetype"); } + +@font-face { font-family: "Oxygen"; font-style: normal; font-weight: 300; src: url("./../../fonts/Oxygen/Oxygen-Light.ttf") format("truetype"); } + +@font-face { font-family: "Oxygen"; font-style: normal; font-weight: 400; src: url("./../../fonts/Oxygen/Oxygen-Regular.ttf") format("truetype"); } + +@font-face { font-family: "Oxygen"; font-style: normal; font-weight: 700; src: url("./../../fonts/Oxygen/Oxygen-Bold.ttf") format("truetype"); } + +h1, h2, h3, h4, h5, h6 { font-family: "Oxygen"; font-weight: 300; color: #444444; text-rendering: optimizeLegibility; padding-top: 0.273em; line-height: 1.15538em; padding-bottom: 0.273em; } +h1 a, h2 a, h3 a, h4 a, h5 a, h6 a { color: #d04526; } + +@media only screen and (max-width: 767px) { h1, h2, h3, h4, h5, h6 { word-wrap: break-word; } } +h1 { font-size: 68px; font-size: 4.25rem; } +h1.xlarge { font-size: 110px; font-size: 6.875rem; } +h1.xxlarge { font-size: 126px; font-size: 7.875rem; } +h1.absurd { font-size: 177px; font-size: 11.0625rem; } + +h2 { font-size: 42px; font-size: 2.625rem; } + +h3 { font-size: 30px; font-size: 1.875rem; } + +h4 { font-size: 26px; font-size: 1.625rem; } + +h5 { font-size: 18px; font-size: 1.125rem; } + +h6 { font-size: 16px; font-size: 1rem; } + +@media only screen and (max-width: 767px) { h1 { font-size: 42px; font-size: 2.625rem; } + h2 { font-size: 36px; font-size: 2.25rem; } } +.subhead { color: #777; font-weight: normal; margin-bottom: 20px; } + +/*===================================================== Links & Paragraph styles ======================================================*/ +p { font-family: "Oxygen"; font-weight: 400; font-size: 16px; font-size: 1rem; margin-bottom: 13px; line-height: 1.625em; } +p.lead { font-size: 20px; font-size: 1.25rem; margin-bottom: 18px; } +@media only screen and (max-width: 768px) { p { font-size: 17.6px; font-size: 1.1rem; line-height: 1.625em; } } + +a { color: #d04526; text-decoration: none; outline: 0; line-height: inherit; } +a:hover { color: #c03d20; } + +/*===================================================== + Lists ======================================================*/ +ul, ol { margin-bottom: 0.273em; } + +ul { list-style: none outside; } + +ol { list-style: decimal; margin-left: 30px; } + +ul.square, ul.circle, ul.disc { margin-left: 25px; } +ul.square { list-style: square outside; } +ul.circle { list-style: circle outside; } +ul.disc { list-style: disc outside; } +ul ul { margin: 4px 0 5px 25px; } + +ol ol { margin: 4px 0 5px 30px; } + +li { padding-bottom: 0.273em; } + +ul.large li { line-height: 21px; } + +dl dt { font-weight: bold; font-size: 16px; font-size: 1rem; } + +@media only screen and (max-width: 768px) { ul, ol, dl, p { text-align: left; } } +/* Mobile */ +em { font-style: italic; line-height: inherit; } + +strong { font-weight: 700; line-height: inherit; } + +small { font-size: 56.4%; line-height: inherit; } + +h1 small, h2 small, h3 small, h4 small, h5 small { color: #777; } + +/* Blockquotes */ +blockquote { line-height: 20px; color: #777; margin: 0 0 18px; padding: 9px 20px 0 19px; border-left: 5px solid #cccccc; } +blockquote p { line-height: 20px; color: #777; } +blockquote cite { display: block; font-size: 12px; font-size: 1.2rem; color: #555555; } +blockquote cite:before { content: "\2014 \0020"; } +blockquote cite a { color: #555555; } +blockquote cite a:visited { color: #555555; } + +hr { border: 1px solid #cccccc; clear: both; margin: 16px 0 18px; height: 0; } + +abbr, acronym { text-transform: uppercase; font-size: 90%; color: #222; border-bottom: 1px solid #cccccc; cursor: help; } + +abbr { text-transform: none; } + +/** Print styles. Inlined to avoid required HTTP connection: www.phpied.com/delay-loading-your-print-css/ Credit to Paul Irish and HTML5 Boilerplate (html5boilerplate.com) */ +@media print { * { background: transparent !important; color: black !important; text-shadow: none !important; filter: none !important; -ms-filter: none !important; } + /* Black prints faster: sanbeiji.com/archives/953 */ + p a { color: #555555 !important; text-decoration: underline; } + p a:visited { color: #555555 !important; text-decoration: underline; } + p a[href]:after { content: " (" attr(href) ")"; } + abbr[title]:after { content: " (" attr(title) ")"; } + .ir a:after { content: ""; } + a[href^="javascript:"]:after, a[href^="#"]:after { content: ""; } + /* Don't show links for images, or javascript/internal links */ + pre, blockquote { border: 1px solid #999; page-break-inside: avoid; } + thead { display: table-header-group; } + /* css-discuss.incutio.com/wiki/Printing_Tables */ + tr, img { page-break-inside: avoid; } + @page { margin: 0.5cm; } + p, h2, h3 { orphans: 3; widows: 3; } + h2, h3 { page-break-after: avoid; } } +/*================================================= + +++ LE GRID +++ A Responsive Grid -- Gumby defaults to a standard 960 grid, but you can change it to whatever you'd like. + ==================================================*/ +/*.container { padding: 0 $gutter-in-px; +}*/ +.row { width: 100%; max-width: 980px; min-width: 320px; margin: 0 auto; padding-left: 20px; padding-right: 20px; } +.row .row { min-width: 0; padding-left: 0; padding-right: 0; } + +/* To fix the grid into a different size, set max-width to your desired width */ +.column, .columns { margin-left: 2.12766%; float: left; min-height: 1px; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } + +.column:first-child, .columns:first-child, .alpha { margin-left: 0; } + +.column.omega, .columns.omega { float: right; } + +/* Column Classes */ +.row .one.column { width: 6.38298%; } +.row .one.columns { width: 6.38298%; } +.row .two.columns { width: 14.89362%; } +.row .three.columns { width: 23.40426%; } +.row .four.columns { width: 31.91489%; } +.row .five.columns { width: 40.42553%; } +.row .six.columns { width: 48.93617%; } +.row .seven.columns { width: 57.44681%; } +.row .eight.columns { width: 65.95745%; } +.row .nine.columns { width: 74.46809%; } +.row .ten.columns { width: 82.97872%; } +.row .eleven.columns { width: 91.48936%; } +.row .twelve.columns { width: 100%; } + +/* Push and Pull Classes */ +.row .push_one { margin-left: 10.6383%; } +.row .push_one:first-child { margin-left: 8.51064%; } +.row .pull_one.one.column { margin-left: -14.89362%; } +.row .pull_one.one.column:first-child { margin-left: 0; } +.row .pull_one.two.columns { margin-left: -23.40426%; } +.row .pull_one.two.columns:first-child { margin-left: 0; } +.row .pull_one.three.columns { margin-left: -31.91489%; } +.row .pull_one.three.columns:first-child { margin-left: 0; } +.row .pull_one.four.columns { margin-left: -40.42553%; } +.row .pull_one.four.columns:first-child { margin-left: 0; } +.row .pull_one.five.columns { margin-left: -48.93617%; } +.row .pull_one.five.columns:first-child { margin-left: 0; } +.row .pull_one.six.columns { margin-left: -57.44681%; } +.row .pull_one.six.columns:first-child { margin-left: 0; } +.row .pull_one.seven.columns { margin-left: -65.95745%; } +.row .pull_one.seven.columns:first-child { margin-left: 0; } +.row .pull_one.eight.columns { margin-left: -74.46809%; } +.row .pull_one.eight.columns:first-child { margin-left: 0; } +.row .pull_one.nine.columns { margin-left: -82.97872%; } +.row .pull_one.nine.columns:first-child { margin-left: 0; } +.row .pull_one.ten.columns { margin-left: -91.48936%; } +.row .pull_one.ten.columns:first-child { margin-left: 0; } +.row .pull_one.eleven.columns { margin-left: -100.0%; } +.row .pull_one.eleven.columns:first-child { margin-left: 0; } +.row .push_two { margin-left: 19.14894%; } +.row .push_two:first-child { margin-left: 17.02128%; } +.row .pull_two.one.column { margin-left: -23.40426%; } +.row .pull_two.one.column:first-child { margin-left: 0; } +.row .pull_two.two.columns { margin-left: -31.91489%; } +.row .pull_two.two.columns:first-child { margin-left: 0; } +.row .pull_two.three.columns { margin-left: -40.42553%; } +.row .pull_two.three.columns:first-child { margin-left: 0; } +.row .pull_two.four.columns { margin-left: -48.93617%; } +.row .pull_two.four.columns:first-child { margin-left: 0; } +.row .pull_two.five.columns { margin-left: -57.44681%; } +.row .pull_two.five.columns:first-child { margin-left: 0; } +.row .pull_two.six.columns { margin-left: -65.95745%; } +.row .pull_two.six.columns:first-child { margin-left: 0; } +.row .pull_two.seven.columns { margin-left: -74.46809%; } +.row .pull_two.seven.columns:first-child { margin-left: 0; } +.row .pull_two.eight.columns { margin-left: -82.97872%; } +.row .pull_two.eight.columns:first-child { margin-left: 0; } +.row .pull_two.nine.columns { margin-left: -91.48936%; } +.row .pull_two.nine.columns:first-child { margin-left: 0; } +.row .pull_two.ten.columns { margin-left: -100.0%; } +.row .pull_two.ten.columns:first-child { margin-left: 0; } +.row .pull_two.eleven.columns { margin-left: -108.51064%; } +.row .pull_two.eleven.columns:first-child { margin-left: 0; } +.row .push_three { margin-left: 27.65957%; } +.row .push_three:first-child { margin-left: 25.53191%; } +.row .pull_three.one.column { margin-left: -31.91489%; } +.row .pull_three.one.column:first-child { margin-left: 0; } +.row .pull_three.two.columns { margin-left: -40.42553%; } +.row .pull_three.two.columns:first-child { margin-left: 0; } +.row .pull_three.three.columns { margin-left: -48.93617%; } +.row .pull_three.three.columns:first-child { margin-left: 0; } +.row .pull_three.four.columns { margin-left: -57.44681%; } +.row .pull_three.four.columns:first-child { margin-left: 0; } +.row .pull_three.five.columns { margin-left: -65.95745%; } +.row .pull_three.five.columns:first-child { margin-left: 0; } +.row .pull_three.six.columns { margin-left: -74.46809%; } +.row .pull_three.six.columns:first-child { margin-left: 0; } +.row .pull_three.seven.columns { margin-left: -82.97872%; } +.row .pull_three.seven.columns:first-child { margin-left: 0; } +.row .pull_three.eight.columns { margin-left: -91.48936%; } +.row .pull_three.eight.columns:first-child { margin-left: 0; } +.row .pull_three.nine.columns { margin-left: -100.0%; } +.row .pull_three.nine.columns:first-child { margin-left: 0; } +.row .pull_three.ten.columns { margin-left: -108.51064%; } +.row .pull_three.ten.columns:first-child { margin-left: 0; } +.row .pull_three.eleven.columns { margin-left: -117.02128%; } +.row .pull_three.eleven.columns:first-child { margin-left: 0; } +.row .push_four { margin-left: 36.17021%; } +.row .push_four:first-child { margin-left: 34.04255%; } +.row .pull_four.one.column { margin-left: -40.42553%; } +.row .pull_four.one.column:first-child { margin-left: 0; } +.row .pull_four.two.columns { margin-left: -48.93617%; } +.row .pull_four.two.columns:first-child { margin-left: 0; } +.row .pull_four.three.columns { margin-left: -57.44681%; } +.row .pull_four.three.columns:first-child { margin-left: 0; } +.row .pull_four.four.columns { margin-left: -65.95745%; } +.row .pull_four.four.columns:first-child { margin-left: 0; } +.row .pull_four.five.columns { margin-left: -74.46809%; } +.row .pull_four.five.columns:first-child { margin-left: 0; } +.row .pull_four.six.columns { margin-left: -82.97872%; } +.row .pull_four.six.columns:first-child { margin-left: 0; } +.row .pull_four.seven.columns { margin-left: -91.48936%; } +.row .pull_four.seven.columns:first-child { margin-left: 0; } +.row .pull_four.eight.columns { margin-left: -100.0%; } +.row .pull_four.eight.columns:first-child { margin-left: 0; } +.row .pull_four.nine.columns { margin-left: -108.51064%; } +.row .pull_four.nine.columns:first-child { margin-left: 0; } +.row .pull_four.ten.columns { margin-left: -117.02128%; } +.row .pull_four.ten.columns:first-child { margin-left: 0; } +.row .pull_four.eleven.columns { margin-left: -125.53191%; } +.row .pull_four.eleven.columns:first-child { margin-left: 0; } +.row .push_five { margin-left: 44.68085%; } +.row .push_five:first-child { margin-left: 42.55319%; } +.row .pull_five.one.column { margin-left: -48.93617%; } +.row .pull_five.one.column:first-child { margin-left: 0; } +.row .pull_five.two.columns { margin-left: -57.44681%; } +.row .pull_five.two.columns:first-child { margin-left: 0; } +.row .pull_five.three.columns { margin-left: -65.95745%; } +.row .pull_five.three.columns:first-child { margin-left: 0; } +.row .pull_five.four.columns { margin-left: -74.46809%; } +.row .pull_five.four.columns:first-child { margin-left: 0; } +.row .pull_five.five.columns { margin-left: -82.97872%; } +.row .pull_five.five.columns:first-child { margin-left: 0; } +.row .pull_five.six.columns { margin-left: -91.48936%; } +.row .pull_five.six.columns:first-child { margin-left: 0; } +.row .pull_five.seven.columns { margin-left: -100.0%; } +.row .pull_five.seven.columns:first-child { margin-left: 0; } +.row .pull_five.eight.columns { margin-left: -108.51064%; } +.row .pull_five.eight.columns:first-child { margin-left: 0; } +.row .pull_five.nine.columns { margin-left: -117.02128%; } +.row .pull_five.nine.columns:first-child { margin-left: 0; } +.row .pull_five.ten.columns { margin-left: -125.53191%; } +.row .pull_five.ten.columns:first-child { margin-left: 0; } +.row .pull_five.eleven.columns { margin-left: -134.04255%; } +.row .pull_five.eleven.columns:first-child { margin-left: 0; } +.row .push_six { margin-left: 53.19149%; } +.row .push_six:first-child { margin-left: 51.06383%; } +.row .pull_six.one.column { margin-left: -57.44681%; } +.row .pull_six.one.column:first-child { margin-left: 0; } +.row .pull_six.two.columns { margin-left: -65.95745%; } +.row .pull_six.two.columns:first-child { margin-left: 0; } +.row .pull_six.three.columns { margin-left: -74.46809%; } +.row .pull_six.three.columns:first-child { margin-left: 0; } +.row .pull_six.four.columns { margin-left: -82.97872%; } +.row .pull_six.four.columns:first-child { margin-left: 0; } +.row .pull_six.five.columns { margin-left: -91.48936%; } +.row .pull_six.five.columns:first-child { margin-left: 0; } +.row .pull_six.six.columns { margin-left: -100.0%; } +.row .pull_six.six.columns:first-child { margin-left: 0; } +.row .pull_six.seven.columns { margin-left: -108.51064%; } +.row .pull_six.seven.columns:first-child { margin-left: 0; } +.row .pull_six.eight.columns { margin-left: -117.02128%; } +.row .pull_six.eight.columns:first-child { margin-left: 0; } +.row .pull_six.nine.columns { margin-left: -125.53191%; } +.row .pull_six.nine.columns:first-child { margin-left: 0; } +.row .pull_six.ten.columns { margin-left: -134.04255%; } +.row .pull_six.ten.columns:first-child { margin-left: 0; } +.row .pull_six.eleven.columns { margin-left: -142.55319%; } +.row .pull_six.eleven.columns:first-child { margin-left: 0; } +.row .push_seven { margin-left: 61.70213%; } +.row .push_seven:first-child { margin-left: 59.57447%; } +.row .pull_seven.one.column { margin-left: -65.95745%; } +.row .pull_seven.one.column:first-child { margin-left: 0; } +.row .pull_seven.two.columns { margin-left: -74.46809%; } +.row .pull_seven.two.columns:first-child { margin-left: 0; } +.row .pull_seven.three.columns { margin-left: -82.97872%; } +.row .pull_seven.three.columns:first-child { margin-left: 0; } +.row .pull_seven.four.columns { margin-left: -91.48936%; } +.row .pull_seven.four.columns:first-child { margin-left: 0; } +.row .pull_seven.five.columns { margin-left: -100.0%; } +.row .pull_seven.five.columns:first-child { margin-left: 0; } +.row .pull_seven.six.columns { margin-left: -108.51064%; } +.row .pull_seven.six.columns:first-child { margin-left: 0; } +.row .pull_seven.seven.columns { margin-left: -117.02128%; } +.row .pull_seven.seven.columns:first-child { margin-left: 0; } +.row .pull_seven.eight.columns { margin-left: -125.53191%; } +.row .pull_seven.eight.columns:first-child { margin-left: 0; } +.row .pull_seven.nine.columns { margin-left: -134.04255%; } +.row .pull_seven.nine.columns:first-child { margin-left: 0; } +.row .pull_seven.ten.columns { margin-left: -142.55319%; } +.row .pull_seven.ten.columns:first-child { margin-left: 0; } +.row .pull_seven.eleven.columns { margin-left: -151.06383%; } +.row .pull_seven.eleven.columns:first-child { margin-left: 0; } +.row .push_eight { margin-left: 70.21277%; } +.row .push_eight:first-child { margin-left: 68.08511%; } +.row .pull_eight.one.column { margin-left: -74.46809%; } +.row .pull_eight.one.column:first-child { margin-left: 0; } +.row .pull_eight.two.columns { margin-left: -82.97872%; } +.row .pull_eight.two.columns:first-child { margin-left: 0; } +.row .pull_eight.three.columns { margin-left: -91.48936%; } +.row .pull_eight.three.columns:first-child { margin-left: 0; } +.row .pull_eight.four.columns { margin-left: -100.0%; } +.row .pull_eight.four.columns:first-child { margin-left: 0; } +.row .pull_eight.five.columns { margin-left: -108.51064%; } +.row .pull_eight.five.columns:first-child { margin-left: 0; } +.row .pull_eight.six.columns { margin-left: -117.02128%; } +.row .pull_eight.six.columns:first-child { margin-left: 0; } +.row .pull_eight.seven.columns { margin-left: -125.53191%; } +.row .pull_eight.seven.columns:first-child { margin-left: 0; } +.row .pull_eight.eight.columns { margin-left: -134.04255%; } +.row .pull_eight.eight.columns:first-child { margin-left: 0; } +.row .pull_eight.nine.columns { margin-left: -142.55319%; } +.row .pull_eight.nine.columns:first-child { margin-left: 0; } +.row .pull_eight.ten.columns { margin-left: -151.06383%; } +.row .pull_eight.ten.columns:first-child { margin-left: 0; } +.row .pull_eight.eleven.columns { margin-left: -159.57447%; } +.row .pull_eight.eleven.columns:first-child { margin-left: 0; } +.row .push_nine { margin-left: 78.7234%; } +.row .push_nine:first-child { margin-left: 76.59574%; } +.row .pull_nine.one.column { margin-left: -82.97872%; } +.row .pull_nine.one.column:first-child { margin-left: 0; } +.row .pull_nine.two.columns { margin-left: -91.48936%; } +.row .pull_nine.two.columns:first-child { margin-left: 0; } +.row .pull_nine.three.columns { margin-left: -100.0%; } +.row .pull_nine.three.columns:first-child { margin-left: 0; } +.row .pull_nine.four.columns { margin-left: -108.51064%; } +.row .pull_nine.four.columns:first-child { margin-left: 0; } +.row .pull_nine.five.columns { margin-left: -117.02128%; } +.row .pull_nine.five.columns:first-child { margin-left: 0; } +.row .pull_nine.six.columns { margin-left: -125.53191%; } +.row .pull_nine.six.columns:first-child { margin-left: 0; } +.row .pull_nine.seven.columns { margin-left: -134.04255%; } +.row .pull_nine.seven.columns:first-child { margin-left: 0; } +.row .pull_nine.eight.columns { margin-left: -142.55319%; } +.row .pull_nine.eight.columns:first-child { margin-left: 0; } +.row .pull_nine.nine.columns { margin-left: -151.06383%; } +.row .pull_nine.nine.columns:first-child { margin-left: 0; } +.row .pull_nine.ten.columns { margin-left: -159.57447%; } +.row .pull_nine.ten.columns:first-child { margin-left: 0; } +.row .pull_nine.eleven.columns { margin-left: -168.08511%; } +.row .pull_nine.eleven.columns:first-child { margin-left: 0; } +.row .push_ten { margin-left: 87.23404%; } +.row .push_ten:first-child { margin-left: 85.10638%; } +.row .pull_ten.one.column { margin-left: -91.48936%; } +.row .pull_ten.one.column:first-child { margin-left: 0; } +.row .pull_ten.two.columns { margin-left: -100.0%; } +.row .pull_ten.two.columns:first-child { margin-left: 0; } +.row .pull_ten.three.columns { margin-left: -108.51064%; } +.row .pull_ten.three.columns:first-child { margin-left: 0; } +.row .pull_ten.four.columns { margin-left: -117.02128%; } +.row .pull_ten.four.columns:first-child { margin-left: 0; } +.row .pull_ten.five.columns { margin-left: -125.53191%; } +.row .pull_ten.five.columns:first-child { margin-left: 0; } +.row .pull_ten.six.columns { margin-left: -134.04255%; } +.row .pull_ten.six.columns:first-child { margin-left: 0; } +.row .pull_ten.seven.columns { margin-left: -142.55319%; } +.row .pull_ten.seven.columns:first-child { margin-left: 0; } +.row .pull_ten.eight.columns { margin-left: -151.06383%; } +.row .pull_ten.eight.columns:first-child { margin-left: 0; } +.row .pull_ten.nine.columns { margin-left: -159.57447%; } +.row .pull_ten.nine.columns:first-child { margin-left: 0; } +.row .pull_ten.ten.columns { margin-left: -168.08511%; } +.row .pull_ten.ten.columns:first-child { margin-left: 0; } +.row .pull_ten.eleven.columns { margin-left: -176.59574%; } +.row .pull_ten.eleven.columns:first-child { margin-left: 0; } +.row .push_eleven { margin-left: 95.74468%; } +.row .push_eleven:first-child { margin-left: 93.61702%; } +.row .pull_eleven.one.column { margin-left: -100.0%; } +.row .pull_eleven.one.column:first-child { margin-left: 0; } +.row .pull_eleven.two.columns { margin-left: -108.51064%; } +.row .pull_eleven.two.columns:first-child { margin-left: 0; } +.row .pull_eleven.three.columns { margin-left: -117.02128%; } +.row .pull_eleven.three.columns:first-child { margin-left: 0; } +.row .pull_eleven.four.columns { margin-left: -125.53191%; } +.row .pull_eleven.four.columns:first-child { margin-left: 0; } +.row .pull_eleven.five.columns { margin-left: -134.04255%; } +.row .pull_eleven.five.columns:first-child { margin-left: 0; } +.row .pull_eleven.six.columns { margin-left: -142.55319%; } +.row .pull_eleven.six.columns:first-child { margin-left: 0; } +.row .pull_eleven.seven.columns { margin-left: -151.06383%; } +.row .pull_eleven.seven.columns:first-child { margin-left: 0; } +.row .pull_eleven.eight.columns { margin-left: -159.57447%; } +.row .pull_eleven.eight.columns:first-child { margin-left: 0; } +.row .pull_eleven.nine.columns { margin-left: -168.08511%; } +.row .pull_eleven.nine.columns:first-child { margin-left: 0; } +.row .pull_eleven.ten.columns { margin-left: -176.59574%; } +.row .pull_eleven.ten.columns:first-child { margin-left: 0; } +.row .pull_eleven.eleven.columns { margin-left: -185.10638%; } +.row .pull_eleven.eleven.columns:first-child { margin-left: 0; } + +/* Centered Classes */ +.row .one.centered { margin-left: 46.80851%; } +.row .two.centered { margin-left: 42.55319%; } +.row .three.centered { margin-left: 38.29787%; } +.row .four.centered { margin-left: 34.04255%; } +.row .five.centered { margin-left: 29.78723%; } +.row .six.centered { margin-left: 25.53191%; } +.row .seven.centered { margin-left: 21.2766%; } +.row .eight.centered { margin-left: 17.02128%; } +.row .nine.centered { margin-left: 12.76596%; } +.row .ten.centered { margin-left: 8.51064%; } +.row .eleven.centered { margin-left: 4.25532%; } + +/* Hybrid Grid Columns */ +.sixteen.colgrid .row .one.column { width: 4.25532%; } +.sixteen.colgrid .row .one.columns { width: 4.25532%; } +.sixteen.colgrid .row .two.columns { width: 10.6383%; } +.sixteen.colgrid .row .three.columns { width: 17.02128%; } +.sixteen.colgrid .row .four.columns { width: 23.40426%; } +.sixteen.colgrid .row .five.columns { width: 29.78723%; } +.sixteen.colgrid .row .six.columns { width: 36.17021%; } +.sixteen.colgrid .row .seven.columns { width: 42.55319%; } +.sixteen.colgrid .row .eight.columns { width: 48.93617%; } +.sixteen.colgrid .row .nine.columns { width: 55.31915%; } +.sixteen.colgrid .row .ten.columns { width: 61.70213%; } +.sixteen.colgrid .row .eleven.columns { width: 68.08511%; } +.sixteen.colgrid .row .twelve.columns { width: 74.46809%; } +.sixteen.colgrid .row .thirteen.columns { width: 80.85106%; } +.sixteen.colgrid .row .fourteen.columns { width: 87.23404%; } +.sixteen.colgrid .row .fifteen.columns { width: 93.61702%; } +.sixteen.colgrid .row .sixteen.columns { width: 100%; } + +/* Hybrid Push and Pull Classes */ +.sixteen.colgrid .row .push_one { margin-left: 8.51064%; } +.sixteen.colgrid .row .push_one:first-child { margin-left: 6.38298%; } +.sixteen.colgrid .row .pull_one.one.column { margin-left: -10.6383%; } +.sixteen.colgrid .row .pull_one.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.two.columns { margin-left: -17.02128%; } +.sixteen.colgrid .row .pull_one.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.three.columns { margin-left: -23.40426%; } +.sixteen.colgrid .row .pull_one.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.four.columns { margin-left: -29.78723%; } +.sixteen.colgrid .row .pull_one.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.five.columns { margin-left: -36.17021%; } +.sixteen.colgrid .row .pull_one.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.six.columns { margin-left: -42.55319%; } +.sixteen.colgrid .row .pull_one.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.seven.columns { margin-left: -48.93617%; } +.sixteen.colgrid .row .pull_one.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.eight.columns { margin-left: -55.31915%; } +.sixteen.colgrid .row .pull_one.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.nine.columns { margin-left: -61.70213%; } +.sixteen.colgrid .row .pull_one.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.ten.columns { margin-left: -68.08511%; } +.sixteen.colgrid .row .pull_one.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.eleven.columns { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_one.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.twelve.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_one.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.thirteen.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_one.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.fourteen.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_one.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_one.fifteen.columns { margin-left: -100%; } +.sixteen.colgrid .row .pull_one.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_two { margin-left: 14.89362%; } +.sixteen.colgrid .row .push_two:first-child { margin-left: 12.76596%; } +.sixteen.colgrid .row .pull_two.one.column { margin-left: -17.02128%; } +.sixteen.colgrid .row .pull_two.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.two.columns { margin-left: -23.40426%; } +.sixteen.colgrid .row .pull_two.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.three.columns { margin-left: -29.78723%; } +.sixteen.colgrid .row .pull_two.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.four.columns { margin-left: -36.17021%; } +.sixteen.colgrid .row .pull_two.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.five.columns { margin-left: -42.55319%; } +.sixteen.colgrid .row .pull_two.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.six.columns { margin-left: -48.93617%; } +.sixteen.colgrid .row .pull_two.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.seven.columns { margin-left: -55.31915%; } +.sixteen.colgrid .row .pull_two.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.eight.columns { margin-left: -61.70213%; } +.sixteen.colgrid .row .pull_two.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.nine.columns { margin-left: -68.08511%; } +.sixteen.colgrid .row .pull_two.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.ten.columns { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_two.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.eleven.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_two.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.twelve.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_two.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.thirteen.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_two.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.fourteen.columns { margin-left: -100%; } +.sixteen.colgrid .row .pull_two.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_two.fifteen.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_two.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_three { margin-left: 21.2766%; } +.sixteen.colgrid .row .push_three:first-child { margin-left: 19.14894%; } +.sixteen.colgrid .row .pull_three.one.column { margin-left: -23.40426%; } +.sixteen.colgrid .row .pull_three.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.two.columns { margin-left: -29.78723%; } +.sixteen.colgrid .row .pull_three.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.three.columns { margin-left: -36.17021%; } +.sixteen.colgrid .row .pull_three.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.four.columns { margin-left: -42.55319%; } +.sixteen.colgrid .row .pull_three.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.five.columns { margin-left: -48.93617%; } +.sixteen.colgrid .row .pull_three.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.six.columns { margin-left: -55.31915%; } +.sixteen.colgrid .row .pull_three.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.seven.columns { margin-left: -61.70213%; } +.sixteen.colgrid .row .pull_three.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.eight.columns { margin-left: -68.08511%; } +.sixteen.colgrid .row .pull_three.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.nine.columns { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_three.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.ten.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_three.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.eleven.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_three.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.twelve.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_three.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.thirteen.columns { margin-left: -100%; } +.sixteen.colgrid .row .pull_three.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.fourteen.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_three.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_three.fifteen.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_three.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_four { margin-left: 27.65957%; } +.sixteen.colgrid .row .push_four:first-child { margin-left: 25.53191%; } +.sixteen.colgrid .row .pull_four.one.column { margin-left: -29.78723%; } +.sixteen.colgrid .row .pull_four.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.two.columns { margin-left: -36.17021%; } +.sixteen.colgrid .row .pull_four.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.three.columns { margin-left: -42.55319%; } +.sixteen.colgrid .row .pull_four.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.four.columns { margin-left: -48.93617%; } +.sixteen.colgrid .row .pull_four.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.five.columns { margin-left: -55.31915%; } +.sixteen.colgrid .row .pull_four.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.six.columns { margin-left: -61.70213%; } +.sixteen.colgrid .row .pull_four.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.seven.columns { margin-left: -68.08511%; } +.sixteen.colgrid .row .pull_four.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.eight.columns { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_four.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.nine.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_four.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.ten.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_four.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.eleven.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_four.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.twelve.columns { margin-left: -100%; } +.sixteen.colgrid .row .pull_four.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.thirteen.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_four.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.fourteen.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_four.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_four.fifteen.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_four.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_five { margin-left: 34.04255%; } +.sixteen.colgrid .row .push_five:first-child { margin-left: 31.91489%; } +.sixteen.colgrid .row .pull_five.one.column { margin-left: -36.17021%; } +.sixteen.colgrid .row .pull_five.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.two.columns { margin-left: -42.55319%; } +.sixteen.colgrid .row .pull_five.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.three.columns { margin-left: -48.93617%; } +.sixteen.colgrid .row .pull_five.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.four.columns { margin-left: -55.31915%; } +.sixteen.colgrid .row .pull_five.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.five.columns { margin-left: -61.70213%; } +.sixteen.colgrid .row .pull_five.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.six.columns { margin-left: -68.08511%; } +.sixteen.colgrid .row .pull_five.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.seven.columns { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_five.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.eight.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_five.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.nine.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_five.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.ten.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_five.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.eleven.columns { margin-left: -100%; } +.sixteen.colgrid .row .pull_five.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.twelve.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_five.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.thirteen.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_five.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.fourteen.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_five.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_five.fifteen.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_five.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_six { margin-left: 40.42553%; } +.sixteen.colgrid .row .push_six:first-child { margin-left: 38.29787%; } +.sixteen.colgrid .row .pull_six.one.column { margin-left: -42.55319%; } +.sixteen.colgrid .row .pull_six.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.two.columns { margin-left: -48.93617%; } +.sixteen.colgrid .row .pull_six.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.three.columns { margin-left: -55.31915%; } +.sixteen.colgrid .row .pull_six.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.four.columns { margin-left: -61.70213%; } +.sixteen.colgrid .row .pull_six.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.five.columns { margin-left: -68.08511%; } +.sixteen.colgrid .row .pull_six.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.six.columns { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_six.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.seven.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_six.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.eight.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_six.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.nine.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_six.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.ten.columns { margin-left: -100.0%; } +.sixteen.colgrid .row .pull_six.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.eleven.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_six.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.twelve.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_six.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.thirteen.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_six.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.fourteen.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_six.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_six.fifteen.columns { margin-left: -131.91489%; } +.sixteen.colgrid .row .pull_six.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_seven { margin-left: 46.80851%; } +.sixteen.colgrid .row .push_seven:first-child { margin-left: 44.68085%; } +.sixteen.colgrid .row .pull_seven.one.column { margin-left: -48.93617%; } +.sixteen.colgrid .row .pull_seven.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.two.columns { margin-left: -55.31915%; } +.sixteen.colgrid .row .pull_seven.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.three.columns { margin-left: -61.70213%; } +.sixteen.colgrid .row .pull_seven.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.four.columns { margin-left: -68.08511%; } +.sixteen.colgrid .row .pull_seven.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.five.columns { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_seven.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.six.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_seven.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.seven.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_seven.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.eight.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_seven.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.nine.columns { margin-left: -100.0%; } +.sixteen.colgrid .row .pull_seven.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.ten.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_seven.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.eleven.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_seven.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.twelve.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_seven.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.thirteen.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_seven.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.fourteen.columns { margin-left: -131.91489%; } +.sixteen.colgrid .row .pull_seven.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_seven.fifteen.columns { margin-left: -138.29787%; } +.sixteen.colgrid .row .pull_seven.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_eight { margin-left: 53.19149%; } +.sixteen.colgrid .row .push_eight:first-child { margin-left: 51.06383%; } +.sixteen.colgrid .row .pull_eight.one.column { margin-left: -55.31915%; } +.sixteen.colgrid .row .pull_eight.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.two.columns { margin-left: -61.70213%; } +.sixteen.colgrid .row .pull_eight.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.three.columns { margin-left: -68.08511%; } +.sixteen.colgrid .row .pull_eight.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.four.columns { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_eight.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.five.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_eight.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.six.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_eight.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.seven.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_eight.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.eight.columns { margin-left: -100%; } +.sixteen.colgrid .row .pull_eight.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.nine.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_eight.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.ten.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_eight.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.eleven.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_eight.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.twelve.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_eight.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.thirteen.columns { margin-left: -131.91489%; } +.sixteen.colgrid .row .pull_eight.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.fourteen.columns { margin-left: -138.29787%; } +.sixteen.colgrid .row .pull_eight.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eight.fifteen.columns { margin-left: -144.68085%; } +.sixteen.colgrid .row .pull_eight.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_nine { margin-left: 59.57447%; } +.sixteen.colgrid .row .push_nine:first-child { margin-left: 57.44681%; } +.sixteen.colgrid .row .pull_nine.one.column { margin-left: -61.70213%; } +.sixteen.colgrid .row .pull_nine.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.two.columns { margin-left: -68.08511%; } +.sixteen.colgrid .row .pull_nine.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.three.columns { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_nine.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.four.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_nine.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.five.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_nine.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.six.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_nine.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.seven.columns { margin-left: -100%; } +.sixteen.colgrid .row .pull_nine.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.eight.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_nine.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.nine.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_nine.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.ten.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_nine.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.eleven.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_nine.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.twelve.columns { margin-left: -131.91489%; } +.sixteen.colgrid .row .pull_nine.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.thirteen.columns { margin-left: -138.29787%; } +.sixteen.colgrid .row .pull_nine.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.fourteen.columns { margin-left: -144.68085%; } +.sixteen.colgrid .row .pull_nine.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_nine.fifteen.columns { margin-left: -151.06383%; } +.sixteen.colgrid .row .pull_nine.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_ten { margin-left: 65.95745%; } +.sixteen.colgrid .row .push_ten:first-child { margin-left: 63.82979%; } +.sixteen.colgrid .row .pull_ten.one.column { margin-left: -68.08511%; } +.sixteen.colgrid .row .pull_ten.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.two.columns { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_ten.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.three.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_ten.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.four.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_ten.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.five.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_ten.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.six.columns { margin-left: -100%; } +.sixteen.colgrid .row .pull_ten.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.seven.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_ten.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.eight.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_ten.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.nine.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_ten.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.ten.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_ten.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.eleven.columns { margin-left: -131.91489%; } +.sixteen.colgrid .row .pull_ten.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.twelve.columns { margin-left: -138.29787%; } +.sixteen.colgrid .row .pull_ten.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.thirteen.columns { margin-left: -144.68085%; } +.sixteen.colgrid .row .pull_ten.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.fourteen.columns { margin-left: -151.06383%; } +.sixteen.colgrid .row .pull_ten.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_ten.fifteen.columns { margin-left: -157.44681%; } +.sixteen.colgrid .row .pull_ten.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_eleven { margin-left: 72.34043%; } +.sixteen.colgrid .row .push_eleven:first-child { margin-left: 70.21277%; } +.sixteen.colgrid .row .pull_eleven.one.column { margin-left: -74.46809%; } +.sixteen.colgrid .row .pull_eleven.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.two.columns { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_eleven.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.three.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_eleven.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.four.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_eleven.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.five.columns { margin-left: -100%; } +.sixteen.colgrid .row .pull_eleven.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.six.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_eleven.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.seven.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_eleven.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.eight.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_eleven.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.nine.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_eleven.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.ten.columns { margin-left: -131.91489%; } +.sixteen.colgrid .row .pull_eleven.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.eleven.columns { margin-left: -138.29787%; } +.sixteen.colgrid .row .pull_eleven.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.twelve.columns { margin-left: -144.68085%; } +.sixteen.colgrid .row .pull_eleven.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.thirteen.columns { margin-left: -151.06383%; } +.sixteen.colgrid .row .pull_eleven.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.fourteen.columns { margin-left: -157.44681%; } +.sixteen.colgrid .row .pull_eleven.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_eleven.fifteen.columns { margin-left: -163.82979%; } +.sixteen.colgrid .row .pull_eleven.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_twelve { margin-left: 78.7234%; } +.sixteen.colgrid .row .push_twelve:first-child { margin-left: 76.59574%; } +.sixteen.colgrid .row .pull_twelve.one.column { margin-left: -80.85106%; } +.sixteen.colgrid .row .pull_twelve.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.two.columns { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_twelve.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.three.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_twelve.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.four.columns { margin-left: -100.0%; } +.sixteen.colgrid .row .pull_twelve.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.five.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_twelve.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.six.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_twelve.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.seven.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_twelve.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.eight.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_twelve.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.nine.columns { margin-left: -131.91489%; } +.sixteen.colgrid .row .pull_twelve.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.ten.columns { margin-left: -138.29787%; } +.sixteen.colgrid .row .pull_twelve.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.eleven.columns { margin-left: -144.68085%; } +.sixteen.colgrid .row .pull_twelve.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.twelve.columns { margin-left: -151.06383%; } +.sixteen.colgrid .row .pull_twelve.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.thirteen.columns { margin-left: -157.44681%; } +.sixteen.colgrid .row .pull_twelve.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.fourteen.columns { margin-left: -163.82979%; } +.sixteen.colgrid .row .pull_twelve.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_twelve.fifteen.columns { margin-left: -170.21277%; } +.sixteen.colgrid .row .pull_twelve.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_thirteen { margin-left: 85.10638%; } +.sixteen.colgrid .row .push_thirteen:first-child { margin-left: 82.97872%; } +.sixteen.colgrid .row .pull_thirteen.one.column { margin-left: -87.23404%; } +.sixteen.colgrid .row .pull_thirteen.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.two.columns { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_thirteen.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.three.columns { margin-left: -100.0%; } +.sixteen.colgrid .row .pull_thirteen.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.four.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_thirteen.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.five.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_thirteen.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.six.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_thirteen.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.seven.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_thirteen.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.eight.columns { margin-left: -131.91489%; } +.sixteen.colgrid .row .pull_thirteen.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.nine.columns { margin-left: -138.29787%; } +.sixteen.colgrid .row .pull_thirteen.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.ten.columns { margin-left: -144.68085%; } +.sixteen.colgrid .row .pull_thirteen.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.eleven.columns { margin-left: -151.06383%; } +.sixteen.colgrid .row .pull_thirteen.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.twelve.columns { margin-left: -157.44681%; } +.sixteen.colgrid .row .pull_thirteen.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.thirteen.columns { margin-left: -163.82979%; } +.sixteen.colgrid .row .pull_thirteen.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.fourteen.columns { margin-left: -170.21277%; } +.sixteen.colgrid .row .pull_thirteen.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_thirteen.fifteen.columns { margin-left: -176.59574%; } +.sixteen.colgrid .row .pull_thirteen.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_fourteen { margin-left: 91.48936%; } +.sixteen.colgrid .row .push_fourteen:first-child { margin-left: 89.3617%; } +.sixteen.colgrid .row .pull_fourteen.one.column { margin-left: -93.61702%; } +.sixteen.colgrid .row .pull_fourteen.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.two.columns { margin-left: -100%; } +.sixteen.colgrid .row .pull_fourteen.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.three.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_fourteen.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.four.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_fourteen.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.five.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_fourteen.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.six.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_fourteen.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.seven.columns { margin-left: -131.91489%; } +.sixteen.colgrid .row .pull_fourteen.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.eight.columns { margin-left: -138.29787%; } +.sixteen.colgrid .row .pull_fourteen.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.nine.columns { margin-left: -144.68085%; } +.sixteen.colgrid .row .pull_fourteen.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.ten.columns { margin-left: -151.06383%; } +.sixteen.colgrid .row .pull_fourteen.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.eleven.columns { margin-left: -157.44681%; } +.sixteen.colgrid .row .pull_fourteen.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.twelve.columns { margin-left: -163.82979%; } +.sixteen.colgrid .row .pull_fourteen.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.thirteen.columns { margin-left: -170.21277%; } +.sixteen.colgrid .row .pull_fourteen.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.fourteen.columns { margin-left: -176.59574%; } +.sixteen.colgrid .row .pull_fourteen.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fourteen.fifteen.columns { margin-left: -182.97872%; } +.sixteen.colgrid .row .pull_fourteen.fifteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .push_fifteen { margin-left: 97.87234%; } +.sixteen.colgrid .row .push_fifteen:first-child { margin-left: 95.74468%; } +.sixteen.colgrid .row .pull_fifteen.one.column { margin-left: -100%; } +.sixteen.colgrid .row .pull_fifteen.one.column:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.two.columns { margin-left: -106.38298%; } +.sixteen.colgrid .row .pull_fifteen.two.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.three.columns { margin-left: -112.76596%; } +.sixteen.colgrid .row .pull_fifteen.three.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.four.columns { margin-left: -119.14894%; } +.sixteen.colgrid .row .pull_fifteen.four.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.five.columns { margin-left: -125.53191%; } +.sixteen.colgrid .row .pull_fifteen.five.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.six.columns { margin-left: -131.91489%; } +.sixteen.colgrid .row .pull_fifteen.six.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.seven.columns { margin-left: -138.29787%; } +.sixteen.colgrid .row .pull_fifteen.seven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.eight.columns { margin-left: -144.68085%; } +.sixteen.colgrid .row .pull_fifteen.eight.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.nine.columns { margin-left: -151.06383%; } +.sixteen.colgrid .row .pull_fifteen.nine.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.ten.columns { margin-left: -157.44681%; } +.sixteen.colgrid .row .pull_fifteen.ten.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.eleven.columns { margin-left: -163.82979%; } +.sixteen.colgrid .row .pull_fifteen.eleven.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.twelve.columns { margin-left: -170.21277%; } +.sixteen.colgrid .row .pull_fifteen.twelve.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.thirteen.columns { margin-left: -176.59574%; } +.sixteen.colgrid .row .pull_fifteen.thirteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.fourteen.columns { margin-left: -182.97872%; } +.sixteen.colgrid .row .pull_fifteen.fourteen.columns:first-child { margin-left: 0; } +.sixteen.colgrid .row .pull_fifteen.fifteen.columns { margin-left: -189.3617%; } +.sixteen.colgrid .row .pull_fifteen.fifteen.columns:first-child { margin-left: 0; } + +/* Hybrid Centered Classes */ +.sixteen.colgrid .row .one.centered { margin-left: 47.87234%; } +.sixteen.colgrid .row .two.centered { margin-left: 44.68085%; } +.sixteen.colgrid .row .three.centered { margin-left: 41.48936%; } +.sixteen.colgrid .row .four.centered { margin-left: 38.29787%; } +.sixteen.colgrid .row .five.centered { margin-left: 35.10638%; } +.sixteen.colgrid .row .six.centered { margin-left: 31.91489%; } +.sixteen.colgrid .row .seven.centered { margin-left: 28.7234%; } +.sixteen.colgrid .row .eight.centered { margin-left: 25.53191%; } +.sixteen.colgrid .row .nine.centered { margin-left: 22.34043%; } +.sixteen.colgrid .row .ten.centered { margin-left: 19.14894%; } +.sixteen.colgrid .row .eleven.centered { margin-left: 15.95745%; } +.sixteen.colgrid .row .twelve.centered { margin-left: 12.76596%; } +.sixteen.colgrid .row .thirteen.centered { margin-left: 9.57447%; } +.sixteen.colgrid .row .fourteen.centered { margin-left: 6.38298%; } +.sixteen.colgrid .row .fifteen.centered { margin-left: 3.19149%; } + +img, object, embed { max-width: 100%; height: auto; } + +img { -ms-interpolation-mode: bicubic; } + +#map_canvas img, .map_canvas img { max-width: none !important; } + +/* Tile Grid */ +.tiles { display: block; overflow: hidden; } +.tiles > li { display: block; height: auto; float: left; padding-bottom: 0; } +.tiles.two_up { margin-left: -4%; } +.tiles.two_up > li { margin-left: 4%; width: 46%; } +.tiles.three_up, .tiles.four_up { margin-left: -2%; } +.tiles.three_up > li { margin-left: 2%; width: 31.3%; } +.tiles.four_up > li { margin-left: 2%; width: 23%; } +.tiles.five_up { margin-left: -1.5%; } +.tiles.five_up > li { margin-left: 1.5%; width: 18.5%; } + +/* Nicolas Gallagher's micro clearfix */ +.clearfix { *zoom: 1; } +.clearfix:before, .clearfix:after { content: ""; display: table; } +.clearfix:after { clear: both; } + +.row { *zoom: 1; } +.row:before, .row:after { content: ""; display: table; } +.row:after { clear: both; } + +.valign { display: table; width: 100%; } +.valign > div, .valign > article, .valign > section, .valign > figure { display: table-cell; vertical-align: middle; } + +/* Mobile */ +@media only screen and (max-width: 767px) { body { -webkit-text-size-adjust: none; -ms-text-size-adjust: none; width: 100%; min-width: 0; } + .container { min-width: 0; margin-left: 0; margin-right: 0; } + .row { width: 100%; min-width: 0; margin-left: 0; margin-right: 0; } + .row .row .column, .row .row .columns { padding: 0; } + .row .centered { margin-left: 0 !important; } + .column, .columns { width: auto !important; float: none; margin-left: 0; margin-right: 0; } + .column:last-child, .columns:last-child { margin-right: 0; float: none; } + [class*="column"] + [class*="column"]:last-child { float: none; } + [class*="column"]:before { display: table; } + [class*="column"]:after { display: table; clear: both; } + [class^="push_"], [class*="push_"], [class^="pull_"], [class*="pull_"] { margin-left: 0 !important; } } +/*===================================================== + Navigation (with dropdowns) + ======================================================*/ +.navbar { width: 100%; min-height: 60px; display: block; margin-bottom: 20px; background: #4a4d50; position: relative; } +@media only screen and (max-width: 767px) { .navbar { border: none; } + .navbar .column, .navbar .columns { min-height: 0; } } +.navbar.fixed { position: fixed; z-index: 99999; } +.navbar.pinned { position: absolute; } +.navbar a.toggle { display: none; } +@media only screen and (max-width: 767px) { .navbar a.toggle { top: 18%; right: 4%; width: 46px; position: absolute; text-align: center; display: inline-block; color: white; background: #4a4d50; height: 40px; line-height: 38px; -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; font-size: 30px; font-size: 1.875rem; } + .navbar a.toggle:hover { background: #565a5d; } + .navbar a.toggle:active, .navbar a.toggle.active { background: #3e4043; } } + +.navbar .logo { display: inline-block; margin: 0 2.12766% 0 0; padding: 0; height: 60px; line-height: 58px; } +.navbar .logo a { display: block; padding: 0; overflow: hidden; height: 60px; line-height: 58px; } +.navbar .logo a img { max-height: 95%; } +@media only screen and (max-width: 767px) { .navbar .logo { float: left; display: inline; } + .navbar .logo a { padding: 0; } + .navbar .logo a img { width: auto; height: auto; max-width: 100%; } } + +.navbar ul { display: table; vertical-align: middle; margin: 0; float: none; } +@media only screen and (max-width: 767px) { .navbar ul { position: absolute; display: block; width: 100% !important; height: 0; max-height: 0; top: 60px; left: 0; overflow: hidden; text-align: center; background: #3e4043; } + .navbar ul.active { height: auto; max-height: 600px; z-index: 999998; -webkit-transition-duration: 0.5s; -moz-transition-duration: 0.5s; -o-transition-duration: 0.5s; transition-duration: 0.5s; -webkit-box-shadow: 0 2px 2px #252728; -moz-box-shadow: 0 2px 2px #252728; box-shadow: 0 2px 2px #252728; } } +.navbar ul li { display: table-cell; text-align: center; padding-bottom: 0; margin: 0; height: 60px; line-height: 58px; } +@media only screen and (max-width: 767px) { .navbar ul li { display: block; position: relative; min-height: 50px; max-height: 320px; height: auto; width: 100%; border-right: 0 !important; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; -webkit-transition-duration: 0.5s; -moz-transition-duration: 0.5s; -o-transition-duration: 0.5s; transition-duration: 0.5s; } } +.navbar ul li > a { display: block; padding: 0 16px; white-space: nowrap; color: white; text-shadow: 0 1px 2px #191a1b, 0 1px 0 #191a1b; height: 60px; line-height: 58px; font-size: 16px; font-size: 1rem; } +.navbar ul li > a i.icon-popup { position: absolute; } +.navbar ul li .btn { border-color: #000101 !important; } +.navbar ul li.field { margin-bottom: 0 !important; margin-right: 0; } +@media only screen and (max-width: 767px) { .navbar ul li.field { padding: 0 20px; } } +.navbar ul li.field input.search { background: #191a1b; border: none; color: #f2f2f2; } +.navbar ul li .dropdown { width: auto; min-width: 0; max-width: 320px; height: 0; position: absolute; background: #fafafa; overflow: hidden; z-index: 999; } +@media only screen and (max-width: 767px) { .navbar ul li .dropdown { width: 100%; max-width: 100%; position: relative; -webkit-box-shadow: none !important; -moz-box-shadow: none !important; box-shadow: none !important; } + .navbar ul li.active .dropdown { border-bottom: 1px solid #313436; } + .navbar ul li.active .dropdown ul { position: relative; top: 0; background: #36393b; min-height: 50px; max-height: 250px; height: auto; overflow: auto; -webkit-box-shadow: none !important; -moz-box-shadow: none !important; box-shadow: none !important; } + .navbar ul li.active .dropdown ul li { min-height: 50px; border-bottom: #3e4043; } + .navbar ul li.active .dropdown ul li a { color: white; border-bottom: 1px solid #313436; } + .navbar ul li.active .dropdown ul li a:hover { color: #d04526; } } + +@media only screen and (min-width: 768px) and (max-width: 939px) { .navbar > ul > li > .btn a { padding: 0 10px 0 10px !important; } + .navbar ul > li .dropdown ul li.active .dropdown { left: -320px; } } + +.navcontain { height: 80px; } +@media only screen and (max-width: 768px) { .navcontain { height: auto; } } + +.pretty.navbar { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #7b8085), color-stop(100%, #313436)); background-image: -webkit-linear-gradient(#7b8085, #313436); background-image: -moz-linear-gradient(#7b8085, #313436); background-image: -o-linear-gradient(#7b8085, #313436); background-image: linear-gradient(#7b8085, #313436); -webkit-box-shadow: inset 0 1px 1px #7b8085, 0 1px 2px rgba(0, 0, 0, 0.8) !important; -moz-box-shadow: inset 0 1px 1px #7b8085, 0 1px 2px rgba(0, 0, 0, 0.8) !important; box-shadow: inset 0 1px 1px #7b8085, 0 1px 2px rgba(0, 0, 0, 0.8) !important; /* Remove this line if you dont want a dropshadow on your navigation*/ } +@media only screen and (max-width: 767px) { .pretty.navbar a.toggle { border: 1px solid #3e4043; background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #7b8085), color-stop(100%, #4a4d50)); background-image: -webkit-linear-gradient(#7b8085, #4a4d50); background-image: -moz-linear-gradient(#7b8085, #4a4d50); background-image: -o-linear-gradient(#7b8085, #4a4d50); background-image: linear-gradient(#7b8085, #4a4d50); -webkit-box-shadow: inset 0 1px 2px #888d91, inset 0 -1px 1px #565a5d, inset 1px 0 1px #565a5d, inset -1px 0 1px #565a5d, 0 1px 1px #63676a; -moz-box-shadow: inset 0 1px 2px #888d91, inset 0 -1px 1px #565a5d, inset 1px 0 1px #565a5d, inset -1px 0 1px #565a5d, 0 1px 1px #63676a; box-shadow: inset 0 1px 2px #888d91, inset 0 -1px 1px #565a5d, inset 1px 0 1px #565a5d, inset -1px 0 1px #565a5d, 0 1px 1px #63676a; } + .pretty.navbar a.toggle i { text-shadow: 0 1px 1px #191a1b; } + .pretty.navbar a.toggle:hover { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #888d91), color-stop(100%, #565a5d)); background-image: -webkit-linear-gradient(#888d91, #565a5d); background-image: -moz-linear-gradient(#888d91, #565a5d); background-image: -o-linear-gradient(#888d91, #565a5d); background-image: linear-gradient(#888d91, #565a5d); } + .pretty.navbar a.toggle:active, .pretty.navbar a.toggle.active { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #3e4043), color-stop(100%, #4a4d50)); background-image: -webkit-linear-gradient(#3e4043, #4a4d50); background-image: -moz-linear-gradient(#3e4043, #4a4d50); background-image: -o-linear-gradient(#3e4043, #4a4d50); background-image: linear-gradient(#3e4043, #4a4d50); -webkit-box-shadow: 0 1px 1px #63676a; -moz-box-shadow: 0 1px 1px #63676a; box-shadow: 0 1px 1px #63676a; } } +.pretty.navbar.row { -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; } +@media only screen and (max-width: 767px) { .pretty.navbar.row { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; } } +.pretty.navbar ul li.field input.search { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #191a1b), color-stop(100%, #4f5255)); background-image: -webkit-linear-gradient(#191a1b, #4f5255); background-image: -moz-linear-gradient(#191a1b, #4f5255); background-image: -o-linear-gradient(#191a1b, #4f5255); background-image: linear-gradient(#191a1b, #4f5255); border: none; -webkit-box-shadow: 0 1px 2px #888d91 !important; -moz-box-shadow: 0 1px 2px #888d91 !important; box-shadow: 0 1px 2px #888d91 !important; /* Remove this line if you dont want a dropshadow on your navigation*/ } +.pretty.navbar > ul > li:first-child, .pretty.navbar .pretty.navbar > ul > li:first-child a:hover { -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } + +.navbar li .dropdown { width: auto; min-width: 0; max-width: 320px; height: 0; position: absolute; background: #fafafa; overflow: hidden; z-index: 999; } +@media only screen and (max-width: 767px) { .navbar li .dropdown .dropdown { width: 100%; max-width: 100%; position: relative; -webkit-box-shadow: none !important; -moz-box-shadow: none !important; box-shadow: none !important; } + .navbar li .dropdown.active .dropdown { border-bottom: 1px solid #313436; } + .navbar li .dropdown.active .dropdown ul { position: relative; top: 0; background: #36393b; min-height: 50px; max-height: 250px; height: auto; overflow: auto; -webkit-box-shadow: none !important; -moz-box-shadow: none !important; box-shadow: none !important; } + .navbar li .dropdown.active .dropdown ul li { min-height: 50px; border-bottom: #3e4043; } + .navbar li .dropdown.active .dropdown ul li a { color: white; border-bottom: 1px solid #313436; } + .navbar li .dropdown.active .dropdown ul li a:hover { color: #d04526; } } + +.navbar li .dropdown ul { margin: 0; display: block; } +.navbar li .dropdown ul > li { position: relative; display: block; width: 100%; float: left; text-align: left; height: auto; -webkit-border-radius: none; -moz-border-radius: none; -ms-border-radius: none; -o-border-radius: none; border-radius: none; } +@media only screen and (min-width: 768px) and (max-width: 939px) { .navbar li .dropdown ul > li { max-width: 320px; word-wrap: break-word; } } +.navbar li .dropdown ul > li a { display: block; padding: 0 20px; color: #d04526; border-bottom: 1px solid #cccccc; text-shadow: none; height: 51px; line-height: 49px; } +@media only screen and (max-width: 767px) { .navbar li .dropdown ul > li a { padding: 0 20px; } } +.navbar li .dropdown ul > li .dropdown { display: none; background: white; } +.navbar li .dropdown ul li:first-child a { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; } + +.gumby-no-touch .navbar ul li:hover > a, .gumby-touch .navbar ul li.active > a { position: relative; background: #868d92; z-index: 1000; } + +.gumby-no-touch .navbar ul li:hover .dropdown, .gumby-touch .navbar ul li.active .dropdown { min-height: 50px; max-height: 561px; overflow: visible; height: auto; width: 100%; padding: 0; border-top: 1px solid #3e4043; -webkit-box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); -moz-box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); box-shadow: 0px 3px 4px rgba(0, 0, 0, 0.3); } + +.gumby-no-touch .navbar ul li:hover .dropdown ul { position: relative; top: 0; min-height: 50px; max-height: 250px; height: auto; -webkit-box-shadow: none !important; -moz-box-shadow: none !important; box-shadow: none !important; -webkit-transition-duration: 0.5s; -moz-transition-duration: 0.5s; -o-transition-duration: 0.5s; transition-duration: 0.5s; } +@media only screen and (max-width: 767px) { .gumby-no-touch .navbar ul li:hover .dropdown ul { overflow: auto; background: #36393b; } + .gumby-no-touch .navbar ul li:hover .dropdown ul li { border-bottom: #3e4043; } + .gumby-no-touch .navbar ul li:hover .dropdown ul li a { color: white; border-bottom: 1px solid #313436; } + .gumby-no-touch .navbar ul li:hover .dropdown ul li a:hover { color: #d04526; } } + +.gumby-no-touch .navbar li .dropdown ul > li:hover .dropdown, .gumby-touch .navbar li .dropdown ul > li.active .dropdown { border-top: none; display: block; position: absolute; z-index: 9999; left: 100%; top: 0; margin-top: 0; } +@media only screen and (max-width: 767px) { .gumby-no-touch .navbar li .dropdown ul > li:hover .dropdown, .gumby-touch .navbar li .dropdown ul > li.active .dropdown { position: relative; left: 0; } + .gumby-no-touch .navbar li .dropdown ul > li:hover .dropdown ul, .gumby-touch .navbar li .dropdown ul > li.active .dropdown ul { background: #252728 !important; } } + +.gumby-no-touch .navbar li .dropdown ul li a:hover { background: #f2f2f2; } + +.gumby-touch .navbar a:hover { color: white !important; } + +.subnav { display: block; width: auto; overflow: hidden; margin: 0 0 18px 0; padding-top: 4px; } +.subnav li, .subnav dt, .subnav dd { float: left; display: inline; margin-left: 9px; margin-bottom: 4px; } +.subnav li:first-child, .subnav dt:first-child, .subnav dd:first-child { margin-left: 0; } +.subnav dt { color: #f2f2f2; font-weight: normal; } +.subnav li a, .subnav dd a { color: white; font-size: 15px; text-decoration: none; -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; } +.subnav li.active a, .subnav dd.active a { background: #4a4d50; padding: 5px 9px; text-shadow: 0 1px 1px #4a4d50; } + +/* Buttons */ +.btn, .skiplink { display: inline-block; width: auto; background: #f2f2f2; -webkit-appearance: none; font-family: "Oxygen"; font-weight: 600; padding: 0 !important; text-align: center; } +.btn > a, .btn input, .btn button, .skiplink > a, .skiplink input, .skiplink button { display: block; padding: 0 18px; color: white; height: 100%; } +.btn input, .btn button, .skiplink input, .skiplink button { background: none; border: none; width: 100%; font-size: 100%; cursor: pointer; font-weight: 400; -webkit-appearance: none; -moz-appearance: none; appearance: none; } + +.btn.xlarge, .skiplink.xlarge { font-size: 30px; font-size: 1.875rem; height: 66px; line-height: 64px; } +.btn.xlarge a, .skiplink.xlarge a { position: relative; padding: 0 30px; } +.btn.xlarge.icon-left a, .skiplink.xlarge.icon-left a { padding-left: 66px; } +.btn.xlarge.icon-left a:before, .skiplink.xlarge.icon-left a:before { left: 20px; } +.btn.xlarge.icon-right a, .skiplink.xlarge.icon-right a { padding-right: 66px; } +.btn.xlarge.icon-right a:after, .skiplink.xlarge.icon-right a:after { right: 20px; } +.btn.large, .skiplink.large { font-size: 26px; font-size: 1.625rem; height: 58px; line-height: 56px; } +.btn.large a, .skiplink.large a { position: relative; padding: 0 26px; } +.btn.large.icon-left a, .skiplink.large.icon-left a { padding-left: 58px; } +.btn.large.icon-left a:before, .skiplink.large.icon-left a:before { left: 17.33333px; } +.btn.large.icon-right a, .skiplink.large.icon-right a { padding-right: 58px; } +.btn.large.icon-right a:after, .skiplink.large.icon-right a:after { right: 17.33333px; } +.btn.medium, .skiplink.medium { font-size: 16px; font-size: 1rem; height: 36px; line-height: 34px; } +.btn.medium a, .skiplink.medium a { position: relative; padding: 0 16px; } +.btn.medium.icon-left a, .skiplink.medium.icon-left a { padding-left: 36px; } +.btn.medium.icon-left a:before, .skiplink.medium.icon-left a:before { left: 10.66667px; } +.btn.medium.icon-right a, .skiplink.medium.icon-right a { padding-right: 36px; } +.btn.medium.icon-right a:after, .skiplink.medium.icon-right a:after { right: 10.66667px; } +.btn.medium a, .skiplink.medium a { padding: 0 18px; } +.btn.small, .skiplink.small { font-size: 10px; font-size: 0.625rem; height: 23px; line-height: 21px; } +.btn.small a, .skiplink.small a { position: relative; padding: 0 10px; } +.btn.small.icon-left a, .skiplink.small.icon-left a { padding-left: 23px; } +.btn.small.icon-left a:before, .skiplink.small.icon-left a:before { left: 6.66667px; } +.btn.small.icon-right a, .skiplink.small.icon-right a { padding-right: 23px; } +.btn.small.icon-right a:after, .skiplink.small.icon-right a:after { right: 6.66667px; } +.btn.small a, .skiplink.small a { padding: 0 10px; } +.btn.oval, .skiplink.oval { -webkit-border-radius: 1000px; -moz-border-radius: 1000px; -ms-border-radius: 1000px; -o-border-radius: 1000px; border-radius: 1000px; } +.btn.pill-left, .skiplink.pill-left { -webkit-border-radius: 500px 0 0 500px; -moz-border-radius: 500px 0 0 500px; -ms-border-radius: 500px 0 0 500px; -o-border-radius: 500px 0 0 500px; border-radius: 500px 0 0 500px; } +.btn.pill-right, .skiplink.pill-right { -webkit-border-radius: 0 500px 500px 0; -moz-border-radius: 0 500px 500px 0; -ms-border-radius: 0 500px 500px 0; -o-border-radius: 0 500px 500px 0; border-radius: 0 500px 500px 0; } + +.btn.primary, .skiplink.primary { background: #3085d6; border: 1px solid #3085d6; } +.btn.primary:hover, .skiplink.primary:hover { background: #5b9ede; } +.btn.primary:active, .skiplink.primary:active { background: #236bb0; } +.btn.secondary, .skiplink.secondary { background: #42a35a; border: 1px solid #42a35a; } +.btn.secondary:hover, .skiplink.secondary:hover { background: #5bbd73; } +.btn.secondary:active, .skiplink.secondary:active { background: #337f46; } +.btn.default, .skiplink.default { background: #f2f2f2; border: 1px solid #f2f2f2; color: #555555; border: 1px solid #f2f2f2; } +.btn.default:hover, .skiplink.default:hover { background: white; } +.btn.default:active, .skiplink.default:active { background: #d8d8d8; } +.btn.default:hover, .skiplink.default:hover { border: 1px solid #e5e5e5; } +.btn.default a, .btn.default input, .btn.default button, .skiplink.default a, .skiplink.default input, .skiplink.default button { color: #555555; } +.btn.info, .skiplink.info { background: #4a4d50; border: 1px solid #4a4d50; } +.btn.info:hover, .skiplink.info:hover { background: #63676a; } +.btn.info:active, .skiplink.info:active { background: #313436; } +.btn.danger, .skiplink.danger { background: #ca3838; border: 1px solid #ca3838; } +.btn.danger:hover, .skiplink.danger:hover { background: #d56060; } +.btn.danger:active, .skiplink.danger:active { background: #a32c2c; } +.btn.warning, .skiplink.warning { background: #f6b83f; border: 1px solid #f6b83f; color: #644405; } +.btn.warning:hover, .skiplink.warning:hover { background: #f8ca70; } +.btn.warning:active, .skiplink.warning:active { background: #f4a60e; } +.btn.warning a, .btn.warning input, .btn.warning button, .skiplink.warning a, .skiplink.warning input, .skiplink.warning button { color: #644405; } +.btn.success, .skiplink.success { background: #58c026; border: 1px solid #58c026; } +.btn.success:hover, .skiplink.success:hover { background: #72d940; } +.btn.success:active, .skiplink.success:active { background: #44951e; } +.btn.metro, .metro .btn, .metro .skiplink, .btn.metro:hover, .metro .btn:hover, .metro .skiplink:hover, .skiplink.metro:hover, .btn.metro:active, .metro .btn:active, .metro .skiplink:active, .skiplink.metro:active, .skiplink.metro { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; } +.btn.metro.rounded, .metro .rounded.btn, .metro .rounded.skiplink, .rounded.skiplink.metro:hover, .rounded.skiplink.metro:active, .skiplink.metro.rounded { -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; } +.btn.pretty, .pretty .btn, .pretty .skiplink, .btn.pretty:hover, .pretty .btn:hover, .pretty .skiplink:hover, .skiplink.pretty:hover, .btn.pretty:active, .pretty .btn:active, .pretty .skiplink:active, .skiplink.pretty:active, .skiplink.pretty { -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; } +.btn.pretty.squared, .pretty .squared.btn, .pretty .squared.skiplink, .squared.skiplink.pretty:hover, .squared.skiplink.pretty:active, .skiplink.pretty.squared { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; } + +.btn.pretty.primary, .pretty .primary.btn, .pretty .primary.skiplink, .primary.skiplink.pretty:hover, .primary.skiplink.pretty:active, .skiplink.pretty.primary { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #85b7e7), color-stop(100%, #2a85dc)); background-image: -webkit-linear-gradient(#85b7e7, #2a85dc); background-image: -moz-linear-gradient(#85b7e7, #2a85dc); background-image: -o-linear-gradient(#85b7e7, #2a85dc); background-image: linear-gradient(#85b7e7, #2a85dc); -webkit-box-shadow: inset 0 0 3px #f0f6fc; -moz-box-shadow: inset 0 0 3px #f0f6fc; box-shadow: inset 0 0 3px #f0f6fc; border: 1px solid #1f5e9b; } +.pretty .primary.btn:hover, .pretty .primary.skiplink:hover, .primary.btn.pretty:hover, .primary.skiplink.pretty:hover, .skiplink.pretty.primary:hover { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #a2d4fc), color-stop(100%, #54b2fe)); background-image: -webkit-linear-gradient(#a2d4fc, #54b2fe); background-image: -moz-linear-gradient(#a2d4fc, #54b2fe); background-image: -o-linear-gradient(#a2d4fc, #54b2fe); background-image: linear-gradient(#a2d4fc, #54b2fe); -webkit-box-shadow: inset 0 0 3px white; -moz-box-shadow: inset 0 0 3px white; box-shadow: inset 0 0 3px white; border: 1px solid #0e90f8; } +.pretty .primary.btn:active, .pretty .primary.skiplink:active, .primary.btn.pretty:active, .primary.skiplink.pretty:active, .skiplink.pretty.primary:active { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #2a85dc), color-stop(100%, #85b7e7)); background-image: -webkit-linear-gradient(#2a85dc, #85b7e7); background-image: -moz-linear-gradient(#2a85dc, #85b7e7); background-image: -o-linear-gradient(#2a85dc, #85b7e7); background-image: linear-gradient(#2a85dc, #85b7e7); -webkit-box-shadow: inset 0 0 3px white; -moz-box-shadow: inset 0 0 3px white; box-shadow: inset 0 0 3px white; } +.btn.pretty.primary a, .pretty .primary.btn a, .pretty .primary.skiplink a, .primary.skiplink.pretty:hover a, .primary.skiplink.pretty:active a, .btn.pretty.primary input, .pretty .primary.btn input, .pretty .primary.skiplink input, .primary.skiplink.pretty:hover input, .primary.skiplink.pretty:active input, .btn.pretty.primary button, .pretty .primary.btn button, .pretty .primary.skiplink button, .primary.skiplink.pretty:hover button, .primary.skiplink.pretty:active button, .skiplink.pretty.primary a, .skiplink.pretty.primary input, .skiplink.pretty.primary button { text-shadow: 0 1px 1px #1a5186; } +.btn.pretty.secondary, .pretty .secondary.btn, .pretty .secondary.skiplink, .secondary.skiplink.pretty:hover, .secondary.skiplink.pretty:active, .skiplink.pretty.secondary { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #80cb92), color-stop(100%, #3ca957)); background-image: -webkit-linear-gradient(#80cb92, #3ca957); background-image: -moz-linear-gradient(#80cb92, #3ca957); background-image: -o-linear-gradient(#80cb92, #3ca957); background-image: linear-gradient(#80cb92, #3ca957); -webkit-box-shadow: inset 0 0 3px #daf0e0; -moz-box-shadow: inset 0 0 3px #daf0e0; box-shadow: inset 0 0 3px #daf0e0; border: 1px solid #2c6d3c; } +.pretty .secondary.btn:hover, .pretty .secondary.skiplink:hover, .secondary.btn.pretty:hover, .secondary.skiplink.pretty:hover, .skiplink.pretty.secondary:hover { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #a1d3ad), color-stop(100%, #68c07d)); background-image: -webkit-linear-gradient(#a1d3ad, #68c07d); background-image: -moz-linear-gradient(#a1d3ad, #68c07d); background-image: -o-linear-gradient(#a1d3ad, #68c07d); background-image: linear-gradient(#a1d3ad, #68c07d); -webkit-box-shadow: inset 0 0 3px #f8fcf9; -moz-box-shadow: inset 0 0 3px #f8fcf9; box-shadow: inset 0 0 3px #f8fcf9; border: 1px solid #469659; } +.pretty .secondary.btn:active, .pretty .secondary.skiplink:active, .secondary.btn.pretty:active, .secondary.skiplink.pretty:active, .skiplink.pretty.secondary:active { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #3ca957), color-stop(100%, #80cb92)); background-image: -webkit-linear-gradient(#3ca957, #80cb92); background-image: -moz-linear-gradient(#3ca957, #80cb92); background-image: -o-linear-gradient(#3ca957, #80cb92); background-image: linear-gradient(#3ca957, #80cb92); -webkit-box-shadow: inset 0 0 3px #ecf8ef; -moz-box-shadow: inset 0 0 3px #ecf8ef; box-shadow: inset 0 0 3px #ecf8ef; } +.btn.pretty.secondary a, .pretty .secondary.btn a, .pretty .secondary.skiplink a, .secondary.skiplink.pretty:hover a, .secondary.skiplink.pretty:active a, .btn.pretty.secondary input, .pretty .secondary.btn input, .pretty .secondary.skiplink input, .secondary.skiplink.pretty:hover input, .secondary.skiplink.pretty:active input, .btn.pretty.secondary button, .pretty .secondary.btn button, .pretty .secondary.skiplink button, .secondary.skiplink.pretty:hover button, .secondary.skiplink.pretty:active button, .skiplink.pretty.secondary a, .skiplink.pretty.secondary input, .skiplink.pretty.secondary button { text-shadow: 0 1px 1px #255a32; } +.btn.pretty.default, .pretty .default.btn, .pretty .default.skiplink, .default.skiplink.pretty:hover, .default.skiplink.pretty:active, .skiplink.pretty.default { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #f3f1f1)); background-image: -webkit-linear-gradient(#ffffff, #f3f1f1); background-image: -moz-linear-gradient(#ffffff, #f3f1f1); background-image: -o-linear-gradient(#ffffff, #f3f1f1); background-image: linear-gradient(#ffffff, #f3f1f1); -webkit-box-shadow: inset 0 0 3px white; -moz-box-shadow: inset 0 0 3px white; box-shadow: inset 0 0 3px white; border: 1px solid #cccccc; } +.pretty .default.btn:hover, .pretty .default.skiplink:hover, .default.btn.pretty:hover, .default.skiplink.pretty:hover, .skiplink.pretty.default:hover { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #ffffff)); background-image: -webkit-linear-gradient(#ffffff, #ffffff); background-image: -moz-linear-gradient(#ffffff, #ffffff); background-image: -o-linear-gradient(#ffffff, #ffffff); background-image: linear-gradient(#ffffff, #ffffff); -webkit-box-shadow: inset 0 0 3px white; -moz-box-shadow: inset 0 0 3px white; box-shadow: inset 0 0 3px white; border: 1px solid #d9d9d9; } +.pretty .default.btn:active, .pretty .default.skiplink:active, .default.btn.pretty:active, .default.skiplink.pretty:active, .skiplink.pretty.default:active { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f3f1f1), color-stop(100%, #ffffff)); background-image: -webkit-linear-gradient(#f3f1f1, #ffffff); background-image: -moz-linear-gradient(#f3f1f1, #ffffff); background-image: -o-linear-gradient(#f3f1f1, #ffffff); background-image: linear-gradient(#f3f1f1, #ffffff); -webkit-box-shadow: inset 0 0 3px white; -moz-box-shadow: inset 0 0 3px white; box-shadow: inset 0 0 3px white; } +.btn.pretty.default a, .pretty .default.btn a, .pretty .default.skiplink a, .default.skiplink.pretty:hover a, .default.skiplink.pretty:active a, .btn.pretty.default input, .pretty .default.btn input, .pretty .default.skiplink input, .default.skiplink.pretty:hover input, .default.skiplink.pretty:active input, .btn.pretty.default button, .pretty .default.btn button, .pretty .default.skiplink button, .default.skiplink.pretty:hover button, .default.skiplink.pretty:active button, .skiplink.pretty.default a, .skiplink.pretty.default input, .skiplink.pretty.default button { text-shadow: 0 1px 1px white; } +.btn.pretty.info, .pretty .info.btn, .pretty .info.skiplink, .info.skiplink.pretty:hover, .info.skiplink.pretty:active, .skiplink.pretty.info { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #7b8085), color-stop(100%, #464d54)); background-image: -webkit-linear-gradient(#7b8085, #464d54); background-image: -moz-linear-gradient(#7b8085, #464d54); background-image: -o-linear-gradient(#7b8085, #464d54); background-image: linear-gradient(#7b8085, #464d54); -webkit-box-shadow: inset 0 0 3px #bdc0c2; -moz-box-shadow: inset 0 0 3px #bdc0c2; box-shadow: inset 0 0 3px #bdc0c2; border: 1px solid #252728; } +.pretty .info.btn:hover, .pretty .info.skiplink:hover, .info.btn.pretty:hover, .info.skiplink.pretty:hover, .skiplink.pretty.info:hover { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #aeb3b6), color-stop(100%, #808e98)); background-image: -webkit-linear-gradient(#aeb3b6, #808e98); background-image: -moz-linear-gradient(#aeb3b6, #808e98); background-image: -o-linear-gradient(#aeb3b6, #808e98); background-image: linear-gradient(#aeb3b6, #808e98); -webkit-box-shadow: inset 0 0 3px #f1f2f3; -moz-box-shadow: inset 0 0 3px #f1f2f3; box-shadow: inset 0 0 3px #f1f2f3; border: 1px solid #60676b; } +.pretty .info.btn:active, .pretty .info.skiplink:active, .info.btn.pretty:active, .info.skiplink.pretty:active, .skiplink.pretty.info:active { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #464d54), color-stop(100%, #7b8085)); background-image: -webkit-linear-gradient(#464d54, #7b8085); background-image: -moz-linear-gradient(#464d54, #7b8085); background-image: -o-linear-gradient(#464d54, #7b8085); background-image: linear-gradient(#464d54, #7b8085); -webkit-box-shadow: inset 0 0 3px #cbcdce; -moz-box-shadow: inset 0 0 3px #cbcdce; box-shadow: inset 0 0 3px #cbcdce; } +.btn.pretty.info a, .pretty .info.btn a, .pretty .info.skiplink a, .info.skiplink.pretty:hover a, .info.skiplink.pretty:active a, .btn.pretty.info input, .pretty .info.btn input, .pretty .info.skiplink input, .info.skiplink.pretty:hover input, .info.skiplink.pretty:active input, .btn.pretty.info button, .pretty .info.btn button, .pretty .info.skiplink button, .info.skiplink.pretty:hover button, .info.skiplink.pretty:active button, .skiplink.pretty.info a, .skiplink.pretty.info input, .skiplink.pretty.info button { text-shadow: 0 1px 1px #191a1b; } +.btn.pretty.danger, .pretty .danger.btn, .pretty .danger.skiplink, .danger.skiplink.pretty:hover, .danger.skiplink.pretty:active, .skiplink.pretty.danger { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #df8989), color-stop(100%, #d03232)); background-image: -webkit-linear-gradient(#df8989, #d03232); background-image: -moz-linear-gradient(#df8989, #d03232); background-image: -o-linear-gradient(#df8989, #d03232); background-image: linear-gradient(#df8989, #d03232); -webkit-box-shadow: inset 0 0 3px #faeded; -moz-box-shadow: inset 0 0 3px #faeded; box-shadow: inset 0 0 3px #faeded; border: 1px solid #8f2626; } +.pretty .danger.btn:hover, .pretty .danger.skiplink:hover, .danger.btn.pretty:hover, .danger.skiplink.pretty:hover, .skiplink.pretty.danger:hover { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f79696), color-stop(100%, #f64a4a)); background-image: -webkit-linear-gradient(#f79696, #f64a4a); background-image: -moz-linear-gradient(#f79696, #f64a4a); background-image: -o-linear-gradient(#f79696, #f64a4a); background-image: linear-gradient(#f79696, #f64a4a); -webkit-box-shadow: inset 0 0 3px white; -moz-box-shadow: inset 0 0 3px white; box-shadow: inset 0 0 3px white; border: 1px solid #e21212; } +.pretty .danger.btn:active, .pretty .danger.skiplink:active, .danger.btn.pretty:active, .danger.skiplink.pretty:active, .skiplink.pretty.danger:active { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #d03232), color-stop(100%, #df8989)); background-image: -webkit-linear-gradient(#d03232, #df8989); background-image: -moz-linear-gradient(#d03232, #df8989); background-image: -o-linear-gradient(#d03232, #df8989); background-image: linear-gradient(#d03232, #df8989); -webkit-box-shadow: inset 0 0 3px white; -moz-box-shadow: inset 0 0 3px white; box-shadow: inset 0 0 3px white; } +.btn.pretty.danger a, .pretty .danger.btn a, .pretty .danger.skiplink a, .danger.skiplink.pretty:hover a, .danger.skiplink.pretty:active a, .btn.pretty.danger input, .pretty .danger.btn input, .pretty .danger.skiplink input, .danger.skiplink.pretty:hover input, .danger.skiplink.pretty:active input, .btn.pretty.danger button, .pretty .danger.btn button, .pretty .danger.skiplink button, .danger.skiplink.pretty:hover button, .danger.skiplink.pretty:active button, .skiplink.pretty.danger a, .skiplink.pretty.danger input, .skiplink.pretty.danger button { text-shadow: 0 1px 1px #7b2121; } +.btn.pretty.warning, .pretty .warning.btn, .pretty .warning.skiplink, .warning.skiplink.pretty:hover, .warning.skiplink.pretty:active, .skiplink.pretty.warning { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #fbdca0), color-stop(100%, #fbba3a)); background-image: -webkit-linear-gradient(#fbdca0, #fbba3a); background-image: -moz-linear-gradient(#fbdca0, #fbba3a); background-image: -o-linear-gradient(#fbdca0, #fbba3a); background-image: linear-gradient(#fbdca0, #fbba3a); -webkit-box-shadow: inset 0 0 3px white; -moz-box-shadow: inset 0 0 3px white; box-shadow: inset 0 0 3px white; border: 1px solid #de960a; color: #644405; } +.pretty .warning.btn:hover, .pretty .warning.skiplink:hover, .warning.btn.pretty:hover, .warning.skiplink.pretty:hover, .skiplink.pretty.warning:hover { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #feecca), color-stop(100%, #ffd37d)); background-image: -webkit-linear-gradient(#feecca, #ffd37d); background-image: -moz-linear-gradient(#feecca, #ffd37d); background-image: -o-linear-gradient(#feecca, #ffd37d); background-image: linear-gradient(#feecca, #ffd37d); -webkit-box-shadow: inset 0 0 3px white; -moz-box-shadow: inset 0 0 3px white; box-shadow: inset 0 0 3px white; border: 1px solid #fcb834; } +.pretty .warning.btn:active, .pretty .warning.skiplink:active, .warning.btn.pretty:active, .warning.skiplink.pretty:active, .skiplink.pretty.warning:active { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #fbba3a), color-stop(100%, #fbdca0)); background-image: -webkit-linear-gradient(#fbba3a, #fbdca0); background-image: -moz-linear-gradient(#fbba3a, #fbdca0); background-image: -o-linear-gradient(#fbba3a, #fbdca0); background-image: linear-gradient(#fbba3a, #fbdca0); -webkit-box-shadow: inset 0 0 3px white; -moz-box-shadow: inset 0 0 3px white; box-shadow: inset 0 0 3px white; } +.btn.pretty.warning a, .pretty .warning.btn a, .pretty .warning.skiplink a, .warning.skiplink.pretty:hover a, .warning.skiplink.pretty:active a, .btn.pretty.warning input, .pretty .warning.btn input, .pretty .warning.skiplink input, .warning.skiplink.pretty:hover input, .warning.skiplink.pretty:active input, .btn.pretty.warning button, .pretty .warning.btn button, .pretty .warning.skiplink button, .warning.skiplink.pretty:hover button, .warning.skiplink.pretty:active button, .skiplink.pretty.warning a, .skiplink.pretty.warning input, .skiplink.pretty.warning button { text-shadow: 0 1px 1px #fbdca0; } +.btn.pretty.success, .pretty .success.btn, .pretty .success.skiplink, .success.skiplink.pretty:hover, .success.skiplink.pretty:active, .skiplink.pretty.success { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #91e26a), color-stop(100%, #56c620)); background-image: -webkit-linear-gradient(#91e26a, #56c620); background-image: -moz-linear-gradient(#91e26a, #56c620); background-image: -o-linear-gradient(#91e26a, #56c620); background-image: linear-gradient(#91e26a, #56c620); -webkit-box-shadow: inset 0 0 3px #e0f7d5; -moz-box-shadow: inset 0 0 3px #e0f7d5; box-shadow: inset 0 0 3px #e0f7d5; border: 1px solid #3b8019; } +.pretty .success.btn:hover, .pretty .success.skiplink:hover, .success.btn.pretty:hover, .success.skiplink.pretty:hover, .skiplink.pretty.success:hover { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #96e570), color-stop(100%, #64df29)); background-image: -webkit-linear-gradient(#96e570, #64df29); background-image: -moz-linear-gradient(#96e570, #64df29); background-image: -o-linear-gradient(#96e570, #64df29); background-image: linear-gradient(#96e570, #64df29); -webkit-box-shadow: inset 0 0 3px #e5f9db; -moz-box-shadow: inset 0 0 3px #e5f9db; box-shadow: inset 0 0 3px #e5f9db; border: 1px solid #479f1d; } +.pretty .success.btn:active, .pretty .success.skiplink:active, .success.btn.pretty:active, .success.skiplink.pretty:active, .skiplink.pretty.success:active { background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #56c620), color-stop(100%, #91e26a)); background-image: -webkit-linear-gradient(#56c620, #91e26a); background-image: -moz-linear-gradient(#56c620, #91e26a); background-image: -o-linear-gradient(#56c620, #91e26a); background-image: linear-gradient(#56c620, #91e26a); -webkit-box-shadow: inset 0 0 3px #f0fbea; -moz-box-shadow: inset 0 0 3px #f0fbea; box-shadow: inset 0 0 3px #f0fbea; } +.btn.pretty.success a, .pretty .success.btn a, .pretty .success.skiplink a, .success.skiplink.pretty:hover a, .success.skiplink.pretty:active a, .btn.pretty.success input, .pretty .success.btn input, .pretty .success.skiplink input, .success.skiplink.pretty:hover input, .success.skiplink.pretty:active input, .btn.pretty.success button, .pretty .success.btn button, .pretty .success.skiplink button, .success.skiplink.pretty:hover button, .success.skiplink.pretty:active button, .skiplink.pretty.success a, .skiplink.pretty.success input, .skiplink.pretty.success button { text-shadow: 0 1px 1px #316b15; } + +/* Icons */ +[class^="icon-"] a:before, [class*=" icon-"] a:before, [class^="icon-"] a:after, [class*=" icon-"] a:after, i[class^="icon-"], i[class*=" icon-"] { font-family: "entypo"; position: absolute; text-decoration: none; zoom: 1; } + +i[class^="icon-"], i[class*=" icon-"] { display: inline-block; position: static; min-width: 20px; margin: 0 5px; text-align: center; } + +/* Form Styles */ +form { margin: 0 0 18px; } +form label { display: block; font-size: 16px; font-size: 1rem; line-height: 1.625em; cursor: pointer; margin-bottom: 9px; } +form label.inline { display: inline-block; padding-right: 20px; } +form dt { margin: 0; } +form textarea { height: 150px; } +form ul, form ul li { margin-left: 0; list-style-type: none; } +form fieldset { border-style: solid; border-width: 0.0625em; padding: 1.5625em; border-color: #d8d8d8; margin: 18px 0; } +form fieldset legend { padding: 5px 10px; } + +.field { position: relative; max-width: 100%; margin-bottom: 10px; vertical-align: middle; font-size: 16px; overflow: hidden; /* remove inline-block white-space — A 0px font-size = 0px of white space */ } +.field.metro, .field .metro { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; } +.field input, .field input[type="*"], .field textarea { max-width: 100%; width: 100%; padding: 0; margin: 0; border: none; outline: none; resize: none; -webkit-appearance: none; font-family: "Oxygen"; font-weight: 300; font-size: 16px; font-size: 1rem; -webkit-box-shadow: none; -moz-box-shadow: none; box-shadow: none; } +.field .input { position: relative; padding: 0 10px; background: #fff; border: 1px solid #d8d8d8; height: 36px; line-height: 34px; font-size: 16px; font-size: 1rem; -webkit-border-radius: 4px; -moz-border-radius: 4px; -ms-border-radius: 4px; -o-border-radius: 4px; border-radius: 4px; } +.field .input.search { height: 36px; line-height: 34px; -webkit-border-radius: 1000px; -moz-border-radius: 1000px; -ms-border-radius: 1000px; -o-border-radius: 1000px; border-radius: 1000px; padding-right: 0; } +.field .input.textarea { height: auto; } +.field .xnarrow { width: 13.33333%; } +.field .narrow { width: 30.66667%; } +.field .normal { width: 48%; } +.field .wide { width: 65.33333%; } +.field .xwide { width: 82.66667%; } +.field .xxwide { width: 100%; } +.field .xnarrow, .field .narrow, .field .normal, .field .wide, .field .xwide, .field .xxwide { margin: 0; } +.field .xnarrow:last-child, .field .narrow:last-child, .field .normal:last-child, .field .wide:last-child, .field .xwide:last-child, .field .xxwide:last-child { margin-left: -4px; } +.field .xnarrow:first-child, .field .narrow:first-child, .field .normal:first-child, .field .wide:first-child, .field .xwide:first-child, .field .xxwide:first-child { margin-right: 3.94%; margin-left: 0; } +.field .xnarrow:first-child:last-child, .field .narrow:first-child:last-child, .field .normal:first-child:last-child, .field .wide:first-child:last-child, .field .xwide:first-child:last-child, .field .xxwide:first-child:last-child { margin: 0; } +.field label + .xnarrow:last-child, .field label + .narrow:last-child, .field label + .normal:last-child, .field label + .wide:last-child, .field label + .xwide:last-child, .field label + .xxwide:last-child { margin-left: 0; } +@media only screen and (max-width: 960px) { .field .xxwide:first-child, .field .xxwide:last-child { margin-right: 0%; } } +.field.prepend, .field.append { font-size: 0; white-space: nowrap; padding-bottom: 3.5px; } +.field.prepend input, .field.prepend .input, .field.append input, .field.append .input { display: inline-block; max-width: 100%; } +.field.prepend input, .field.prepend .input { -webkit-border-radius: 0px 4px 4px 0; -moz-border-radius: 0px 4px 4px 0; -ms-border-radius: 0px 4px 4px 0; -o-border-radius: 0px 4px 4px 0; border-radius: 0px 4px 4px 0; } +.field.append input, .field.append .input { -webkit-border-radius: 4px 0 0 4px; -moz-border-radius: 4px 0 0 4px; -ms-border-radius: 4px 0 0 4px; -o-border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px; } +.field.prepend.append input { -webkit-border-radius: 0; -moz-border-radius: 0; -ms-border-radius: 0; -o-border-radius: 0; border-radius: 0; } +.field.prepend.append input:first-child { -webkit-border-radius: 4px 0 0 4px; -moz-border-radius: 4px 0 0 4px; -ms-border-radius: 4px 0 0 4px; -o-border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px; } +.field.prepend.append input:last-child { margin-left: -1px; -webkit-border-radius: 0px 4px 4px 0; -moz-border-radius: 0px 4px 4px 0; -ms-border-radius: 0px 4px 4px 0; -o-border-radius: 0px 4px 4px 0; border-radius: 0px 4px 4px 0; } +.field.prepend .adjoined, .field.append .adjoined, .field.prepend .btn, .field.append .btn { position: relative; display: inline-block; margin-bottom: 0; z-index: 99; } +.field.prepend .btn a, .field.prepend .btn input, .field.prepend .btn button, .field.append .btn a, .field.append .btn input, .field.append .btn button { padding: 0 12px; } +.field.prepend .adjoined, .field.append .adjoined { padding: 0 10px 0 10px; background: #f2f2f2; border: 1px solid #d8d8d8; font-family: "Oxygen"; font-weight: 600; color: #555555; font-size: 16px; font-size: 1rem; height: 36px; line-height: 34px; } +.field.prepend *:first-child { -webkit-border-radius: 4px 0 0 4px; -moz-border-radius: 4px 0 0 4px; -ms-border-radius: 4px 0 0 4px; -o-border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px; } +.field.prepend input:first-child { margin-right: 0; } +.field.prepend .adjoined, .field.prepend .btn { margin-right: -1px; } +.field .adjoined:first-child { margin-left: 0 !important; } +.field.append .adjoined, .field.append .btn { margin-left: -1px; } +.field.append *:last-child { -webkit-border-radius: 0px 4px 4px 0; -moz-border-radius: 0px 4px 4px 0; -ms-border-radius: 0px 4px 4px 0; -o-border-radius: 0px 4px 4px 0; border-radius: 0px 4px 4px 0; } +.field.append button, .field.prepend button { display: inline-block; } +.field.append input:first-child { margin-right: 0; } +.field.double input, .field.double .input { width: 50% !important; } +.field.double input:last-child, .field.double .input:last-child { margin-left: -1px; } +.field.danger:after { font-family: "entypo"; content: "\2716"; font-size: 16px; position: absolute; top: 14%; right: 15px; z-index: 999; color: #ca3838; } +.field.danger.no-icon:after { display: none; } +.field.danger.append:after, .field.danger.prepend:after { content: ""; } +.field.danger input, .field.danger .input, .field.danger textarea, .field.danger .textarea, .field.danger .radio span, .field.danger .checkbox span, .field.danger .picker { border-color: #ca3838; color: #ca3838; background: #f0c5c5; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; } +.field.danger textarea { color: #ca3838; } +.field.danger input::-webkit-input-placeholder, .field.danger textarea::-webkit-input-placeholder { color: #ca3838; } +.field.danger input:-moz-placeholder, .field.danger textarea:-moz-placeholder { color: #ca3838; } +.field.warning:after { font-family: "entypo"; content: "\26a0"; font-size: 16px; position: absolute; top: 14%; right: 15px; z-index: 999; color: #f6b83f; } +.field.warning.no-icon:after { display: none; } +.field.warning.append:after, .field.warning.prepend:after { content: ""; } +.field.warning input, .field.warning .input, .field.warning textarea, .field.warning .textarea, .field.warning .radio span, .field.warning .checkbox span, .field.warning .picker { border-color: #f6b83f; color: #f6b83f; background: #fef7ea; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; } +.field.warning textarea { color: #f6b83f; } +.field.warning input::-webkit-input-placeholder, .field.warning textarea::-webkit-input-placeholder { color: #f6b83f; } +.field.warning input:-moz-placeholder, .field.warning textarea:-moz-placeholder { color: #f6b83f; } +.field.success:after { font-family: "entypo"; content: "\2713"; font-size: 16px; position: absolute; top: 14%; right: 15px; z-index: 999; color: #58c026; } +.field.success.no-icon:after { display: none; } +.field.success.append:after, .field.success.prepend:after { content: ""; } +.field.success input, .field.success .input, .field.success textarea, .field.success .textarea, .field.success .radio span, .field.success .checkbox span, .field.success .picker { border-color: #58c026; color: #58c026; background: #c0eeaa; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; } +.field.success textarea { color: #58c026; } +.field.success input::-webkit-input-placeholder, .field.success textarea::-webkit-input-placeholder { color: #58c026; } +.field.success input:-moz-placeholder, .field.success textarea:-moz-placeholder { color: #58c026; } +.field .picker.danger { border-color: #ca3838; color: #ca3838; background: #f0c5c5; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; } +.field .picker.danger select, .field .picker.danger:after { color: #ca3838; } +.field .picker.warning { border-color: #f6b83f; color: #f6b83f; background: #fef7ea; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; } +.field .picker.warning select, .field .picker.warning:after { color: #f6b83f; } +.field .picker.success { border-color: #58c026; color: #58c026; background: #c0eeaa; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; } +.field .picker.success select, .field .picker.success:after { color: #58c026; } + +.field .text input[type="search"] { -webkit-appearance: textfield; } + +.no-js .radio input { -webkit-appearance: radio; margin-left: 1px; } +.no-js .checkbox input { -webkit-appearance: checkbox; } +.no-js .radio input, .no-js .checkbox input { display: inline-block; width: 16px; } + +.js .field .radio, .js .field .checkbox { position: relative; } +.js .field .radio.danger, .js .field .checkbox.danger { color: #ca3838; } +.js .field .radio.danger span, .js .field .checkbox.danger span { border-color: #ca3838; color: #ca3838; background: #f0c5c5; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; } +.js .field .radio.warning, .js .field .checkbox.warning { color: #f6b83f; } +.js .field .radio.warning span, .js .field .checkbox.warning span { border-color: #f6b83f; color: #f6b83f; background: #fef7ea; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; } +.js .field .radio.success, .js .field .checkbox.success { color: #58c026; color: #555555; } +.js .field .radio.success i, .js .field .checkbox.success i { color: #58c026; } +.js .field .radio.success span, .js .field .checkbox.success span { border-color: #58c026; color: #58c026; background: #c0eeaa; -webkit-transition-duration: 0.2s; -moz-transition-duration: 0.2s; -o-transition-duration: 0.2s; transition-duration: 0.2s; } +.js .field .radio.checked i, .js .field .checkbox.checked i { position: absolute; top: -1px; left: -8px; line-height: 16px; } +.js .field .radio span, .js .field .checkbox span { display: inline-block; width: 16px; height: 16px; position: relative; top: 2px; border: solid 1px #ccc; background: #fefefe; } +.js .field .radio input[type="radio"], .js .field .radio input[type="checkbox"], .js .field .checkbox input[type="radio"], .js .field .checkbox input[type="checkbox"] { display: none; } +.js .field .radio span { -webkit-border-radius: 8px; -moz-border-radius: 8px; -ms-border-radius: 8px; -o-border-radius: 8px; border-radius: 8px; } +.js .field .checkbox span { -webkit-border-radius: 3px; -moz-border-radius: 3px; -ms-border-radius: 3px; -o-border-radius: 3px; border-radius: 3px; } + +.field .text input[type="search"] { -webkit-appearance: textfield; } + +/* Form Picker Element ("; + + // Support: IE8, Opera 11-12.16 + // Nothing should be selected when empty strings follow ^= or $= or *= + // The test attribute must be unknown in Opera but "safe" for WinRT + // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section + if ( div.querySelectorAll("[msallowclip^='']").length ) { + rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" ); + } + + // Support: IE8 + // Boolean attributes and "value" are not treated correctly + if ( !div.querySelectorAll("[selected]").length ) { + rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" ); + } + + // Webkit/Opera - :checked should return selected option elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":checked").length ) { + rbuggyQSA.push(":checked"); + } + }); + + assert(function( div ) { + // Support: Windows 8 Native Apps + // The type and name attributes are restricted during .innerHTML assignment + var input = doc.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "name", "D" ); + + // Support: IE8 + // Enforce case-sensitivity of name attribute + if ( div.querySelectorAll("[name=d]").length ) { + rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" ); + } + + // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled) + // IE8 throws error here and will not see later tests + if ( !div.querySelectorAll(":enabled").length ) { + rbuggyQSA.push( ":enabled", ":disabled" ); + } + + // Opera 10-11 does not throw on post-comma invalid pseudos + div.querySelectorAll("*,:x"); + rbuggyQSA.push(",.*:"); + }); + } + + if ( (support.matchesSelector = rnative.test( (matches = docElem.matches || + docElem.webkitMatchesSelector || + docElem.mozMatchesSelector || + docElem.oMatchesSelector || + docElem.msMatchesSelector) )) ) { + + assert(function( div ) { + // Check to see if it's possible to do matchesSelector + // on a disconnected node (IE 9) + support.disconnectedMatch = matches.call( div, "div" ); + + // This should fail with an exception + // Gecko does not error, returns false instead + matches.call( div, "[s!='']:x" ); + rbuggyMatches.push( "!=", pseudos ); + }); + } + + rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join("|") ); + rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") ); + + /* Contains + ---------------------------------------------------------------------- */ + hasCompare = rnative.test( docElem.compareDocumentPosition ); + + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = hasCompare || rnative.test( docElem.contains ) ? + function( a, b ) { + var adown = a.nodeType === 9 ? a.documentElement : a, + bup = b && b.parentNode; + return a === bup || !!( bup && bup.nodeType === 1 && ( + adown.contains ? + adown.contains( bup ) : + a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16 + )); + } : + function( a, b ) { + if ( b ) { + while ( (b = b.parentNode) ) { + if ( b === a ) { + return true; + } + } + } + return false; + }; + + /* Sorting + ---------------------------------------------------------------------- */ + + // Document order sorting + sortOrder = hasCompare ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + // Sort on method existence if only one input has compareDocumentPosition + var compare = !a.compareDocumentPosition - !b.compareDocumentPosition; + if ( compare ) { + return compare; + } + + // Calculate position if both inputs belong to the same document + compare = ( a.ownerDocument || a ) === ( b.ownerDocument || b ) ? + a.compareDocumentPosition( b ) : + + // Otherwise we know they are disconnected + 1; + + // Disconnected nodes + if ( compare & 1 || + (!support.sortDetached && b.compareDocumentPosition( a ) === compare) ) { + + // Choose the first element that is related to our preferred document + if ( a === doc || a.ownerDocument === preferredDoc && contains(preferredDoc, a) ) { + return -1; + } + if ( b === doc || b.ownerDocument === preferredDoc && contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } : + function( a, b ) { + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Parentless nodes are either documents or disconnected + if ( !aup || !bup ) { + return a === doc ? -1 : + b === doc ? 1 : + aup ? -1 : + bup ? 1 : + sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + + // If the nodes are siblings, we can do a quick check + } else if ( aup === bup ) { + return siblingCheck( a, b ); + } + + // Otherwise we need full lists of their ancestors for comparison + cur = a; + while ( (cur = cur.parentNode) ) { + ap.unshift( cur ); + } + cur = b; + while ( (cur = cur.parentNode) ) { + bp.unshift( cur ); + } + + // Walk down the tree looking for a discrepancy + while ( ap[i] === bp[i] ) { + i++; + } + + return i ? + // Do a sibling check if the nodes have a common ancestor + siblingCheck( ap[i], bp[i] ) : + + // Otherwise nodes in our document sort first + ap[i] === preferredDoc ? -1 : + bp[i] === preferredDoc ? 1 : + 0; + }; + + return doc; +}; + +Sizzle.matches = function( expr, elements ) { + return Sizzle( expr, null, null, elements ); +}; + +Sizzle.matchesSelector = function( elem, expr ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + // Make sure that attribute selectors are quoted + expr = expr.replace( rattributeQuotes, "='$1']" ); + + if ( support.matchesSelector && documentIsHTML && + ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) && + ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) { + + try { + var ret = matches.call( elem, expr ); + + // IE 9's matchesSelector returns false on disconnected nodes + if ( ret || support.disconnectedMatch || + // As well, disconnected nodes are said to be in a document + // fragment in IE 9 + elem.document && elem.document.nodeType !== 11 ) { + return ret; + } + } catch(e) {} + } + + return Sizzle( expr, document, null, [ elem ] ).length > 0; +}; + +Sizzle.contains = function( context, elem ) { + // Set document vars if needed + if ( ( context.ownerDocument || context ) !== document ) { + setDocument( context ); + } + return contains( context, elem ); +}; + +Sizzle.attr = function( elem, name ) { + // Set document vars if needed + if ( ( elem.ownerDocument || elem ) !== document ) { + setDocument( elem ); + } + + var fn = Expr.attrHandle[ name.toLowerCase() ], + // Don't get fooled by Object.prototype properties (jQuery #13807) + val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ? + fn( elem, name, !documentIsHTML ) : + undefined; + + return val !== undefined ? + val : + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null; +}; + +Sizzle.error = function( msg ) { + throw new Error( "Syntax error, unrecognized expression: " + msg ); +}; + +/** + * Document sorting and removing duplicates + * @param {ArrayLike} results + */ +Sizzle.uniqueSort = function( results ) { + var elem, + duplicates = [], + j = 0, + i = 0; + + // Unless we *know* we can detect duplicates, assume their presence + hasDuplicate = !support.detectDuplicates; + sortInput = !support.sortStable && results.slice( 0 ); + results.sort( sortOrder ); + + if ( hasDuplicate ) { + while ( (elem = results[i++]) ) { + if ( elem === results[ i ] ) { + j = duplicates.push( i ); + } + } + while ( j-- ) { + results.splice( duplicates[ j ], 1 ); + } + } + + // Clear input after sorting to release objects + // See https://github.com/jquery/sizzle/pull/225 + sortInput = null; + + return results; +}; + +/** + * Utility function for retrieving the text value of an array of DOM nodes + * @param {Array|Element} elem + */ +getText = Sizzle.getText = function( elem ) { + var node, + ret = "", + i = 0, + nodeType = elem.nodeType; + + if ( !nodeType ) { + // If no nodeType, this is expected to be an array + while ( (node = elem[i++]) ) { + // Do not traverse comment nodes + ret += getText( node ); + } + } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) { + // Use textContent for elements + // innerText usage removed for consistency of new lines (jQuery #11153) + if ( typeof elem.textContent === "string" ) { + return elem.textContent; + } else { + // Traverse its children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + ret += getText( elem ); + } + } + } else if ( nodeType === 3 || nodeType === 4 ) { + return elem.nodeValue; + } + // Do not include comment or processing instruction nodes + + return ret; +}; + +Expr = Sizzle.selectors = { + + // Can be adjusted by the user + cacheLength: 50, + + createPseudo: markFunction, + + match: matchExpr, + + attrHandle: {}, + + find: {}, + + relative: { + ">": { dir: "parentNode", first: true }, + " ": { dir: "parentNode" }, + "+": { dir: "previousSibling", first: true }, + "~": { dir: "previousSibling" } + }, + + preFilter: { + "ATTR": function( match ) { + match[1] = match[1].replace( runescape, funescape ); + + // Move the given value to match[3] whether quoted or unquoted + match[3] = ( match[3] || match[4] || match[5] || "" ).replace( runescape, funescape ); + + if ( match[2] === "~=" ) { + match[3] = " " + match[3] + " "; + } + + return match.slice( 0, 4 ); + }, + + "CHILD": function( match ) { + /* matches from matchExpr["CHILD"] + 1 type (only|nth|...) + 2 what (child|of-type) + 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...) + 4 xn-component of xn+y argument ([+-]?\d*n|) + 5 sign of xn-component + 6 x of xn-component + 7 sign of y-component + 8 y of y-component + */ + match[1] = match[1].toLowerCase(); + + if ( match[1].slice( 0, 3 ) === "nth" ) { + // nth-* requires argument + if ( !match[3] ) { + Sizzle.error( match[0] ); + } + + // numeric x and y parameters for Expr.filter.CHILD + // remember that false/true cast respectively to 0/1 + match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) ); + match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" ); + + // other types prohibit arguments + } else if ( match[3] ) { + Sizzle.error( match[0] ); + } + + return match; + }, + + "PSEUDO": function( match ) { + var excess, + unquoted = !match[6] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] ) { + match[2] = match[4] || match[5] || ""; + + // Strip excess characters from unquoted arguments + } else if ( unquoted && rpseudo.test( unquoted ) && + // Get excess from tokenize (recursively) + (excess = tokenize( unquoted, true )) && + // advance to the next closing parenthesis + (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) { + + // excess is a negative index + match[0] = match[0].slice( 0, excess ); + match[2] = unquoted.slice( 0, excess ); + } + + // Return only captures needed by the pseudo filter method (type and argument) + return match.slice( 0, 3 ); + } + }, + + filter: { + + "TAG": function( nodeNameSelector ) { + var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase(); + return nodeNameSelector === "*" ? + function() { return true; } : + function( elem ) { + return elem.nodeName && elem.nodeName.toLowerCase() === nodeName; + }; + }, + + "CLASS": function( className ) { + var pattern = classCache[ className + " " ]; + + return pattern || + (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) && + classCache( className, function( elem ) { + return pattern.test( typeof elem.className === "string" && elem.className || typeof elem.getAttribute !== strundefined && elem.getAttribute("class") || "" ); + }); + }, + + "ATTR": function( name, operator, check ) { + return function( elem ) { + var result = Sizzle.attr( elem, name ); + + if ( result == null ) { + return operator === "!="; + } + if ( !operator ) { + return true; + } + + result += ""; + + return operator === "=" ? result === check : + operator === "!=" ? result !== check : + operator === "^=" ? check && result.indexOf( check ) === 0 : + operator === "*=" ? check && result.indexOf( check ) > -1 : + operator === "$=" ? check && result.slice( -check.length ) === check : + operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 : + operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" : + false; + }; + }, + + "CHILD": function( type, what, argument, first, last ) { + var simple = type.slice( 0, 3 ) !== "nth", + forward = type.slice( -4 ) !== "last", + ofType = what === "of-type"; + + return first === 1 && last === 0 ? + + // Shortcut for :nth-*(n) + function( elem ) { + return !!elem.parentNode; + } : + + function( elem, context, xml ) { + var cache, outerCache, node, diff, nodeIndex, start, + dir = simple !== forward ? "nextSibling" : "previousSibling", + parent = elem.parentNode, + name = ofType && elem.nodeName.toLowerCase(), + useCache = !xml && !ofType; + + if ( parent ) { + + // :(first|last|only)-(child|of-type) + if ( simple ) { + while ( dir ) { + node = elem; + while ( (node = node[ dir ]) ) { + if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) { + return false; + } + } + // Reverse direction for :only-* (if we haven't yet done so) + start = dir = type === "only" && !start && "nextSibling"; + } + return true; + } + + start = [ forward ? parent.firstChild : parent.lastChild ]; + + // non-xml :nth-child(...) stores cache data on `parent` + if ( forward && useCache ) { + // Seek `elem` from a previously-cached index + outerCache = parent[ expando ] || (parent[ expando ] = {}); + cache = outerCache[ type ] || []; + nodeIndex = cache[0] === dirruns && cache[1]; + diff = cache[0] === dirruns && cache[2]; + node = nodeIndex && parent.childNodes[ nodeIndex ]; + + while ( (node = ++nodeIndex && node && node[ dir ] || + + // Fallback to seeking `elem` from the start + (diff = nodeIndex = 0) || start.pop()) ) { + + // When found, cache indexes on `parent` and break + if ( node.nodeType === 1 && ++diff && node === elem ) { + outerCache[ type ] = [ dirruns, nodeIndex, diff ]; + break; + } + } + + // Use previously-cached element index if available + } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) { + diff = cache[1]; + + // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...) + } else { + // Use the same loop as above to seek `elem` from the start + while ( (node = ++nodeIndex && node && node[ dir ] || + (diff = nodeIndex = 0) || start.pop()) ) { + + if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) { + // Cache the index of each encountered element + if ( useCache ) { + (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ]; + } + + if ( node === elem ) { + break; + } + } + } + } + + // Incorporate the offset, then check against cycle size + diff -= last; + return diff === first || ( diff % first === 0 && diff / first >= 0 ); + } + }; + }, + + "PSEUDO": function( pseudo, argument ) { + // pseudo-class names are case-insensitive + // http://www.w3.org/TR/selectors/#pseudo-classes + // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters + // Remember that setFilters inherits from pseudos + var args, + fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] || + Sizzle.error( "unsupported pseudo: " + pseudo ); + + // The user may use createPseudo to indicate that + // arguments are needed to create the filter function + // just as Sizzle does + if ( fn[ expando ] ) { + return fn( argument ); + } + + // But maintain support for old signatures + if ( fn.length > 1 ) { + args = [ pseudo, pseudo, "", argument ]; + return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ? + markFunction(function( seed, matches ) { + var idx, + matched = fn( seed, argument ), + i = matched.length; + while ( i-- ) { + idx = indexOf.call( seed, matched[i] ); + seed[ idx ] = !( matches[ idx ] = matched[i] ); + } + }) : + function( elem ) { + return fn( elem, 0, args ); + }; + } + + return fn; + } + }, + + pseudos: { + // Potentially complex pseudos + "not": markFunction(function( selector ) { + // Trim the selector passed to compile + // to avoid treating leading and trailing + // spaces as combinators + var input = [], + results = [], + matcher = compile( selector.replace( rtrim, "$1" ) ); + + return matcher[ expando ] ? + markFunction(function( seed, matches, context, xml ) { + var elem, + unmatched = matcher( seed, null, xml, [] ), + i = seed.length; + + // Match elements unmatched by `matcher` + while ( i-- ) { + if ( (elem = unmatched[i]) ) { + seed[i] = !(matches[i] = elem); + } + } + }) : + function( elem, context, xml ) { + input[0] = elem; + matcher( input, null, xml, results ); + return !results.pop(); + }; + }), + + "has": markFunction(function( selector ) { + return function( elem ) { + return Sizzle( selector, elem ).length > 0; + }; + }), + + "contains": markFunction(function( text ) { + return function( elem ) { + return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1; + }; + }), + + // "Whether an element is represented by a :lang() selector + // is based solely on the element's language value + // being equal to the identifier C, + // or beginning with the identifier C immediately followed by "-". + // The matching of C against the element's language value is performed case-insensitively. + // The identifier C does not have to be a valid language name." + // http://www.w3.org/TR/selectors/#lang-pseudo + "lang": markFunction( function( lang ) { + // lang value must be a valid identifier + if ( !ridentifier.test(lang || "") ) { + Sizzle.error( "unsupported lang: " + lang ); + } + lang = lang.replace( runescape, funescape ).toLowerCase(); + return function( elem ) { + var elemLang; + do { + if ( (elemLang = documentIsHTML ? + elem.lang : + elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) { + + elemLang = elemLang.toLowerCase(); + return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0; + } + } while ( (elem = elem.parentNode) && elem.nodeType === 1 ); + return false; + }; + }), + + // Miscellaneous + "target": function( elem ) { + var hash = window.location && window.location.hash; + return hash && hash.slice( 1 ) === elem.id; + }, + + "root": function( elem ) { + return elem === docElem; + }, + + "focus": function( elem ) { + return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex); + }, + + // Boolean properties + "enabled": function( elem ) { + return elem.disabled === false; + }, + + "disabled": function( elem ) { + return elem.disabled === true; + }, + + "checked": function( elem ) { + // In CSS3, :checked should return both checked and selected elements + // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked + var nodeName = elem.nodeName.toLowerCase(); + return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected); + }, + + "selected": function( elem ) { + // Accessing this property makes selected-by-default + // options in Safari work properly + if ( elem.parentNode ) { + elem.parentNode.selectedIndex; + } + + return elem.selected === true; + }, + + // Contents + "empty": function( elem ) { + // http://www.w3.org/TR/selectors/#empty-pseudo + // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5), + // but not by others (comment: 8; processing instruction: 7; etc.) + // nodeType < 6 works because attributes (2) do not appear as children + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeType < 6 ) { + return false; + } + } + return true; + }, + + "parent": function( elem ) { + return !Expr.pseudos["empty"]( elem ); + }, + + // Element/input types + "header": function( elem ) { + return rheader.test( elem.nodeName ); + }, + + "input": function( elem ) { + return rinputs.test( elem.nodeName ); + }, + + "button": function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === "button" || name === "button"; + }, + + "text": function( elem ) { + var attr; + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + + // Support: IE<8 + // New HTML5 attribute values (e.g., "search") appear with elem.type === "text" + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === "text" ); + }, + + // Position-in-collection + "first": createPositionalPseudo(function() { + return [ 0 ]; + }), + + "last": createPositionalPseudo(function( matchIndexes, length ) { + return [ length - 1 ]; + }), + + "eq": createPositionalPseudo(function( matchIndexes, length, argument ) { + return [ argument < 0 ? argument + length : argument ]; + }), + + "even": createPositionalPseudo(function( matchIndexes, length ) { + var i = 0; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "odd": createPositionalPseudo(function( matchIndexes, length ) { + var i = 1; + for ( ; i < length; i += 2 ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "lt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; --i >= 0; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }), + + "gt": createPositionalPseudo(function( matchIndexes, length, argument ) { + var i = argument < 0 ? argument + length : argument; + for ( ; ++i < length; ) { + matchIndexes.push( i ); + } + return matchIndexes; + }) + } +}; + +Expr.pseudos["nth"] = Expr.pseudos["eq"]; + +// Add button/input type pseudos +for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) { + Expr.pseudos[ i ] = createInputPseudo( i ); +} +for ( i in { submit: true, reset: true } ) { + Expr.pseudos[ i ] = createButtonPseudo( i ); +} + +// Easy API for creating new setFilters +function setFilters() {} +setFilters.prototype = Expr.filters = Expr.pseudos; +Expr.setFilters = new setFilters(); + +tokenize = Sizzle.tokenize = function( selector, parseOnly ) { + var matched, match, tokens, type, + soFar, groups, preFilters, + cached = tokenCache[ selector + " " ]; + + if ( cached ) { + return parseOnly ? 0 : cached.slice( 0 ); + } + + soFar = selector; + groups = []; + preFilters = Expr.preFilter; + + while ( soFar ) { + + // Comma and first run + if ( !matched || (match = rcomma.exec( soFar )) ) { + if ( match ) { + // Don't consume trailing commas as valid + soFar = soFar.slice( match[0].length ) || soFar; + } + groups.push( (tokens = []) ); + } + + matched = false; + + // Combinators + if ( (match = rcombinators.exec( soFar )) ) { + matched = match.shift(); + tokens.push({ + value: matched, + // Cast descendant combinators to space + type: match[0].replace( rtrim, " " ) + }); + soFar = soFar.slice( matched.length ); + } + + // Filters + for ( type in Expr.filter ) { + if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] || + (match = preFilters[ type ]( match ))) ) { + matched = match.shift(); + tokens.push({ + value: matched, + type: type, + matches: match + }); + soFar = soFar.slice( matched.length ); + } + } + + if ( !matched ) { + break; + } + } + + // Return the length of the invalid excess + // if we're just parsing + // Otherwise, throw an error or return tokens + return parseOnly ? + soFar.length : + soFar ? + Sizzle.error( selector ) : + // Cache the tokens + tokenCache( selector, groups ).slice( 0 ); +}; + +function toSelector( tokens ) { + var i = 0, + len = tokens.length, + selector = ""; + for ( ; i < len; i++ ) { + selector += tokens[i].value; + } + return selector; +} + +function addCombinator( matcher, combinator, base ) { + var dir = combinator.dir, + checkNonElements = base && dir === "parentNode", + doneName = done++; + + return combinator.first ? + // Check against closest ancestor/preceding element + function( elem, context, xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + return matcher( elem, context, xml ); + } + } + } : + + // Check against all ancestor/preceding elements + function( elem, context, xml ) { + var oldCache, outerCache, + newCache = [ dirruns, doneName ]; + + // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching + if ( xml ) { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + if ( matcher( elem, context, xml ) ) { + return true; + } + } + } + } else { + while ( (elem = elem[ dir ]) ) { + if ( elem.nodeType === 1 || checkNonElements ) { + outerCache = elem[ expando ] || (elem[ expando ] = {}); + if ( (oldCache = outerCache[ dir ]) && + oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) { + + // Assign to newCache so results back-propagate to previous elements + return (newCache[ 2 ] = oldCache[ 2 ]); + } else { + // Reuse newcache so results back-propagate to previous elements + outerCache[ dir ] = newCache; + + // A match means we're done; a fail means we have to keep checking + if ( (newCache[ 2 ] = matcher( elem, context, xml )) ) { + return true; + } + } + } + } + } + }; +} + +function elementMatcher( matchers ) { + return matchers.length > 1 ? + function( elem, context, xml ) { + var i = matchers.length; + while ( i-- ) { + if ( !matchers[i]( elem, context, xml ) ) { + return false; + } + } + return true; + } : + matchers[0]; +} + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function condense( unmatched, map, filter, context, xml ) { + var elem, + newUnmatched = [], + i = 0, + len = unmatched.length, + mapped = map != null; + + for ( ; i < len; i++ ) { + if ( (elem = unmatched[i]) ) { + if ( !filter || filter( elem, context, xml ) ) { + newUnmatched.push( elem ); + if ( mapped ) { + map.push( i ); + } + } + } + } + + return newUnmatched; +} + +function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) { + if ( postFilter && !postFilter[ expando ] ) { + postFilter = setMatcher( postFilter ); + } + if ( postFinder && !postFinder[ expando ] ) { + postFinder = setMatcher( postFinder, postSelector ); + } + return markFunction(function( seed, results, context, xml ) { + var temp, i, elem, + preMap = [], + postMap = [], + preexisting = results.length, + + // Get initial elements from seed or context + elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ), + + // Prefilter to get matcher input, preserving a map for seed-results synchronization + matcherIn = preFilter && ( seed || !selector ) ? + condense( elems, preMap, preFilter, context, xml ) : + elems, + + matcherOut = matcher ? + // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results, + postFinder || ( seed ? preFilter : preexisting || postFilter ) ? + + // ...intermediate processing is necessary + [] : + + // ...otherwise use results directly + results : + matcherIn; + + // Find primary matches + if ( matcher ) { + matcher( matcherIn, matcherOut, context, xml ); + } + + // Apply postFilter + if ( postFilter ) { + temp = condense( matcherOut, postMap ); + postFilter( temp, [], context, xml ); + + // Un-match failing elements by moving them back to matcherIn + i = temp.length; + while ( i-- ) { + if ( (elem = temp[i]) ) { + matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem); + } + } + } + + if ( seed ) { + if ( postFinder || preFilter ) { + if ( postFinder ) { + // Get the final matcherOut by condensing this intermediate into postFinder contexts + temp = []; + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) ) { + // Restore matcherIn since elem is not yet a final match + temp.push( (matcherIn[i] = elem) ); + } + } + postFinder( null, (matcherOut = []), temp, xml ); + } + + // Move matched elements from seed to results to keep them synchronized + i = matcherOut.length; + while ( i-- ) { + if ( (elem = matcherOut[i]) && + (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) { + + seed[temp] = !(results[temp] = elem); + } + } + } + + // Add elements to results, through postFinder if defined + } else { + matcherOut = condense( + matcherOut === results ? + matcherOut.splice( preexisting, matcherOut.length ) : + matcherOut + ); + if ( postFinder ) { + postFinder( null, results, matcherOut, xml ); + } else { + push.apply( results, matcherOut ); + } + } + }); +} + +function matcherFromTokens( tokens ) { + var checkContext, matcher, j, + len = tokens.length, + leadingRelative = Expr.relative[ tokens[0].type ], + implicitRelative = leadingRelative || Expr.relative[" "], + i = leadingRelative ? 1 : 0, + + // The foundational matcher ensures that elements are reachable from top-level context(s) + matchContext = addCombinator( function( elem ) { + return elem === checkContext; + }, implicitRelative, true ), + matchAnyContext = addCombinator( function( elem ) { + return indexOf.call( checkContext, elem ) > -1; + }, implicitRelative, true ), + matchers = [ function( elem, context, xml ) { + return ( !leadingRelative && ( xml || context !== outermostContext ) ) || ( + (checkContext = context).nodeType ? + matchContext( elem, context, xml ) : + matchAnyContext( elem, context, xml ) ); + } ]; + + for ( ; i < len; i++ ) { + if ( (matcher = Expr.relative[ tokens[i].type ]) ) { + matchers = [ addCombinator(elementMatcher( matchers ), matcher) ]; + } else { + matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches ); + + // Return special upon seeing a positional matcher + if ( matcher[ expando ] ) { + // Find the next relative operator (if any) for proper handling + j = ++i; + for ( ; j < len; j++ ) { + if ( Expr.relative[ tokens[j].type ] ) { + break; + } + } + return setMatcher( + i > 1 && elementMatcher( matchers ), + i > 1 && toSelector( + // If the preceding token was a descendant combinator, insert an implicit any-element `*` + tokens.slice( 0, i - 1 ).concat({ value: tokens[ i - 2 ].type === " " ? "*" : "" }) + ).replace( rtrim, "$1" ), + matcher, + i < j && matcherFromTokens( tokens.slice( i, j ) ), + j < len && matcherFromTokens( (tokens = tokens.slice( j )) ), + j < len && toSelector( tokens ) + ); + } + matchers.push( matcher ); + } + } + + return elementMatcher( matchers ); +} + +function matcherFromGroupMatchers( elementMatchers, setMatchers ) { + var bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, outermost ) { + var elem, j, matcher, + matchedCount = 0, + i = "0", + unmatched = seed && [], + setMatched = [], + contextBackup = outermostContext, + // We must always have either seed elements or outermost context + elems = seed || byElement && Expr.find["TAG"]( "*", outermost ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1), + len = elems.length; + + if ( outermost ) { + outermostContext = context !== document && context; + } + + // Add elements passing elementMatchers directly to results + // Keep `i` a string if there are no elements so `matchedCount` will be "00" below + // Support: IE<9, Safari + // Tolerate NodeList properties (IE: "length"; Safari: ) matching elements by id + for ( ; i !== len && (elem = elems[i]) != null; i++ ) { + if ( byElement && elem ) { + j = 0; + while ( (matcher = elementMatchers[j++]) ) { + if ( matcher( elem, context, xml ) ) { + results.push( elem ); + break; + } + } + if ( outermost ) { + dirruns = dirrunsUnique; + } + } + + // Track unmatched elements for set filters + if ( bySet ) { + // They will have gone through all possible matchers + if ( (elem = !matcher && elem) ) { + matchedCount--; + } + + // Lengthen the array for every element, matched or not + if ( seed ) { + unmatched.push( elem ); + } + } + } + + // Apply set filters to unmatched elements + matchedCount += i; + if ( bySet && i !== matchedCount ) { + j = 0; + while ( (matcher = setMatchers[j++]) ) { + matcher( unmatched, setMatched, context, xml ); + } + + if ( seed ) { + // Reintegrate element matches to eliminate the need for sorting + if ( matchedCount > 0 ) { + while ( i-- ) { + if ( !(unmatched[i] || setMatched[i]) ) { + setMatched[i] = pop.call( results ); + } + } + } + + // Discard index placeholder values to get only actual matches + setMatched = condense( setMatched ); + } + + // Add matches to results + push.apply( results, setMatched ); + + // Seedless set matches succeeding multiple successful matchers stipulate sorting + if ( outermost && !seed && setMatched.length > 0 && + ( matchedCount + setMatchers.length ) > 1 ) { + + Sizzle.uniqueSort( results ); + } + } + + // Override manipulation of globals by nested matchers + if ( outermost ) { + dirruns = dirrunsUnique; + outermostContext = contextBackup; + } + + return unmatched; + }; + + return bySet ? + markFunction( superMatcher ) : + superMatcher; +} + +compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) { + var i, + setMatchers = [], + elementMatchers = [], + cached = compilerCache[ selector + " " ]; + + if ( !cached ) { + // Generate a function of recursive functions that can be used to check each element + if ( !match ) { + match = tokenize( selector ); + } + i = match.length; + while ( i-- ) { + cached = matcherFromTokens( match[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + + // Save selector and tokenization + cached.selector = selector; + } + return cached; +}; + +/** + * A low-level selection function that works with Sizzle's compiled + * selector functions + * @param {String|Function} selector A selector or a pre-compiled + * selector function built with Sizzle.compile + * @param {Element} context + * @param {Array} [results] + * @param {Array} [seed] A set of elements to match against + */ +select = Sizzle.select = function( selector, context, results, seed ) { + var i, tokens, token, type, find, + compiled = typeof selector === "function" && selector, + match = !seed && tokenize( (selector = compiled.selector || selector) ); + + results = results || []; + + // Try to minimize operations if there is no seed and only one group + if ( match.length === 1 ) { + + // Take a shortcut and set the context if the root selector is an ID + tokens = match[0] = match[0].slice( 0 ); + if ( tokens.length > 2 && (token = tokens[0]).type === "ID" && + support.getById && context.nodeType === 9 && documentIsHTML && + Expr.relative[ tokens[1].type ] ) { + + context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0]; + if ( !context ) { + return results; + + // Precompiled matchers will still verify ancestry, so step up a level + } else if ( compiled ) { + context = context.parentNode; + } + + selector = selector.slice( tokens.shift().value.length ); + } + + // Fetch a seed set for right-to-left matching + i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length; + while ( i-- ) { + token = tokens[i]; + + // Abort if we hit a combinator + if ( Expr.relative[ (type = token.type) ] ) { + break; + } + if ( (find = Expr.find[ type ]) ) { + // Search, expanding context for leading sibling combinators + if ( (seed = find( + token.matches[0].replace( runescape, funescape ), + rsibling.test( tokens[0].type ) && testContext( context.parentNode ) || context + )) ) { + + // If seed is empty or no tokens remain, we can return early + tokens.splice( i, 1 ); + selector = seed.length && toSelector( tokens ); + if ( !selector ) { + push.apply( results, seed ); + return results; + } + + break; + } + } + } + } + + // Compile and execute a filtering function if one is not provided + // Provide `match` to avoid retokenization if we modified the selector above + ( compiled || compile( selector, match ) )( + seed, + context, + !documentIsHTML, + results, + rsibling.test( selector ) && testContext( context.parentNode ) || context + ); + return results; +}; + +// One-time assignments + +// Sort stability +support.sortStable = expando.split("").sort( sortOrder ).join("") === expando; + +// Support: Chrome<14 +// Always assume duplicates if they aren't passed to the comparison function +support.detectDuplicates = !!hasDuplicate; + +// Initialize against the default document +setDocument(); + +// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27) +// Detached nodes confoundingly follow *each other* +support.sortDetached = assert(function( div1 ) { + // Should return 1, but returns 4 (following) + return div1.compareDocumentPosition( document.createElement("div") ) & 1; +}); + +// Support: IE<8 +// Prevent attribute/property "interpolation" +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !assert(function( div ) { + div.innerHTML = ""; + return div.firstChild.getAttribute("href") === "#" ; +}) ) { + addHandle( "type|href|height|width", function( elem, name, isXML ) { + if ( !isXML ) { + return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 ); + } + }); +} + +// Support: IE<9 +// Use defaultValue in place of getAttribute("value") +if ( !support.attributes || !assert(function( div ) { + div.innerHTML = ""; + div.firstChild.setAttribute( "value", "" ); + return div.firstChild.getAttribute( "value" ) === ""; +}) ) { + addHandle( "value", function( elem, name, isXML ) { + if ( !isXML && elem.nodeName.toLowerCase() === "input" ) { + return elem.defaultValue; + } + }); +} + +// Support: IE<9 +// Use getAttributeNode to fetch booleans when getAttribute lies +if ( !assert(function( div ) { + return div.getAttribute("disabled") == null; +}) ) { + addHandle( booleans, function( elem, name, isXML ) { + var val; + if ( !isXML ) { + return elem[ name ] === true ? name.toLowerCase() : + (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + null; + } + }); +} + +return Sizzle; + +})( window ); + + + +jQuery.find = Sizzle; +jQuery.expr = Sizzle.selectors; +jQuery.expr[":"] = jQuery.expr.pseudos; +jQuery.unique = Sizzle.uniqueSort; +jQuery.text = Sizzle.getText; +jQuery.isXMLDoc = Sizzle.isXML; +jQuery.contains = Sizzle.contains; + + + +var rneedsContext = jQuery.expr.match.needsContext; + +var rsingleTag = (/^<(\w+)\s*\/?>(?:<\/\1>|)$/); + + + +var risSimple = /^.[^:#\[\.,]*$/; + +// Implement the identical functionality for filter and not +function winnow( elements, qualifier, not ) { + if ( jQuery.isFunction( qualifier ) ) { + return jQuery.grep( elements, function( elem, i ) { + /* jshint -W018 */ + return !!qualifier.call( elem, i, elem ) !== not; + }); + + } + + if ( qualifier.nodeType ) { + return jQuery.grep( elements, function( elem ) { + return ( elem === qualifier ) !== not; + }); + + } + + if ( typeof qualifier === "string" ) { + if ( risSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( indexOf.call( qualifier, elem ) >= 0 ) !== not; + }); +} + +jQuery.filter = function( expr, elems, not ) { + var elem = elems[ 0 ]; + + if ( not ) { + expr = ":not(" + expr + ")"; + } + + return elems.length === 1 && elem.nodeType === 1 ? + jQuery.find.matchesSelector( elem, expr ) ? [ elem ] : [] : + jQuery.find.matches( expr, jQuery.grep( elems, function( elem ) { + return elem.nodeType === 1; + })); +}; + +jQuery.fn.extend({ + find: function( selector ) { + var i, + len = this.length, + ret = [], + self = this; + + if ( typeof selector !== "string" ) { + return this.pushStack( jQuery( selector ).filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( self[ i ], this ) ) { + return true; + } + } + }) ); + } + + for ( i = 0; i < len; i++ ) { + jQuery.find( selector, self[ i ], ret ); + } + + // Needed because $( selector, context ) becomes $( context ).find( selector ) + ret = this.pushStack( len > 1 ? jQuery.unique( ret ) : ret ); + ret.selector = this.selector ? this.selector + " " + selector : selector; + return ret; + }, + filter: function( selector ) { + return this.pushStack( winnow(this, selector || [], false) ); + }, + not: function( selector ) { + return this.pushStack( winnow(this, selector || [], true) ); + }, + is: function( selector ) { + return !!winnow( + this, + + // If this is a positional/relative selector, check membership in the returned set + // so $("p:first").is("p:last") won't return true for a doc with two "p". + typeof selector === "string" && rneedsContext.test( selector ) ? + jQuery( selector ) : + selector || [], + false + ).length; + } +}); + + +// Initialize a jQuery object + + +// A central reference to the root jQuery(document) +var rootjQuery, + + // A simple way to check for HTML strings + // Prioritize #id over to avoid XSS via location.hash (#9521) + // Strict HTML recognition (#11290: must start with <) + rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]*))$/, + + init = jQuery.fn.init = function( selector, context ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector[0] === "<" && selector[ selector.length - 1 ] === ">" && selector.length >= 3 ) { + // Assume that strings that start and end with <> are HTML and skip the regex check + match = [ null, selector, null ]; + + } else { + match = rquickExpr.exec( selector ); + } + + // Match html or make sure no context is specified for #id + if ( match && (match[1] || !context) ) { + + // HANDLE: $(html) -> $(array) + if ( match[1] ) { + context = context instanceof jQuery ? context[0] : context; + + // scripts is true for back-compat + // Intentionally let the error be thrown if parseHTML is not present + jQuery.merge( this, jQuery.parseHTML( + match[1], + context && context.nodeType ? context.ownerDocument || context : document, + true + ) ); + + // HANDLE: $(html, props) + if ( rsingleTag.test( match[1] ) && jQuery.isPlainObject( context ) ) { + for ( match in context ) { + // Properties of context are called as methods if possible + if ( jQuery.isFunction( this[ match ] ) ) { + this[ match ]( context[ match ] ); + + // ...and otherwise set as attributes + } else { + this.attr( match, context[ match ] ); + } + } + } + + return this; + + // HANDLE: $(#id) + } else { + elem = document.getElementById( match[2] ); + + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Inject the element directly into the jQuery object + this.length = 1; + this[0] = elem; + } + + this.context = document; + this.selector = selector; + return this; + } + + // HANDLE: $(expr, $(...)) + } else if ( !context || context.jquery ) { + return ( context || rootjQuery ).find( selector ); + + // HANDLE: $(expr, context) + // (which is just equivalent to: $(context).find(expr) + } else { + return this.constructor( context ).find( selector ); + } + + // HANDLE: $(DOMElement) + } else if ( selector.nodeType ) { + this.context = this[0] = selector; + this.length = 1; + return this; + + // HANDLE: $(function) + // Shortcut for document ready + } else if ( jQuery.isFunction( selector ) ) { + return typeof rootjQuery.ready !== "undefined" ? + rootjQuery.ready( selector ) : + // Execute immediately if ready is not present + selector( jQuery ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }; + +// Give the init function the jQuery prototype for later instantiation +init.prototype = jQuery.fn; + +// Initialize central reference +rootjQuery = jQuery( document ); + + +var rparentsprev = /^(?:parents|prev(?:Until|All))/, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.extend({ + dir: function( elem, dir, until ) { + var matched = [], + truncate = until !== undefined; + + while ( (elem = elem[ dir ]) && elem.nodeType !== 9 ) { + if ( elem.nodeType === 1 ) { + if ( truncate && jQuery( elem ).is( until ) ) { + break; + } + matched.push( elem ); + } + } + return matched; + }, + + sibling: function( n, elem ) { + var matched = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + matched.push( n ); + } + } + + return matched; + } +}); + +jQuery.fn.extend({ + has: function( target ) { + var targets = jQuery( target, this ), + l = targets.length; + + return this.filter(function() { + var i = 0; + for ( ; i < l; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + matched = [], + pos = rneedsContext.test( selectors ) || typeof selectors !== "string" ? + jQuery( selectors, context || this.context ) : + 0; + + for ( ; i < l; i++ ) { + for ( cur = this[i]; cur && cur !== context; cur = cur.parentNode ) { + // Always skip document fragments + if ( cur.nodeType < 11 && (pos ? + pos.index(cur) > -1 : + + // Don't pass non-elements to Sizzle + cur.nodeType === 1 && + jQuery.find.matchesSelector(cur, selectors)) ) { + + matched.push( cur ); + break; + } + } + } + + return this.pushStack( matched.length > 1 ? jQuery.unique( matched ) : matched ); + }, + + // Determine the position of an element within + // the matched set of elements + index: function( elem ) { + + // No argument, return index in parent + if ( !elem ) { + return ( this[ 0 ] && this[ 0 ].parentNode ) ? this.first().prevAll().length : -1; + } + + // index in selector + if ( typeof elem === "string" ) { + return indexOf.call( jQuery( elem ), this[ 0 ] ); + } + + // Locate the position of the desired element + return indexOf.call( this, + + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[ 0 ] : elem + ); + }, + + add: function( selector, context ) { + return this.pushStack( + jQuery.unique( + jQuery.merge( this.get(), jQuery( selector, context ) ) + ) + ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +function sibling( cur, dir ) { + while ( (cur = cur[dir]) && cur.nodeType !== 1 ) {} + return cur; +} + +jQuery.each({ + parent: function( elem ) { + var parent = elem.parentNode; + return parent && parent.nodeType !== 11 ? parent : null; + }, + parents: function( elem ) { + return jQuery.dir( elem, "parentNode" ); + }, + parentsUntil: function( elem, i, until ) { + return jQuery.dir( elem, "parentNode", until ); + }, + next: function( elem ) { + return sibling( elem, "nextSibling" ); + }, + prev: function( elem ) { + return sibling( elem, "previousSibling" ); + }, + nextAll: function( elem ) { + return jQuery.dir( elem, "nextSibling" ); + }, + prevAll: function( elem ) { + return jQuery.dir( elem, "previousSibling" ); + }, + nextUntil: function( elem, i, until ) { + return jQuery.dir( elem, "nextSibling", until ); + }, + prevUntil: function( elem, i, until ) { + return jQuery.dir( elem, "previousSibling", until ); + }, + siblings: function( elem ) { + return jQuery.sibling( ( elem.parentNode || {} ).firstChild, elem ); + }, + children: function( elem ) { + return jQuery.sibling( elem.firstChild ); + }, + contents: function( elem ) { + return elem.contentDocument || jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var matched = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + matched = jQuery.filter( selector, matched ); + } + + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + jQuery.unique( matched ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + matched.reverse(); + } + } + + return this.pushStack( matched ); + }; +}); +var rnotwhite = (/\S+/g); + + + +// String to Object options format cache +var optionsCache = {}; + +// Convert String-formatted options into Object-formatted ones and store in cache +function createOptions( options ) { + var object = optionsCache[ options ] = {}; + jQuery.each( options.match( rnotwhite ) || [], function( _, flag ) { + object[ flag ] = true; + }); + return object; +} + +/* + * Create a callback list using the following parameters: + * + * options: an optional list of space-separated options that will change how + * the callback list behaves or a more traditional option object + * + * By default a callback list will act like an event callback list and can be + * "fired" multiple times. + * + * Possible options: + * + * once: will ensure the callback list can only be fired once (like a Deferred) + * + * memory: will keep track of previous values and will call any callback added + * after the list has been fired right away with the latest "memorized" + * values (like a Deferred) + * + * unique: will ensure a callback can only be added once (no duplicate in the list) + * + * stopOnFalse: interrupt callings when a callback returns false + * + */ +jQuery.Callbacks = function( options ) { + + // Convert options from String-formatted to Object-formatted if needed + // (we check in cache first) + options = typeof options === "string" ? + ( optionsCache[ options ] || createOptions( options ) ) : + jQuery.extend( {}, options ); + + var // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // Flag to know if list is currently firing + firing, + // First callback to fire (used internally by add and fireWith) + firingStart, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // Actual callback list + list = [], + // Stack of fire calls for repeatable lists + stack = !options.once && [], + // Fire callbacks + fire = function( data ) { + memory = options.memory && data; + fired = true; + firingIndex = firingStart || 0; + firingStart = 0; + firingLength = list.length; + firing = true; + for ( ; list && firingIndex < firingLength; firingIndex++ ) { + if ( list[ firingIndex ].apply( data[ 0 ], data[ 1 ] ) === false && options.stopOnFalse ) { + memory = false; // To prevent further calls using add + break; + } + } + firing = false; + if ( list ) { + if ( stack ) { + if ( stack.length ) { + fire( stack.shift() ); + } + } else if ( memory ) { + list = []; + } else { + self.disable(); + } + } + }, + // Actual Callbacks object + self = { + // Add a callback or a collection of callbacks to the list + add: function() { + if ( list ) { + // First, we save the current length + var start = list.length; + (function add( args ) { + jQuery.each( args, function( _, arg ) { + var type = jQuery.type( arg ); + if ( type === "function" ) { + if ( !options.unique || !self.has( arg ) ) { + list.push( arg ); + } + } else if ( arg && arg.length && type !== "string" ) { + // Inspect recursively + add( arg ); + } + }); + })( arguments ); + // Do we need to add the callbacks to the + // current firing batch? + if ( firing ) { + firingLength = list.length; + // With memory, if we're not firing then + // we should call right away + } else if ( memory ) { + firingStart = start; + fire( memory ); + } + } + return this; + }, + // Remove a callback from the list + remove: function() { + if ( list ) { + jQuery.each( arguments, function( _, arg ) { + var index; + while ( ( index = jQuery.inArray( arg, list, index ) ) > -1 ) { + list.splice( index, 1 ); + // Handle firing indexes + if ( firing ) { + if ( index <= firingLength ) { + firingLength--; + } + if ( index <= firingIndex ) { + firingIndex--; + } + } + } + }); + } + return this; + }, + // Check if a given callback is in the list. + // If no argument is given, return whether or not list has callbacks attached. + has: function( fn ) { + return fn ? jQuery.inArray( fn, list ) > -1 : !!( list && list.length ); + }, + // Remove all callbacks from the list + empty: function() { + list = []; + firingLength = 0; + return this; + }, + // Have the list do nothing anymore + disable: function() { + list = stack = memory = undefined; + return this; + }, + // Is it disabled? + disabled: function() { + return !list; + }, + // Lock the list in its current state + lock: function() { + stack = undefined; + if ( !memory ) { + self.disable(); + } + return this; + }, + // Is it locked? + locked: function() { + return !stack; + }, + // Call all callbacks with the given context and arguments + fireWith: function( context, args ) { + if ( list && ( !fired || stack ) ) { + args = args || []; + args = [ context, args.slice ? args.slice() : args ]; + if ( firing ) { + stack.push( args ); + } else { + fire( args ); + } + } + return this; + }, + // Call all the callbacks with the given arguments + fire: function() { + self.fireWith( this, arguments ); + return this; + }, + // To know if the callbacks have already been called at least once + fired: function() { + return !!fired; + } + }; + + return self; +}; + + +jQuery.extend({ + + Deferred: function( func ) { + var tuples = [ + // action, add listener, listener list, final state + [ "resolve", "done", jQuery.Callbacks("once memory"), "resolved" ], + [ "reject", "fail", jQuery.Callbacks("once memory"), "rejected" ], + [ "notify", "progress", jQuery.Callbacks("memory") ] + ], + state = "pending", + promise = { + state: function() { + return state; + }, + always: function() { + deferred.done( arguments ).fail( arguments ); + return this; + }, + then: function( /* fnDone, fnFail, fnProgress */ ) { + var fns = arguments; + return jQuery.Deferred(function( newDefer ) { + jQuery.each( tuples, function( i, tuple ) { + var fn = jQuery.isFunction( fns[ i ] ) && fns[ i ]; + // deferred[ done | fail | progress ] for forwarding actions to newDefer + deferred[ tuple[1] ](function() { + var returned = fn && fn.apply( this, arguments ); + if ( returned && jQuery.isFunction( returned.promise ) ) { + returned.promise() + .done( newDefer.resolve ) + .fail( newDefer.reject ) + .progress( newDefer.notify ); + } else { + newDefer[ tuple[ 0 ] + "With" ]( this === promise ? newDefer.promise() : this, fn ? [ returned ] : arguments ); + } + }); + }); + fns = null; + }).promise(); + }, + // Get a promise for this deferred + // If obj is provided, the promise aspect is added to the object + promise: function( obj ) { + return obj != null ? jQuery.extend( obj, promise ) : promise; + } + }, + deferred = {}; + + // Keep pipe for back-compat + promise.pipe = promise.then; + + // Add list-specific methods + jQuery.each( tuples, function( i, tuple ) { + var list = tuple[ 2 ], + stateString = tuple[ 3 ]; + + // promise[ done | fail | progress ] = list.add + promise[ tuple[1] ] = list.add; + + // Handle state + if ( stateString ) { + list.add(function() { + // state = [ resolved | rejected ] + state = stateString; + + // [ reject_list | resolve_list ].disable; progress_list.lock + }, tuples[ i ^ 1 ][ 2 ].disable, tuples[ 2 ][ 2 ].lock ); + } + + // deferred[ resolve | reject | notify ] + deferred[ tuple[0] ] = function() { + deferred[ tuple[0] + "With" ]( this === deferred ? promise : this, arguments ); + return this; + }; + deferred[ tuple[0] + "With" ] = list.fireWith; + }); + + // Make the deferred a promise + promise.promise( deferred ); + + // Call given func if any + if ( func ) { + func.call( deferred, deferred ); + } + + // All done! + return deferred; + }, + + // Deferred helper + when: function( subordinate /* , ..., subordinateN */ ) { + var i = 0, + resolveValues = slice.call( arguments ), + length = resolveValues.length, + + // the count of uncompleted subordinates + remaining = length !== 1 || ( subordinate && jQuery.isFunction( subordinate.promise ) ) ? length : 0, + + // the master Deferred. If resolveValues consist of only a single Deferred, just use that. + deferred = remaining === 1 ? subordinate : jQuery.Deferred(), + + // Update function for both resolve and progress values + updateFunc = function( i, contexts, values ) { + return function( value ) { + contexts[ i ] = this; + values[ i ] = arguments.length > 1 ? slice.call( arguments ) : value; + if ( values === progressValues ) { + deferred.notifyWith( contexts, values ); + } else if ( !( --remaining ) ) { + deferred.resolveWith( contexts, values ); + } + }; + }, + + progressValues, progressContexts, resolveContexts; + + // add listeners to Deferred subordinates; treat others as resolved + if ( length > 1 ) { + progressValues = new Array( length ); + progressContexts = new Array( length ); + resolveContexts = new Array( length ); + for ( ; i < length; i++ ) { + if ( resolveValues[ i ] && jQuery.isFunction( resolveValues[ i ].promise ) ) { + resolveValues[ i ].promise() + .done( updateFunc( i, resolveContexts, resolveValues ) ) + .fail( deferred.reject ) + .progress( updateFunc( i, progressContexts, progressValues ) ); + } else { + --remaining; + } + } + } + + // if we're not waiting on anything, resolve the master + if ( !remaining ) { + deferred.resolveWith( resolveContexts, resolveValues ); + } + + return deferred.promise(); + } +}); + + +// The deferred used on DOM ready +var readyList; + +jQuery.fn.ready = function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; +}; + +jQuery.extend({ + // Is the DOM ready to be used? Set to true once it occurs. + isReady: false, + + // A counter to track how many items to wait for before + // the ready event fires. See #6781 + readyWait: 1, + + // Hold (or release) the ready event + holdReady: function( hold ) { + if ( hold ) { + jQuery.readyWait++; + } else { + jQuery.ready( true ); + } + }, + + // Handle when the DOM is ready + ready: function( wait ) { + + // Abort if there are pending holds or we're already ready + if ( wait === true ? --jQuery.readyWait : jQuery.isReady ) { + return; + } + + // Remember that the DOM is ready + jQuery.isReady = true; + + // If a normal DOM Ready event fired, decrement, and wait if need be + if ( wait !== true && --jQuery.readyWait > 0 ) { + return; + } + + // If there are functions bound, to execute + readyList.resolveWith( document, [ jQuery ] ); + + // Trigger any bound ready events + if ( jQuery.fn.triggerHandler ) { + jQuery( document ).triggerHandler( "ready" ); + jQuery( document ).off( "ready" ); + } + } +}); + +/** + * The ready event handler and self cleanup method + */ +function completed() { + document.removeEventListener( "DOMContentLoaded", completed, false ); + window.removeEventListener( "load", completed, false ); + jQuery.ready(); +} + +jQuery.ready.promise = function( obj ) { + if ( !readyList ) { + + readyList = jQuery.Deferred(); + + // Catch cases where $(document).ready() is called after the browser event has already occurred. + // we once tried to use readyState "interactive" here, but it caused issues like the one + // discovered by ChrisS here: http://bugs.jquery.com/ticket/12282#comment:15 + if ( document.readyState === "complete" ) { + // Handle it asynchronously to allow scripts the opportunity to delay ready + setTimeout( jQuery.ready ); + + } else { + + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed, false ); + } + } + return readyList.promise( obj ); +}; + +// Kick off the DOM ready check even if the user does not +jQuery.ready.promise(); + + + + +// Multifunctional method to get and set values of a collection +// The value/s can optionally be executed if it's a function +var access = jQuery.access = function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + len = elems.length, + bulk = key == null; + + // Sets many values + if ( jQuery.type( key ) === "object" ) { + chainable = true; + for ( i in key ) { + jQuery.access( elems, fn, i, key[i], true, emptyGet, raw ); + } + + // Sets one value + } else if ( value !== undefined ) { + chainable = true; + + if ( !jQuery.isFunction( value ) ) { + raw = true; + } + + if ( bulk ) { + // Bulk operations run against the entire set + if ( raw ) { + fn.call( elems, value ); + fn = null; + + // ...except when executing function values + } else { + bulk = fn; + fn = function( elem, key, value ) { + return bulk.call( jQuery( elem ), value ); + }; + } + } + + if ( fn ) { + for ( ; i < len; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + len ? fn( elems[0], key ) : emptyGet; +}; + + +/** + * Determines whether an object can have data + */ +jQuery.acceptData = function( owner ) { + // Accepts only: + // - Node + // - Node.ELEMENT_NODE + // - Node.DOCUMENT_NODE + // - Object + // - Any + /* jshint -W018 */ + return owner.nodeType === 1 || owner.nodeType === 9 || !( +owner.nodeType ); +}; + + +function Data() { + // Support: Android < 4, + // Old WebKit does not have Object.preventExtensions/freeze method, + // return new empty object instead with no [[set]] accessor + Object.defineProperty( this.cache = {}, 0, { + get: function() { + return {}; + } + }); + + this.expando = jQuery.expando + Math.random(); +} + +Data.uid = 1; +Data.accepts = jQuery.acceptData; + +Data.prototype = { + key: function( owner ) { + // We can accept data for non-element nodes in modern browsers, + // but we should not, see #8335. + // Always return the key for a frozen object. + if ( !Data.accepts( owner ) ) { + return 0; + } + + var descriptor = {}, + // Check if the owner object already has a cache key + unlock = owner[ this.expando ]; + + // If not, create one + if ( !unlock ) { + unlock = Data.uid++; + + // Secure it in a non-enumerable, non-writable property + try { + descriptor[ this.expando ] = { value: unlock }; + Object.defineProperties( owner, descriptor ); + + // Support: Android < 4 + // Fallback to a less secure definition + } catch ( e ) { + descriptor[ this.expando ] = unlock; + jQuery.extend( owner, descriptor ); + } + } + + // Ensure the cache object + if ( !this.cache[ unlock ] ) { + this.cache[ unlock ] = {}; + } + + return unlock; + }, + set: function( owner, data, value ) { + var prop, + // There may be an unlock assigned to this node, + // if there is no entry for this "owner", create one inline + // and set the unlock as though an owner entry had always existed + unlock = this.key( owner ), + cache = this.cache[ unlock ]; + + // Handle: [ owner, key, value ] args + if ( typeof data === "string" ) { + cache[ data ] = value; + + // Handle: [ owner, { properties } ] args + } else { + // Fresh assignments by object are shallow copied + if ( jQuery.isEmptyObject( cache ) ) { + jQuery.extend( this.cache[ unlock ], data ); + // Otherwise, copy the properties one-by-one to the cache object + } else { + for ( prop in data ) { + cache[ prop ] = data[ prop ]; + } + } + } + return cache; + }, + get: function( owner, key ) { + // Either a valid cache is found, or will be created. + // New caches will be created and the unlock returned, + // allowing direct access to the newly created + // empty data object. A valid owner object must be provided. + var cache = this.cache[ this.key( owner ) ]; + + return key === undefined ? + cache : cache[ key ]; + }, + access: function( owner, key, value ) { + var stored; + // In cases where either: + // + // 1. No key was specified + // 2. A string key was specified, but no value provided + // + // Take the "read" path and allow the get method to determine + // which value to return, respectively either: + // + // 1. The entire cache object + // 2. The data stored at the key + // + if ( key === undefined || + ((key && typeof key === "string") && value === undefined) ) { + + stored = this.get( owner, key ); + + return stored !== undefined ? + stored : this.get( owner, jQuery.camelCase(key) ); + } + + // [*]When the key is not a string, or both a key and value + // are specified, set or extend (existing objects) with either: + // + // 1. An object of properties + // 2. A key and value + // + this.set( owner, key, value ); + + // Since the "set" path can have two possible entry points + // return the expected data based on which path was taken[*] + return value !== undefined ? value : key; + }, + remove: function( owner, key ) { + var i, name, camel, + unlock = this.key( owner ), + cache = this.cache[ unlock ]; + + if ( key === undefined ) { + this.cache[ unlock ] = {}; + + } else { + // Support array or space separated string of keys + if ( jQuery.isArray( key ) ) { + // If "name" is an array of keys... + // When data is initially created, via ("key", "val") signature, + // keys will be converted to camelCase. + // Since there is no way to tell _how_ a key was added, remove + // both plain key and camelCase key. #12786 + // This will only penalize the array argument path. + name = key.concat( key.map( jQuery.camelCase ) ); + } else { + camel = jQuery.camelCase( key ); + // Try the string as a key before any manipulation + if ( key in cache ) { + name = [ key, camel ]; + } else { + // If a key with the spaces exists, use it. + // Otherwise, create an array by matching non-whitespace + name = camel; + name = name in cache ? + [ name ] : ( name.match( rnotwhite ) || [] ); + } + } + + i = name.length; + while ( i-- ) { + delete cache[ name[ i ] ]; + } + } + }, + hasData: function( owner ) { + return !jQuery.isEmptyObject( + this.cache[ owner[ this.expando ] ] || {} + ); + }, + discard: function( owner ) { + if ( owner[ this.expando ] ) { + delete this.cache[ owner[ this.expando ] ]; + } + } +}; +var data_priv = new Data(); + +var data_user = new Data(); + + + +/* + Implementation Summary + + 1. Enforce API surface and semantic compatibility with 1.9.x branch + 2. Improve the module's maintainability by reducing the storage + paths to a single mechanism. + 3. Use the same single mechanism to support "private" and "user" data. + 4. _Never_ expose "private" data to user code (TODO: Drop _data, _removeData) + 5. Avoid exposing implementation details on user objects (eg. expando properties) + 6. Provide a clear path for implementation upgrade to WeakMap in 2014 +*/ +var rbrace = /^(?:\{[\w\W]*\}|\[[\w\W]*\])$/, + rmultiDash = /([A-Z])/g; + +function dataAttr( elem, key, data ) { + var name; + + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + name = "data-" + key.replace( rmultiDash, "-$1" ).toLowerCase(); + data = elem.getAttribute( name ); + + if ( typeof data === "string" ) { + try { + data = data === "true" ? true : + data === "false" ? false : + data === "null" ? null : + // Only convert to a number if it doesn't change the string + +data + "" === data ? +data : + rbrace.test( data ) ? jQuery.parseJSON( data ) : + data; + } catch( e ) {} + + // Make sure we set the data so it isn't changed later + data_user.set( elem, key, data ); + } else { + data = undefined; + } + } + return data; +} + +jQuery.extend({ + hasData: function( elem ) { + return data_user.hasData( elem ) || data_priv.hasData( elem ); + }, + + data: function( elem, name, data ) { + return data_user.access( elem, name, data ); + }, + + removeData: function( elem, name ) { + data_user.remove( elem, name ); + }, + + // TODO: Now that all calls to _data and _removeData have been replaced + // with direct calls to data_priv methods, these can be deprecated. + _data: function( elem, name, data ) { + return data_priv.access( elem, name, data ); + }, + + _removeData: function( elem, name ) { + data_priv.remove( elem, name ); + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var i, name, data, + elem = this[ 0 ], + attrs = elem && elem.attributes; + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = data_user.get( elem ); + + if ( elem.nodeType === 1 && !data_priv.get( elem, "hasDataAttrs" ) ) { + i = attrs.length; + while ( i-- ) { + + // Support: IE11+ + // The attrs elements can be null (#14894) + if ( attrs[ i ] ) { + name = attrs[ i ].name; + if ( name.indexOf( "data-" ) === 0 ) { + name = jQuery.camelCase( name.slice(5) ); + dataAttr( elem, name, data[ name ] ); + } + } + } + data_priv.set( elem, "hasDataAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + data_user.set( this, key ); + }); + } + + return access( this, function( value ) { + var data, + camelKey = jQuery.camelCase( key ); + + // The calling jQuery object (element matches) is not empty + // (and therefore has an element appears at this[ 0 ]) and the + // `value` parameter was not undefined. An empty jQuery object + // will result in `undefined` for elem = this[ 0 ] which will + // throw an exception if an attempt to read a data cache is made. + if ( elem && value === undefined ) { + // Attempt to get data from the cache + // with the key as-is + data = data_user.get( elem, key ); + if ( data !== undefined ) { + return data; + } + + // Attempt to get data from the cache + // with the key camelized + data = data_user.get( elem, camelKey ); + if ( data !== undefined ) { + return data; + } + + // Attempt to "discover" the data in + // HTML5 custom data-* attrs + data = dataAttr( elem, camelKey, undefined ); + if ( data !== undefined ) { + return data; + } + + // We tried really hard, but the data doesn't exist. + return; + } + + // Set the data... + this.each(function() { + // First, attempt to store a copy or reference of any + // data that might've been store with a camelCased key. + var data = data_user.get( this, camelKey ); + + // For HTML5 data-* attribute interop, we have to + // store property names with dashes in a camelCase form. + // This might not apply to all properties...* + data_user.set( this, camelKey, value ); + + // *... In the case of properties that might _actually_ + // have dashes, we need to also store a copy of that + // unchanged property. + if ( key.indexOf("-") !== -1 && data !== undefined ) { + data_user.set( this, key, value ); + } + }); + }, null, value, arguments.length > 1, null, true ); + }, + + removeData: function( key ) { + return this.each(function() { + data_user.remove( this, key ); + }); + } +}); + + +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = data_priv.get( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray( data ) ) { + queue = data_priv.access( elem, type, jQuery.makeArray(data) ); + } else { + queue.push( data ); + } + } + return queue || []; + } + }, + + dequeue: function( elem, type ) { + type = type || "fx"; + + var queue = jQuery.queue( elem, type ), + startLength = queue.length, + fn = queue.shift(), + hooks = jQuery._queueHooks( elem, type ), + next = function() { + jQuery.dequeue( elem, type ); + }; + + // If the fx queue is dequeued, always remove the progress sentinel + if ( fn === "inprogress" ) { + fn = queue.shift(); + startLength--; + } + + if ( fn ) { + + // Add a progress sentinel to prevent the fx queue from being + // automatically dequeued + if ( type === "fx" ) { + queue.unshift( "inprogress" ); + } + + // clear up the last queue stop function + delete hooks.stop; + fn.call( elem, next, hooks ); + } + + if ( !startLength && hooks ) { + hooks.empty.fire(); + } + }, + + // not intended for public consumption - generates a queueHooks object, or returns the current one + _queueHooks: function( elem, type ) { + var key = type + "queueHooks"; + return data_priv.get( elem, key ) || data_priv.access( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + data_priv.remove( elem, [ type + "queue", key ] ); + }) + }); + } +}); + +jQuery.fn.extend({ + queue: function( type, data ) { + var setter = 2; + + if ( typeof type !== "string" ) { + data = type; + type = "fx"; + setter--; + } + + if ( arguments.length < setter ) { + return jQuery.queue( this[0], type ); + } + + return data === undefined ? + this : + this.each(function() { + var queue = jQuery.queue( this, type, data ); + + // ensure a hooks for this queue + jQuery._queueHooks( this, type ); + + if ( type === "fx" && queue[0] !== "inprogress" ) { + jQuery.dequeue( this, type ); + } + }); + }, + dequeue: function( type ) { + return this.each(function() { + jQuery.dequeue( this, type ); + }); + }, + clearQueue: function( type ) { + return this.queue( type || "fx", [] ); + }, + // Get a promise resolved when queues of a certain type + // are emptied (fx is the type by default) + promise: function( type, obj ) { + var tmp, + count = 1, + defer = jQuery.Deferred(), + elements = this, + i = this.length, + resolve = function() { + if ( !( --count ) ) { + defer.resolveWith( elements, [ elements ] ); + } + }; + + if ( typeof type !== "string" ) { + obj = type; + type = undefined; + } + type = type || "fx"; + + while ( i-- ) { + tmp = data_priv.get( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var pnum = (/[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/).source; + +var cssExpand = [ "Top", "Right", "Bottom", "Left" ]; + +var isHidden = function( elem, el ) { + // isHidden might be called from jQuery#filter function; + // in that case, element will be second argument + elem = el || elem; + return jQuery.css( elem, "display" ) === "none" || !jQuery.contains( elem.ownerDocument, elem ); + }; + +var rcheckableType = (/^(?:checkbox|radio)$/i); + + + +(function() { + var fragment = document.createDocumentFragment(), + div = fragment.appendChild( document.createElement( "div" ) ), + input = document.createElement( "input" ); + + // #11217 - WebKit loses check when the name is after the checked attribute + // Support: Windows Web Apps (WWA) + // `name` and `type` need .setAttribute for WWA + input.setAttribute( "type", "radio" ); + input.setAttribute( "checked", "checked" ); + input.setAttribute( "name", "t" ); + + div.appendChild( input ); + + // Support: Safari 5.1, iOS 5.1, Android 4.x, Android 2.3 + // old WebKit doesn't clone checked state correctly in fragments + support.checkClone = div.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Make sure textarea (and checkbox) defaultValue is properly cloned + // Support: IE9-IE11+ + div.innerHTML = ""; + support.noCloneChecked = !!div.cloneNode( true ).lastChild.defaultValue; +})(); +var strundefined = typeof undefined; + + + +support.focusinBubbles = "onfocusin" in window; + + +var + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|pointer|contextmenu)|click/, + rfocusMorph = /^(?:focusinfocus|focusoutblur)$/, + rtypenamespace = /^([^.]*)(?:\.(.+)|)$/; + +function returnTrue() { + return true; +} + +function returnFalse() { + return false; +} + +function safeActiveElement() { + try { + return document.activeElement; + } catch ( err ) { } +} + +/* + * Helper functions for managing events -- not part of the public interface. + * Props to Dean Edwards' addEvent library for many of the ideas. + */ +jQuery.event = { + + global: {}, + + add: function( elem, types, handler, data, selector ) { + + var handleObjIn, eventHandle, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = data_priv.get( elem ); + + // Don't attach events to noData or text/comment nodes (but allow plain objects) + if ( !elemData ) { + return; + } + + // Caller can pass in an object of custom data in lieu of the handler + if ( handler.handler ) { + handleObjIn = handler; + handler = handleObjIn.handler; + selector = handleObjIn.selector; + } + + // Make sure that the handler has a unique ID, used to find/remove it later + if ( !handler.guid ) { + handler.guid = jQuery.guid++; + } + + // Init the element's event structure and main handler, if this is the first + if ( !(events = elemData.events) ) { + events = elemData.events = {}; + } + if ( !(eventHandle = elemData.handle) ) { + eventHandle = elemData.handle = function( e ) { + // Discard the second event of a jQuery.event.trigger() and + // when an event is called after a page has unloaded + return typeof jQuery !== strundefined && jQuery.event.triggered !== e.type ? + jQuery.event.dispatch.apply( elem, arguments ) : undefined; + }; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // There *must* be a type, no attaching namespace-only handlers + if ( !type ) { + continue; + } + + // If event changes its type, use the special event handlers for the changed type + special = jQuery.event.special[ type ] || {}; + + // If selector defined, determine special event api type, otherwise given type + type = ( selector ? special.delegateType : special.bindType ) || type; + + // Update special based on newly reset type + special = jQuery.event.special[ type ] || {}; + + // handleObj is passed to all event handlers + handleObj = jQuery.extend({ + type: type, + origType: origType, + data: data, + handler: handler, + guid: handler.guid, + selector: selector, + needsContext: selector && jQuery.expr.match.needsContext.test( selector ), + namespace: namespaces.join(".") + }, handleObjIn ); + + // Init the event handler queue if we're the first + if ( !(handlers = events[ type ]) ) { + handlers = events[ type ] = []; + handlers.delegateCount = 0; + + // Only use addEventListener if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + } + } + } + + if ( special.add ) { + special.add.call( elem, handleObj ); + + if ( !handleObj.handler.guid ) { + handleObj.handler.guid = handler.guid; + } + } + + // Add to the element's handler list, delegates in front + if ( selector ) { + handlers.splice( handlers.delegateCount++, 0, handleObj ); + } else { + handlers.push( handleObj ); + } + + // Keep track of which events have ever been used, for event optimization + jQuery.event.global[ type ] = true; + } + + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + + var j, origCount, tmp, + events, t, handleObj, + special, handlers, type, namespaces, origType, + elemData = data_priv.hasData( elem ) && data_priv.get( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( rnotwhite ) || [ "" ]; + t = types.length; + while ( t-- ) { + tmp = rtypenamespace.exec( types[t] ) || []; + type = origType = tmp[1]; + namespaces = ( tmp[2] || "" ).split( "." ).sort(); + + // Unbind all events (on this namespace, if provided) for the element + if ( !type ) { + for ( type in events ) { + jQuery.event.remove( elem, type + types[ t ], handler, selector, true ); + } + continue; + } + + special = jQuery.event.special[ type ] || {}; + type = ( selector ? special.delegateType : special.bindType ) || type; + handlers = events[ type ] || []; + tmp = tmp[2] && new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ); + + // Remove matching events + origCount = j = handlers.length; + while ( j-- ) { + handleObj = handlers[ j ]; + + if ( ( mappedTypes || origType === handleObj.origType ) && + ( !handler || handler.guid === handleObj.guid ) && + ( !tmp || tmp.test( handleObj.namespace ) ) && + ( !selector || selector === handleObj.selector || selector === "**" && handleObj.selector ) ) { + handlers.splice( j, 1 ); + + if ( handleObj.selector ) { + handlers.delegateCount--; + } + if ( special.remove ) { + special.remove.call( elem, handleObj ); + } + } + } + + // Remove generic event handler if we removed something and no more handlers exist + // (avoids potential for endless recursion during removal of special event handlers) + if ( origCount && !handlers.length ) { + if ( !special.teardown || special.teardown.call( elem, namespaces, elemData.handle ) === false ) { + jQuery.removeEvent( elem, type, elemData.handle ); + } + + delete events[ type ]; + } + } + + // Remove the expando if it's no longer used + if ( jQuery.isEmptyObject( events ) ) { + delete elemData.handle; + data_priv.remove( elem, "events" ); + } + }, + + trigger: function( event, data, elem, onlyHandlers ) { + + var i, cur, tmp, bubbleType, ontype, handle, special, + eventPath = [ elem || document ], + type = hasOwn.call( event, "type" ) ? event.type : event, + namespaces = hasOwn.call( event, "namespace" ) ? event.namespace.split(".") : []; + + cur = tmp = elem = elem || document; + + // Don't do events on text and comment nodes + if ( elem.nodeType === 3 || elem.nodeType === 8 ) { + return; + } + + // focus/blur morphs to focusin/out; ensure we're not firing them right now + if ( rfocusMorph.test( type + jQuery.event.triggered ) ) { + return; + } + + if ( type.indexOf(".") >= 0 ) { + // Namespaced trigger; create a regexp to match event type in handle() + namespaces = type.split("."); + type = namespaces.shift(); + namespaces.sort(); + } + ontype = type.indexOf(":") < 0 && "on" + type; + + // Caller can pass in a jQuery.Event object, Object, or just an event type string + event = event[ jQuery.expando ] ? + event : + new jQuery.Event( type, typeof event === "object" && event ); + + // Trigger bitmask: & 1 for native handlers; & 2 for jQuery (always true) + event.isTrigger = onlyHandlers ? 2 : 3; + event.namespace = namespaces.join("."); + event.namespace_re = event.namespace ? + new RegExp( "(^|\\.)" + namespaces.join("\\.(?:.*\\.|)") + "(\\.|$)" ) : + null; + + // Clean up the event in case it is being reused + event.result = undefined; + if ( !event.target ) { + event.target = elem; + } + + // Clone any incoming data and prepend the event, creating the handler arg list + data = data == null ? + [ event ] : + jQuery.makeArray( data, [ event ] ); + + // Allow special events to draw outside the lines + special = jQuery.event.special[ type ] || {}; + if ( !onlyHandlers && special.trigger && special.trigger.apply( elem, data ) === false ) { + return; + } + + // Determine event propagation path in advance, per W3C events spec (#9951) + // Bubble up to document, then to window; watch for a global ownerDocument var (#9724) + if ( !onlyHandlers && !special.noBubble && !jQuery.isWindow( elem ) ) { + + bubbleType = special.delegateType || type; + if ( !rfocusMorph.test( bubbleType + type ) ) { + cur = cur.parentNode; + } + for ( ; cur; cur = cur.parentNode ) { + eventPath.push( cur ); + tmp = cur; + } + + // Only add window if we got to document (e.g., not plain obj or detached DOM) + if ( tmp === (elem.ownerDocument || document) ) { + eventPath.push( tmp.defaultView || tmp.parentWindow || window ); + } + } + + // Fire handlers on the event path + i = 0; + while ( (cur = eventPath[i++]) && !event.isPropagationStopped() ) { + + event.type = i > 1 ? + bubbleType : + special.bindType || type; + + // jQuery handler + handle = ( data_priv.get( cur, "events" ) || {} )[ event.type ] && data_priv.get( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && handle.apply && jQuery.acceptData( cur ) ) { + event.result = handle.apply( cur, data ); + if ( event.result === false ) { + event.preventDefault(); + } + } + } + event.type = type; + + // If nobody prevented the default action, do it now + if ( !onlyHandlers && !event.isDefaultPrevented() ) { + + if ( (!special._default || special._default.apply( eventPath.pop(), data ) === false) && + jQuery.acceptData( elem ) ) { + + // Call a native DOM method on the target with the same name name as the event. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && jQuery.isFunction( elem[ type ] ) && !jQuery.isWindow( elem ) ) { + + // Don't re-trigger an onFOO event when we call its FOO() method + tmp = elem[ ontype ]; + + if ( tmp ) { + elem[ ontype ] = null; + } + + // Prevent re-triggering of the same event, since we already bubbled it above + jQuery.event.triggered = type; + elem[ type ](); + jQuery.event.triggered = undefined; + + if ( tmp ) { + elem[ ontype ] = tmp; + } + } + } + } + + return event.result; + }, + + dispatch: function( event ) { + + // Make a writable jQuery.Event from the native event object + event = jQuery.event.fix( event ); + + var i, j, ret, matched, handleObj, + handlerQueue = [], + args = slice.call( arguments ), + handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [], + special = jQuery.event.special[ event.type ] || {}; + + // Use the fix-ed jQuery.Event rather than the (read-only) native event + args[0] = event; + event.delegateTarget = this; + + // Call the preDispatch hook for the mapped type, and let it bail if desired + if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { + return; + } + + // Determine handlers + handlerQueue = jQuery.event.handlers.call( this, event, handlers ); + + // Run delegates first; they may want to stop propagation beneath us + i = 0; + while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { + event.currentTarget = matched.elem; + + j = 0; + while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { + + // Triggered event must either 1) have no namespace, or + // 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace). + if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { + + event.handleObj = handleObj; + event.data = handleObj.data; + + ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) + .apply( matched.elem, args ); + + if ( ret !== undefined ) { + if ( (event.result = ret) === false ) { + event.preventDefault(); + event.stopPropagation(); + } + } + } + } + } + + // Call the postDispatch hook for the mapped type + if ( special.postDispatch ) { + special.postDispatch.call( this, event ); + } + + return event.result; + }, + + handlers: function( event, handlers ) { + var i, matches, sel, handleObj, + handlerQueue = [], + delegateCount = handlers.delegateCount, + cur = event.target; + + // Find delegate handlers + // Black-hole SVG instance trees (#13180) + // Avoid non-left-click bubbling in Firefox (#3861) + if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { + + for ( ; cur !== this; cur = cur.parentNode || this ) { + + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.disabled !== true || event.type !== "click" ) { + matches = []; + for ( i = 0; i < delegateCount; i++ ) { + handleObj = handlers[ i ]; + + // Don't conflict with Object.prototype properties (#13203) + sel = handleObj.selector + " "; + + if ( matches[ sel ] === undefined ) { + matches[ sel ] = handleObj.needsContext ? + jQuery( sel, this ).index( cur ) >= 0 : + jQuery.find( sel, this, null, [ cur ] ).length; + } + if ( matches[ sel ] ) { + matches.push( handleObj ); + } + } + if ( matches.length ) { + handlerQueue.push({ elem: cur, handlers: matches }); + } + } + } + } + + // Add the remaining (directly-bound) handlers + if ( delegateCount < handlers.length ) { + handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) }); + } + + return handlerQueue; + }, + + // Includes some event props shared by KeyEvent and MouseEvent + props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "), + + fixHooks: {}, + + keyHooks: { + props: "char charCode key keyCode".split(" "), + filter: function( event, original ) { + + // Add which for key events + if ( event.which == null ) { + event.which = original.charCode != null ? original.charCode : original.keyCode; + } + + return event; + } + }, + + mouseHooks: { + props: "button buttons clientX clientY offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var eventDoc, doc, body, + button = original.button; + + // Calculate pageX/Y if missing and clientX/Y available + if ( event.pageX == null && original.clientX != null ) { + eventDoc = event.target.ownerDocument || document; + doc = eventDoc.documentElement; + body = eventDoc.body; + + event.pageX = original.clientX + ( doc && doc.scrollLeft || body && body.scrollLeft || 0 ) - ( doc && doc.clientLeft || body && body.clientLeft || 0 ); + event.pageY = original.clientY + ( doc && doc.scrollTop || body && body.scrollTop || 0 ) - ( doc && doc.clientTop || body && body.clientTop || 0 ); + } + + // Add which for click: 1 === left; 2 === middle; 3 === right + // Note: button is not normalized, so don't use it + if ( !event.which && button !== undefined ) { + event.which = ( button & 1 ? 1 : ( button & 2 ? 3 : ( button & 4 ? 2 : 0 ) ) ); + } + + return event; + } + }, + + fix: function( event ) { + if ( event[ jQuery.expando ] ) { + return event; + } + + // Create a writable copy of the event object and normalize some properties + var i, prop, copy, + type = event.type, + originalEvent = event, + fixHook = this.fixHooks[ type ]; + + if ( !fixHook ) { + this.fixHooks[ type ] = fixHook = + rmouseEvent.test( type ) ? this.mouseHooks : + rkeyEvent.test( type ) ? this.keyHooks : + {}; + } + copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props; + + event = new jQuery.Event( originalEvent ); + + i = copy.length; + while ( i-- ) { + prop = copy[ i ]; + event[ prop ] = originalEvent[ prop ]; + } + + // Support: Cordova 2.5 (WebKit) (#13255) + // All events should have a target; Cordova deviceready doesn't + if ( !event.target ) { + event.target = document; + } + + // Support: Safari 6.0+, Chrome < 28 + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + special: { + load: { + // Prevent triggered image.load events from bubbling to window.load + noBubble: true + }, + focus: { + // Fire native event if possible so blur/focus sequence is correct + trigger: function() { + if ( this !== safeActiveElement() && this.focus ) { + this.focus(); + return false; + } + }, + delegateType: "focusin" + }, + blur: { + trigger: function() { + if ( this === safeActiveElement() && this.blur ) { + this.blur(); + return false; + } + }, + delegateType: "focusout" + }, + click: { + // For checkbox, fire native event so checked state will be right + trigger: function() { + if ( this.type === "checkbox" && this.click && jQuery.nodeName( this, "input" ) ) { + this.click(); + return false; + } + }, + + // For cross-browser consistency, don't fire native .click() on links + _default: function( event ) { + return jQuery.nodeName( event.target, "a" ); + } + }, + + beforeunload: { + postDispatch: function( event ) { + + // Support: Firefox 20+ + // Firefox doesn't alert if the returnValue field is not set. + if ( event.result !== undefined && event.originalEvent ) { + event.originalEvent.returnValue = event.result; + } + } + } + }, + + simulate: function( type, elem, event, bubble ) { + // Piggyback on a donor event to simulate a different one. + // Fake originalEvent to avoid donor's stopPropagation, but if the + // simulated event prevents default then we do the same on the donor. + var e = jQuery.extend( + new jQuery.Event(), + event, + { + type: type, + isSimulated: true, + originalEvent: {} + } + ); + if ( bubble ) { + jQuery.event.trigger( e, null, elem ); + } else { + jQuery.event.dispatch.call( elem, e ); + } + if ( e.isDefaultPrevented() ) { + event.preventDefault(); + } + } +}; + +jQuery.removeEvent = function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } +}; + +jQuery.Event = function( src, props ) { + // Allow instantiation without the 'new' keyword + if ( !(this instanceof jQuery.Event) ) { + return new jQuery.Event( src, props ); + } + + // Event object + if ( src && src.type ) { + this.originalEvent = src; + this.type = src.type; + + // Events bubbling up the document may have been marked as prevented + // by a handler lower down the tree; reflect the correct value. + this.isDefaultPrevented = src.defaultPrevented || + src.defaultPrevented === undefined && + // Support: Android < 4.0 + src.returnValue === false ? + returnTrue : + returnFalse; + + // Event type + } else { + this.type = src; + } + + // Put explicitly provided properties onto the event object + if ( props ) { + jQuery.extend( this, props ); + } + + // Create a timestamp if incoming event doesn't have one + this.timeStamp = src && src.timeStamp || jQuery.now(); + + // Mark it as fixed + this[ jQuery.expando ] = true; +}; + +// jQuery.Event is based on DOM3 Events as specified by the ECMAScript Language Binding +// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html +jQuery.Event.prototype = { + isDefaultPrevented: returnFalse, + isPropagationStopped: returnFalse, + isImmediatePropagationStopped: returnFalse, + + preventDefault: function() { + var e = this.originalEvent; + + this.isDefaultPrevented = returnTrue; + + if ( e && e.preventDefault ) { + e.preventDefault(); + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + + if ( e && e.stopPropagation ) { + e.stopPropagation(); + } + }, + stopImmediatePropagation: function() { + var e = this.originalEvent; + + this.isImmediatePropagationStopped = returnTrue; + + if ( e && e.stopImmediatePropagation ) { + e.stopImmediatePropagation(); + } + + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +// Support: Chrome 15+ +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout", + pointerenter: "pointerover", + pointerleave: "pointerout" +}, function( orig, fix ) { + jQuery.event.special[ orig ] = { + delegateType: fix, + bindType: fix, + + handle: function( event ) { + var ret, + target = this, + related = event.relatedTarget, + handleObj = event.handleObj; + + // For mousenter/leave call the handler if related is outside the target. + // NB: No relatedTarget if the mouse left/entered the browser window + if ( !related || (related !== target && !jQuery.contains( target, related )) ) { + event.type = handleObj.origType; + ret = handleObj.handler.apply( this, arguments ); + event.type = fix; + } + return ret; + } + }; +}); + +// Create "bubbling" focus and blur events +// Support: Firefox, Chrome, Safari +if ( !support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler on the document while someone wants focusin/focusout + var handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + var doc = this.ownerDocument || this, + attaches = data_priv.access( doc, fix ); + + if ( !attaches ) { + doc.addEventListener( orig, handler, true ); + } + data_priv.access( doc, fix, ( attaches || 0 ) + 1 ); + }, + teardown: function() { + var doc = this.ownerDocument || this, + attaches = data_priv.access( doc, fix ) - 1; + + if ( !attaches ) { + doc.removeEventListener( orig, handler, true ); + data_priv.remove( doc, fix ); + + } else { + data_priv.access( doc, fix, attaches ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var origFn, type; + + // Types can be a map of types/handlers + if ( typeof types === "object" ) { + // ( types-Object, selector, data ) + if ( typeof selector !== "string" ) { + // ( types-Object, data ) + data = data || selector; + selector = undefined; + } + for ( type in types ) { + this.on( type, selector, data, types[ type ], one ); + } + return this; + } + + if ( data == null && fn == null ) { + // ( types, fn ) + fn = selector; + data = selector = undefined; + } else if ( fn == null ) { + if ( typeof selector === "string" ) { + // ( types, selector, fn ) + fn = data; + data = undefined; + } else { + // ( types, data, fn ) + fn = data; + data = selector; + selector = undefined; + } + } + if ( fn === false ) { + fn = returnFalse; + } else if ( !fn ) { + return this; + } + + if ( one === 1 ) { + origFn = fn; + fn = function( event ) { + // Can use an empty set, since event contains the info + jQuery().off( event ); + return origFn.apply( this, arguments ); + }; + // Use same guid so caller can remove using origFn + fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); + } + return this.each( function() { + jQuery.event.add( this, types, fn, data, selector ); + }); + }, + one: function( types, selector, data, fn ) { + return this.on( types, selector, data, fn, 1 ); + }, + off: function( types, selector, fn ) { + var handleObj, type; + if ( types && types.preventDefault && types.handleObj ) { + // ( event ) dispatched jQuery.Event + handleObj = types.handleObj; + jQuery( types.delegateTarget ).off( + handleObj.namespace ? handleObj.origType + "." + handleObj.namespace : handleObj.origType, + handleObj.selector, + handleObj.handler + ); + return this; + } + if ( typeof types === "object" ) { + // ( types-object [, selector] ) + for ( type in types ) { + this.off( type, selector, types[ type ] ); + } + return this; + } + if ( selector === false || typeof selector === "function" ) { + // ( types [, fn] ) + fn = selector; + selector = undefined; + } + if ( fn === false ) { + fn = returnFalse; + } + return this.each(function() { + jQuery.event.remove( this, types, fn, selector ); + }); + }, + + trigger: function( type, data ) { + return this.each(function() { + jQuery.event.trigger( type, data, this ); + }); + }, + triggerHandler: function( type, data ) { + var elem = this[0]; + if ( elem ) { + return jQuery.event.trigger( type, data, elem, true ); + } + } +}); + + +var + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rhtml = /<|&#?\w+;/, + rnoInnerhtml = /<(?:script|style|link)/i, + // checked="checked" or checked + rchecked = /checked\s*(?:[^=]|=\s*.checked.)/i, + rscriptType = /^$|\/(?:java|ecma)script/i, + rscriptTypeMasked = /^true\/(.*)/, + rcleanScript = /^\s*\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + + // Support: IE 9 + option: [ 1, "" ], + + thead: [ 1, "", "
" ], + col: [ 2, "", "
" ], + tr: [ 2, "", "
" ], + td: [ 3, "", "
" ], + + _default: [ 0, "", "" ] + }; + +// Support: IE 9 +wrapMap.optgroup = wrapMap.option; + +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +// Support: 1.x compatibility +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType !== 11 ? content : content.firstChild, "tr" ) ? + + elem.getElementsByTagName("tbody")[0] || + elem.appendChild( elem.ownerDocument.createElement("tbody") ) : + elem; +} + +// Replace/restore the type attribute of script elements for safe DOM manipulation +function disableScript( elem ) { + elem.type = (elem.getAttribute("type") !== null) + "/" + elem.type; + return elem; +} +function restoreScript( elem ) { + var match = rscriptTypeMasked.exec( elem.type ); + + if ( match ) { + elem.type = match[ 1 ]; + } else { + elem.removeAttribute("type"); + } + + return elem; +} + +// Mark scripts as having already been evaluated +function setGlobalEval( elems, refElements ) { + var i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + data_priv.set( + elems[ i ], "globalEval", !refElements || data_priv.get( refElements[ i ], "globalEval" ) + ); + } +} + +function cloneCopyEvent( src, dest ) { + var i, l, type, pdataOld, pdataCur, udataOld, udataCur, events; + + if ( dest.nodeType !== 1 ) { + return; + } + + // 1. Copy private data: events, handlers, etc. + if ( data_priv.hasData( src ) ) { + pdataOld = data_priv.access( src ); + pdataCur = data_priv.set( dest, pdataOld ); + events = pdataOld.events; + + if ( events ) { + delete pdataCur.handle; + pdataCur.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + } + + // 2. Copy user data + if ( data_user.hasData( src ) ) { + udataOld = data_user.access( src ); + udataCur = jQuery.extend( {}, udataOld ); + + data_user.set( dest, udataCur ); + } +} + +function getAll( context, tag ) { + var ret = context.getElementsByTagName ? context.getElementsByTagName( tag || "*" ) : + context.querySelectorAll ? context.querySelectorAll( tag || "*" ) : + []; + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], ret ) : + ret; +} + +// Support: IE >= 9 +function fixInput( src, dest ) { + var nodeName = dest.nodeName.toLowerCase(); + + // Fails to persist the checked state of a cloned checkbox or radio button. + if ( nodeName === "input" && rcheckableType.test( src.type ) ) { + dest.checked = src.checked; + + // Fails to return the selected option to the default selected state when cloning options + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var i, l, srcElements, destElements, + clone = elem.cloneNode( true ), + inPage = jQuery.contains( elem.ownerDocument, elem ); + + // Support: IE >= 9 + // Fix Cloning issues + if ( !support.noCloneChecked && ( elem.nodeType === 1 || elem.nodeType === 11 ) && + !jQuery.isXMLDoc( elem ) ) { + + // We eschew Sizzle here for performance reasons: http://jsperf.com/getall-vs-sizzle/2 + destElements = getAll( clone ); + srcElements = getAll( elem ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + fixInput( srcElements[ i ], destElements[ i ] ); + } + } + + // Copy the events from the original to the clone + if ( dataAndEvents ) { + if ( deepDataAndEvents ) { + srcElements = srcElements || getAll( elem ); + destElements = destElements || getAll( clone ); + + for ( i = 0, l = srcElements.length; i < l; i++ ) { + cloneCopyEvent( srcElements[ i ], destElements[ i ] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + // Return the cloned set + return clone; + }, + + buildFragment: function( elems, context, scripts, selection ) { + var elem, tmp, tag, wrap, contains, j, + fragment = context.createDocumentFragment(), + nodes = [], + i = 0, + l = elems.length; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge( nodes, elem.nodeType ? [ elem ] : elem ); + + // Convert non-html into a text node + } else if ( !rhtml.test( elem ) ) { + nodes.push( context.createTextNode( elem ) ); + + // Convert html into DOM nodes + } else { + tmp = tmp || fragment.appendChild( context.createElement("div") ); + + // Deserialize a standard representation + tag = ( rtagName.exec( elem ) || [ "", "" ] )[ 1 ].toLowerCase(); + wrap = wrapMap[ tag ] || wrapMap._default; + tmp.innerHTML = wrap[ 1 ] + elem.replace( rxhtmlTag, "<$1>" ) + wrap[ 2 ]; + + // Descend through wrappers to the right content + j = wrap[ 0 ]; + while ( j-- ) { + tmp = tmp.lastChild; + } + + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge( nodes, tmp.childNodes ); + + // Remember the top-level container + tmp = fragment.firstChild; + + // Fixes #12346 + // Support: Webkit, IE + tmp.textContent = ""; + } + } + } + + // Remove wrapper from fragment + fragment.textContent = ""; + + i = 0; + while ( (elem = nodes[ i++ ]) ) { + + // #4087 - If origin and destination elements are the same, and this is + // that element, do not do anything + if ( selection && jQuery.inArray( elem, selection ) !== -1 ) { + continue; + } + + contains = jQuery.contains( elem.ownerDocument, elem ); + + // Append to fragment + tmp = getAll( fragment.appendChild( elem ), "script" ); + + // Preserve script evaluation history + if ( contains ) { + setGlobalEval( tmp ); + } + + // Capture executables + if ( scripts ) { + j = 0; + while ( (elem = tmp[ j++ ]) ) { + if ( rscriptType.test( elem.type || "" ) ) { + scripts.push( elem ); + } + } + } + } + + return fragment; + }, + + cleanData: function( elems ) { + var data, elem, type, key, + special = jQuery.event.special, + i = 0; + + for ( ; (elem = elems[ i ]) !== undefined; i++ ) { + if ( jQuery.acceptData( elem ) ) { + key = elem[ data_priv.expando ]; + + if ( key && (data = data_priv.cache[ key ]) ) { + if ( data.events ) { + for ( type in data.events ) { + if ( special[ type ] ) { + jQuery.event.remove( elem, type ); + + // This is a shortcut to avoid jQuery.event.remove's overhead + } else { + jQuery.removeEvent( elem, type, data.handle ); + } + } + } + if ( data_priv.cache[ key ] ) { + // Discard any remaining `private` data + delete data_priv.cache[ key ]; + } + } + } + // Discard any remaining `user` data + delete data_user.cache[ elem[ data_user.expando ] ]; + } + } +}); + +jQuery.fn.extend({ + text: function( value ) { + return access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().each(function() { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + this.textContent = value; + } + }); + }, null, value, arguments.length ); + }, + + append: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.appendChild( elem ); + } + }); + }, + + prepend: function() { + return this.domManip( arguments, function( elem ) { + if ( this.nodeType === 1 || this.nodeType === 11 || this.nodeType === 9 ) { + var target = manipulationTarget( this, elem ); + target.insertBefore( elem, target.firstChild ); + } + }); + }, + + before: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this ); + } + }); + }, + + after: function() { + return this.domManip( arguments, function( elem ) { + if ( this.parentNode ) { + this.parentNode.insertBefore( elem, this.nextSibling ); + } + }); + }, + + remove: function( selector, keepData /* Internal Use Only */ ) { + var elem, + elems = selector ? jQuery.filter( selector, this ) : this, + i = 0; + + for ( ; (elem = elems[i]) != null; i++ ) { + if ( !keepData && elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem ) ); + } + + if ( elem.parentNode ) { + if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) { + setGlobalEval( getAll( elem, "script" ) ); + } + elem.parentNode.removeChild( elem ); + } + } + + return this; + }, + + empty: function() { + var elem, + i = 0; + + for ( ; (elem = this[i]) != null; i++ ) { + if ( elem.nodeType === 1 ) { + + // Prevent memory leaks + jQuery.cleanData( getAll( elem, false ) ); + + // Remove any remaining nodes + elem.textContent = ""; + } + } + + return this; + }, + + clone: function( dataAndEvents, deepDataAndEvents ) { + dataAndEvents = dataAndEvents == null ? false : dataAndEvents; + deepDataAndEvents = deepDataAndEvents == null ? dataAndEvents : deepDataAndEvents; + + return this.map(function() { + return jQuery.clone( this, dataAndEvents, deepDataAndEvents ); + }); + }, + + html: function( value ) { + return access( this, function( value ) { + var elem = this[ 0 ] || {}, + i = 0, + l = this.length; + + if ( value === undefined && elem.nodeType === 1 ) { + return elem.innerHTML; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + !wrapMap[ ( rtagName.exec( value ) || [ "", "" ] )[ 1 ].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for ( ; i < l; i++ ) { + elem = this[ i ] || {}; + + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + elem.innerHTML = value; + } + } + + elem = 0; + + // If using innerHTML throws an exception, use the fallback method + } catch( e ) {} + } + + if ( elem ) { + this.empty().append( value ); + } + }, null, value, arguments.length ); + }, + + replaceWith: function() { + var arg = arguments[ 0 ]; + + // Make the changes, replacing each context element with the new content + this.domManip( arguments, function( elem ) { + arg = this.parentNode; + + jQuery.cleanData( getAll( this ) ); + + if ( arg ) { + arg.replaceChild( elem, this ); + } + }); + + // Force removal if there was no new content (e.g., from empty arguments) + return arg && (arg.length || arg.nodeType) ? this : this.remove(); + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, callback ) { + + // Flatten any nested arrays + args = concat.apply( [], args ); + + var fragment, first, scripts, hasScripts, node, doc, + i = 0, + l = this.length, + set = this, + iNoClone = l - 1, + value = args[ 0 ], + isFunction = jQuery.isFunction( value ); + + // We can't cloneNode fragments that contain checked, in WebKit + if ( isFunction || + ( l > 1 && typeof value === "string" && + !support.checkClone && rchecked.test( value ) ) ) { + return this.each(function( index ) { + var self = set.eq( index ); + if ( isFunction ) { + args[ 0 ] = value.call( this, index, self.html() ); + } + self.domManip( args, callback ); + }); + } + + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, this ); + first = fragment.firstChild; + + if ( fragment.childNodes.length === 1 ) { + fragment = first; + } + + if ( first ) { + scripts = jQuery.map( getAll( fragment, "script" ), disableScript ); + hasScripts = scripts.length; + + // Use the original fragment for the last item instead of the first because it can end up + // being emptied incorrectly in certain situations (#8070). + for ( ; i < l; i++ ) { + node = fragment; + + if ( i !== iNoClone ) { + node = jQuery.clone( node, true, true ); + + // Keep references to cloned scripts for later restoration + if ( hasScripts ) { + // Support: QtWebKit + // jQuery.merge because push.apply(_, arraylike) throws + jQuery.merge( scripts, getAll( node, "script" ) ); + } + } + + callback.call( this[ i ], node, i ); + } + + if ( hasScripts ) { + doc = scripts[ scripts.length - 1 ].ownerDocument; + + // Reenable scripts + jQuery.map( scripts, restoreScript ); + + // Evaluate executable scripts on first document insertion + for ( i = 0; i < hasScripts; i++ ) { + node = scripts[ i ]; + if ( rscriptType.test( node.type || "" ) && + !data_priv.access( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Optional AJAX dependency, but won't run scripts if not present + if ( jQuery._evalUrl ) { + jQuery._evalUrl( node.src ); + } + } else { + jQuery.globalEval( node.textContent.replace( rcleanScript, "" ) ); + } + } + } + } + } + } + + return this; + } +}); + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1, + i = 0; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone( true ); + jQuery( insert[ i ] )[ original ]( elems ); + + // Support: QtWebKit + // .get() because push.apply(_, arraylike) throws + push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +}); + + +var iframe, + elemdisplay = {}; + +/** + * Retrieve the actual display of a element + * @param {String} name nodeName of the element + * @param {Object} doc Document object + */ +// Called only from within defaultDisplay +function actualDisplay( name, doc ) { + var style, + elem = jQuery( doc.createElement( name ) ).appendTo( doc.body ), + + // getDefaultComputedStyle might be reliably used only on attached element + display = window.getDefaultComputedStyle && ( style = window.getDefaultComputedStyle( elem[ 0 ] ) ) ? + + // Use of this method is a temporary fix (more like optmization) until something better comes along, + // since it was removed from specification and supported only in FF + style.display : jQuery.css( elem[ 0 ], "display" ); + + // We don't have any data stored on the element, + // so use "detach" method as fast way to get rid of the element + elem.detach(); + + return display; +} + +/** + * Try to determine the default display value of an element + * @param {String} nodeName + */ +function defaultDisplay( nodeName ) { + var doc = document, + display = elemdisplay[ nodeName ]; + + if ( !display ) { + display = actualDisplay( nodeName, doc ); + + // If the simple way fails, read from inside an iframe + if ( display === "none" || !display ) { + + // Use the already-created iframe if possible + iframe = (iframe || jQuery( "