diff --git a/CHANGELOG.md b/CHANGELOG.md index ce3f5fe9274..ae0f2ade275 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,18 @@ +CHANGELOG for 1.0.0-alpha5 +=================== +This changelog references the relevant changes (new features, changes and bugs) done in 1.0.0-alpha5 versions. + +* 1.0.0-alpha5 (2013-08-29) + * Custom entity creation + * Cron Job + * Record ownership + * Grid Improvements + * Filter Improvements + * Email Template Improvements + * Implemented extractor for messages in PHP code + * Removed dependency on SonataAdminBundle + * Added possibility to unpin page using pin icon + CHANGELOG for 1.0.0-alpha4 =================== This changelog references the relevant changes (new features, changes and bugs) done in 1.0.0-alpha4 versions. diff --git a/UPGRADE.md b/UPGRADE.md index cdcd65fff46..4585f409416 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -1,7 +1,7 @@ -UPGRADE FROM 1.0.0-alpha2 to 1.0.0-alpha3 +UPGRADE FROM 1.0.0-alpha versions ======================= ### General - * Upgrade to 1.0.0-alpha3 is not supported and full reinstall is required + * Upgrade to any 1.0.0-alpha is not supported and full reinstall is required \ No newline at end of file diff --git a/composer.json b/composer.json index 374e289b288..8d77e0a3f72 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,7 @@ "doctrine/doctrine-fixtures-bundle": "@dev", "twig/extensions": "1.0.*@dev", "symfony/assetic-bundle": "2.3.*@dev", - "symfony/swiftmailer-bundle": "2.3.2", + "symfony/swiftmailer-bundle": "2.3.*", "symfony/monolog-bundle": "2.3.*", "sensio/distribution-bundle": "2.3.*", "sensio/framework-extra-bundle": "2.3.*", @@ -36,23 +36,17 @@ "ddeboer/data-import": "dev-master", "stof/doctrine-extensions-bundle": "dev-master", "escapestudios/wsse-authentication-bundle": "2.1.x-dev", - "sonata-project/admin-bundle": "dev-master", - "sonata-project/doctrine-orm-admin-bundle": "dev-master", "liip/imagine-bundle": "dev-master", "leafo/lessphp": "dev-master", "willdurand/expose-translation-bundle": "0.2.*@dev", "apy/jsfv-bundle": "dev-master", "genemu/form-bundle": "2.2.*", - "a2lix/translation-form-bundle" : "1.0@dev" + "a2lix/translation-form-bundle" : "1.*@dev" }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/orocrm/SonataAdminBundle.git" - } - ], "autoload": { - "psr-0": { "Oro\\Bundle": "src/" } + "psr-0": { + "Oro\\Bundle": "src/" + } }, "minimum-stability": "dev", "extra": { diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2f9ce8dca63..5203d46286e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -11,10 +11,11 @@ processIsolation = "false" stopOnFailure = "false" syntaxCheck = "false" - bootstrap = "tests/bootstrap.php"> + bootstrap = "tests/bootstrap.php" +> - src/*/Bundle/*Bundle/Tests/Unit + src/Oro/Bundle/*Bundle/Tests/Unit diff --git a/src/Oro/Bundle/AddressBundle/AttributeType/AddressType.php b/src/Oro/Bundle/AddressBundle/AttributeType/AddressType.php deleted file mode 100644 index 6df0ed20650..00000000000 --- a/src/Oro/Bundle/AddressBundle/AttributeType/AddressType.php +++ /dev/null @@ -1,15 +0,0 @@ -getDoctrine()->getRepository('OroAddressBundle:AddressType')->findAll(); return $this->handleView( diff --git a/src/Oro/Bundle/AddressBundle/Controller/Api/Soap/AddressController.php b/src/Oro/Bundle/AddressBundle/Controller/Api/Soap/AddressController.php index 4ff950b44fc..e958b7a2f52 100644 --- a/src/Oro/Bundle/AddressBundle/Controller/Api/Soap/AddressController.php +++ b/src/Oro/Bundle/AddressBundle/Controller/Api/Soap/AddressController.php @@ -5,12 +5,12 @@ use Symfony\Component\Form\FormInterface; use BeSimple\SoapBundle\ServiceDefinition\Annotation as Soap; -use Oro\Bundle\SoapBundle\Controller\Api\Soap\FlexibleSoapController; -use Oro\Bundle\SoapBundle\Entity\Manager\ApiFlexibleEntityManager; +use Oro\Bundle\SoapBundle\Controller\Api\Soap\SoapController; +use Oro\Bundle\SoapBundle\Entity\Manager\ApiEntityManager; use Oro\Bundle\SoapBundle\Form\Handler\ApiFormHandler; use Oro\Bundle\UserBundle\Annotation\AclAncestor; -class AddressController extends FlexibleSoapController +class AddressController extends SoapController { /** * @Soap\Method("getAddresses") @@ -37,8 +37,8 @@ public function getAction($id) /** * @Soap\Method("createAddress") - * @Soap\Param("address", phpType = "Oro\Bundle\AddressBundle\Entity\AddressSoap") - * @Soap\Result(phpType = "boolean") + * @Soap\Param("address", phpType = "Oro\Bundle\AddressBundle\Entity\Address") + * @Soap\Result(phpType = "int") * @AclAncestor("oro_address_create") */ public function createAction($address) @@ -49,7 +49,7 @@ public function createAction($address) /** * @Soap\Method("updateAddress") * @Soap\Param("id", phpType = "int") - * @Soap\Param("address", phpType = "Oro\Bundle\AddressBundle\Entity\AddressSoap") + * @Soap\Param("address", phpType = "Oro\Bundle\AddressBundle\Entity\Address") * @Soap\Result(phpType = "boolean") * @AclAncestor("oro_address_edit") */ @@ -70,7 +70,7 @@ public function deleteAction($id) } /** - * @return ApiFlexibleEntityManager + * @return ApiEntityManager */ public function getManager() { diff --git a/src/Oro/Bundle/AddressBundle/Entity/AbstractAddress.php b/src/Oro/Bundle/AddressBundle/Entity/AbstractAddress.php index 89b4e2d6898..12f0a717a27 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/AbstractAddress.php +++ b/src/Oro/Bundle/AddressBundle/Entity/AbstractAddress.php @@ -3,19 +3,20 @@ namespace Oro\Bundle\AddressBundle\Entity; use Doctrine\ORM\Mapping as ORM; -use JMS\Serializer\Annotation\Type; + use BeSimple\SoapBundle\ServiceDefinition\Annotation as Soap; -use Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityFlexible; -use Oro\Bundle\FlexibleEntityBundle\Model\FlexibleValueInterface; use Symfony\Component\Validator\ExecutionContext; /** * Address * * @ORM\MappedSuperclass + * @ORM\HasLifecycleCallbacks + * + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) */ -abstract class AbstractAddress extends AbstractEntityFlexible +abstract class AbstractAddress implements EmptyItem { /** * @var integer @@ -109,6 +110,20 @@ abstract class AbstractAddress extends AbstractEntityFlexible */ protected $lastName; + /** + * @var \DateTime $created + * + * @ORM\Column(type="datetime") + */ + protected $created; + + /** + * @var \DateTime $updated + * + * @ORM\Column(type="datetime") + */ + protected $updated; + /** * Get id * @@ -119,6 +134,16 @@ public function getId() return $this->id; } + /** + * Set id + * + * @param int $id + */ + public function setId($id) + { + $this->id = $id; + } + /** * Set label * @@ -145,7 +170,7 @@ public function getLabel() /** * Set street * - * @param string $street + * @param string $street * @return AbstractAddress */ public function setStreet($street) @@ -168,7 +193,7 @@ public function getStreet() /** * Set street2 * - * @param string $street2 + * @param string $street2 * @return AbstractAddress */ public function setStreet2($street2) @@ -191,7 +216,7 @@ public function getStreet2() /** * Set city * - * @param string $city + * @param string $city * @return AbstractAddress */ public function setCity($city) @@ -274,7 +299,7 @@ public function getUniversalState() /** * Set postal_code * - * @param string $postalCode + * @param string $postalCode * @return AbstractAddress */ public function setPostalCode($postalCode) @@ -297,7 +322,7 @@ public function getPostalCode() /** * Set country * - * @param Country $country + * @param Country $country * @return AbstractAddress */ public function setCountry($country) @@ -374,6 +399,16 @@ public function getCreatedAt() return $this->created; } + /** + * Set address created date/time + * + * @param \DateTime $created + */ + public function setCreatedAt(\DateTime $created) + { + $this->created = $created; + } + /** * Get address last update date/time * @@ -384,6 +419,16 @@ public function getUpdatedAt() return $this->updated; } + /** + * Set address updated date/time + * + * @param \DateTime $updated + */ + public function setUpdatedAt(\DateTime $updated) + { + $this->updated = $updated; + } + /** * Pre persist event listener * @@ -440,7 +485,7 @@ public function __toString() */ public function isEmpty() { - $isEmpty = empty($this->label) + return empty($this->label) && empty($this->firstName) && empty($this->lastName) && empty($this->street) @@ -450,11 +495,29 @@ public function isEmpty() && empty($this->stateText) && empty($this->country) && empty($this->postalCode); - /** @var FlexibleValueInterface $value */ - foreach ($this->values as $value) { - $flexibleValue = $value->getData(); - $isEmpty = $isEmpty && empty($flexibleValue); + } + + /** + * @param mixed $other + * @return bool + */ + public function isEqual($other) + { + $class = get_class($this); + + if (!$other instanceof $class) { + return false; } - return $isEmpty; + + /** @var AbstractAddress $other */ + if ($this->getId() && $other->getId()) { + return $this->getId() == $other->getId(); + } + + if ($this->getId() || $other->getId()) { + return false; + } + + return $this === $other; } } diff --git a/src/Oro/Bundle/AddressBundle/Entity/AbstractEmail.php b/src/Oro/Bundle/AddressBundle/Entity/AbstractEmail.php new file mode 100644 index 00000000000..15d3663f3a1 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Entity/AbstractEmail.php @@ -0,0 +1,128 @@ +email = $email; + $this->primary = false; + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Set id + * + * @param int $id + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * Set email + * + * @param string $email + * @return AbstractEmail + */ + public function setEmail($email) + { + $this->email = $email; + + return $this; + } + + /** + * Get email + * + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * @param bool $primary + * @return AbstractEmail + */ + public function setPrimary($primary) + { + $this->primary = (bool)$primary; + + return $this; + } + + /** + * @return bool + */ + public function isPrimary() + { + return $this->primary; + } + + /** + * @return string + */ + public function __toString() + { + return (string)$this->getEmail(); + } + + /** + * Check if entity is empty. + * + * @return bool + */ + public function isEmpty() + { + return empty($this->email); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Entity/AbstractPhone.php b/src/Oro/Bundle/AddressBundle/Entity/AbstractPhone.php new file mode 100644 index 00000000000..04e6787f127 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Entity/AbstractPhone.php @@ -0,0 +1,128 @@ +phone = $phone; + $this->primary = false; + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Set id + * + * @param int $id + */ + public function setId($id) + { + $this->id = $id; + } + + /** + * Set phone + * + * @param string $phone + * @return AbstractPhone + */ + public function setPhone($phone) + { + $this->phone = $phone; + + return $this; + } + + /** + * Get phone + * + * @return string + */ + public function getPhone() + { + return $this->phone; + } + + /** + * @param bool $primary + * @return AbstractPhone + */ + public function setPrimary($primary) + { + $this->primary = (bool)$primary; + + return $this; + } + + /** + * @return bool + */ + public function isPrimary() + { + return $this->primary; + } + + /** + * @return string + */ + public function __toString() + { + return (string)$this->getPhone(); + } + + /** + * Check if entity is empty. + * + * @return bool + */ + public function isEmpty() + { + return empty($this->phone); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Entity/AbstractTypedAddress.php b/src/Oro/Bundle/AddressBundle/Entity/AbstractTypedAddress.php index 925ba3d8da3..7068f305bb1 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/AbstractTypedAddress.php +++ b/src/Oro/Bundle/AddressBundle/Entity/AbstractTypedAddress.php @@ -15,12 +15,13 @@ * * @ORM\MappedSuperclass */ -abstract class AbstractTypedAddress extends AbstractAddress +abstract class AbstractTypedAddress extends AbstractAddress implements PrimaryItem { /** * Many-to-many relation field, relation parameters must be in specific class * * @var Collection + * * @Soap\ComplexType("Oro\Bundle\AddressBundle\Entity\AddressType[]", nillable=true) */ protected $types; @@ -35,9 +36,8 @@ abstract class AbstractTypedAddress extends AbstractAddress public function __construct() { - parent::__construct(); - $this->types = new ArrayCollection(); + $this->primary = false; } /** @@ -138,7 +138,7 @@ public function removeType(AddressType $type) */ public function setPrimary($primary) { - $this->primary = $primary; + $this->primary = (bool)$primary; return $this; } @@ -148,7 +148,7 @@ public function setPrimary($primary) */ public function isPrimary() { - return $this->primary; + return (bool)$this->primary; } /** @@ -158,6 +158,6 @@ public function isEmpty() { return parent::isEmpty() && $this->types->isEmpty() - && empty($this->primary); + && !$this->primary; } } diff --git a/src/Oro/Bundle/AddressBundle/Entity/Address.php b/src/Oro/Bundle/AddressBundle/Entity/Address.php index cb87b3b2246..af4c8dbfd49 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/Address.php +++ b/src/Oro/Bundle/AddressBundle/Entity/Address.php @@ -3,27 +3,13 @@ namespace Oro\Bundle\AddressBundle\Entity; use Doctrine\ORM\Mapping as ORM; -use JMS\Serializer\Annotation\Exclude; /** * Address * * @ORM\Table("oro_address") - * @ORM\HasLifecycleCallbacks() - * @ORM\Entity(repositoryClass="Oro\Bundle\AddressBundle\Entity\Repository\AddressRepository") + * @ORM\Entity */ class Address extends AbstractAddress { - /** - * This inheritance needed to add possibility to store address in separate table - * http://docs.doctrine-project.org/en/latest/reference/inheritance-mapping.html - */ - - /** - * @var \Oro\Bundle\FlexibleEntityBundle\Model\AbstractFlexibleValue[] - * - * @ORM\OneToMany(targetEntity="Oro\Bundle\AddressBundle\Entity\Value\AddressValue", mappedBy="entity", cascade={"persist", "remove"}, orphanRemoval=true) - * @Exclude - */ - protected $values; } diff --git a/src/Oro/Bundle/AddressBundle/Entity/AddressSoap.php b/src/Oro/Bundle/AddressBundle/Entity/AddressSoap.php deleted file mode 100644 index a8a9c8d1ac7..00000000000 --- a/src/Oro/Bundle/AddressBundle/Entity/AddressSoap.php +++ /dev/null @@ -1,19 +0,0 @@ -label; + return (string)$this->label; } } diff --git a/src/Oro/Bundle/AddressBundle/Entity/Country.php b/src/Oro/Bundle/AddressBundle/Entity/Country.php index 06a98881901..382c3d2de6a 100644 --- a/src/Oro/Bundle/AddressBundle/Entity/Country.php +++ b/src/Oro/Bundle/AddressBundle/Entity/Country.php @@ -2,8 +2,6 @@ namespace Oro\Bundle\AddressBundle\Entity; -use JMS\Serializer\Annotation\Exclude; - use Doctrine\ORM\Mapping as ORM; use Doctrine\Common\Collections\ArrayCollection; use Gedmo\Mapping\Annotation as Gedmo; @@ -57,7 +55,6 @@ class Country implements Translatable * cascade={"ALL"}, * fetch="EXTRA_LAZY" * ) - * @Exclude */ protected $regions; diff --git a/src/Oro/Bundle/AddressBundle/Entity/EmptyItem.php b/src/Oro/Bundle/AddressBundle/Entity/EmptyItem.php new file mode 100644 index 00000000000..59b328db3e6 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Entity/EmptyItem.php @@ -0,0 +1,11 @@ +getClassMetadata($class); $this->class = $metadata->getName(); $this->om = $om; - $this->fm = $fm; } /** * Returns an empty address instance * - * @return \Oro\Bundle\AddressBundle\Entity\AbstractAddress + * @return AbstractAddress */ public function createAddress() { @@ -64,8 +48,8 @@ public function createAddress() /** * Updates an address * - * @param AbstractAddress $address - * @param bool $flush Whether to flush the changes (default true) + * @param AbstractAddress $address + * @param bool $flush Whether to flush the changes (default true) * @throws \RuntimeException */ public function updateAddress(AbstractAddress $address, $flush = true) @@ -90,7 +74,7 @@ public function deleteAddress(AbstractAddress $address) /** * Finds one address by the given criteria * - * @param array $criteria + * @param array $criteria * @return AbstractAddress */ public function findAddressBy(array $criteria) @@ -121,7 +105,7 @@ public function getClass() /** * Return related repository * - * @return \Doctrine\Common\Persistence\ObjectRepository + * @return ObjectRepository */ public function getRepository() { @@ -131,42 +115,10 @@ public function getRepository() /** * Retrieve object manager * - * @return \Doctrine\Common\Persistence\ObjectManager + * @return ObjectManager */ public function getStorageManager() { return $this->om; } - - /** - * Returns basic query instance to get collection with all user instances - * - * @param int $limit - * @param int $offset - * @return Paginator - */ - public function getListQuery($limit = 10, $offset = 1) - { - /** @var FlexibleEntityRepository $repository */ - $repository = $this->fm->getFlexibleRepository(); - - return $repository->findByWithAttributesQB(array(), null, array('id' => 'ASC'), $limit, $offset); - } - - /** - * Provide proxy method calls to flexible manager - * - * @param string $name - * @param array $args - * @return mixed - * @throws \RuntimeException - */ - public function __call($name, $args) - { - if (method_exists($this->fm, $name)) { - return call_user_func_array(array($this->fm, $name), $args); - } - - throw new \RuntimeException(sprintf('Unknown method "%s"', $name)); - } } diff --git a/src/Oro/Bundle/AddressBundle/Entity/PrimaryItem.php b/src/Oro/Bundle/AddressBundle/Entity/PrimaryItem.php new file mode 100644 index 00000000000..863f9030bd8 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Entity/PrimaryItem.php @@ -0,0 +1,20 @@ +property = $property; - $this->propertyPath = new PropertyPath($this->property); - $this->propertyAccessor = PropertyAccess::getPropertyAccessor(); - $this->entityClass = $entityClass; - } - - /** - * {@inheritdoc} - */ - public static function getSubscribedEvents() - { - return array( - FormEvents::POST_BIND => 'postBind', - FormEvents::PRE_SET_DATA => 'preSet', - FormEvents::PRE_BIND => 'preBind' - ); - } - - /** - * Pre set empty collection elements. - * - * @param FormEvent $event - */ - public function preSet(FormEvent $event) - { - $data = $event->getData(); - - if (!$data) { - return; - } - - /** @var Collection $addresses */ - $addresses = $this->propertyAccessor->getValue($data, $this->propertyPath); - - if ($addresses->isEmpty()) { - - $this->propertyAccessor->setValue( - $data, - $this->propertyPath, - array(new $this->entityClass()) - ); - } - } - - /** - * Removes empty collection elements. - * - * @param FormEvent $event - */ - public function postBind(FormEvent $event) - { - $data = $event->getData(); - - if (!$data) { - return; - } - - /** @var Collection $addresses */ - $addresses = $this->propertyAccessor->getValue($data, $this->propertyPath); - $notEmptyAddresses = $addresses->filter( - function (AbstractTypedAddress $address) { - return !$address->isEmpty(); - } - ); - - $this->propertyAccessor->setValue($data, $this->propertyPath, $notEmptyAddresses); - } - - /** - * Remove empty addresses to prevent validation. - * - * @param FormEvent $event - */ - public function preBind(FormEvent $event) - { - $data = $event->getData(); - - if (!$data) { - return; - } - - $addresses = array(); - $hasPrimary = false; - if ($data && array_key_exists($this->property, $data)) { - foreach ($data[$this->property] as $addressRow) { - if (!$this->isArrayEmpty($addressRow)) { - $hasPrimary = $hasPrimary || (array_key_exists('primary', $addressRow) && $addressRow['primary']); - $addresses[] = $addressRow; - } - } - } - - // Set first non empty address for new item as primary - if ($addresses) { - if ((!array_key_exists('id', $data) || !$data['id']) && !$hasPrimary) { - $first = array_shift($addresses); - $first['primary'] = true; - array_unshift($addresses, $first); - } - $data[$this->property] = $addresses; - $event->setData($data); - } - } - - /** - * Check if array is empty - * - * @param array $array - * @return bool - */ - protected function isArrayEmpty($array) - { - foreach ($array as $val) { - if (is_array($val)) { - if (!$this->isArrayEmpty($val)) { - return false; - } - } elseif (!empty($val)) { - return false; - } - } - return true; - } -} diff --git a/src/Oro/Bundle/AddressBundle/Form/EventListener/BuildAddressFormListener.php b/src/Oro/Bundle/AddressBundle/Form/EventListener/AddressCountryAndRegionSubscriber.php similarity index 94% rename from src/Oro/Bundle/AddressBundle/Form/EventListener/BuildAddressFormListener.php rename to src/Oro/Bundle/AddressBundle/Form/EventListener/AddressCountryAndRegionSubscriber.php index 939bb77fe91..e3740c0155c 100644 --- a/src/Oro/Bundle/AddressBundle/Form/EventListener/BuildAddressFormListener.php +++ b/src/Oro/Bundle/AddressBundle/Form/EventListener/AddressCountryAndRegionSubscriber.php @@ -5,13 +5,12 @@ use Symfony\Component\Form\FormEvents; use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\EventDispatcher\EventSubscriberInterface; -use Doctrine\ORM\EntityRepository; use Doctrine\Common\Persistence\ObjectManager; use Oro\Bundle\AddressBundle\Entity\Country; use Oro\Bundle\AddressBundle\Entity\Repository\RegionRepository; -class BuildAddressFormListener implements EventSubscriberInterface +class AddressCountryAndRegionSubscriber implements EventSubscriberInterface { private $om; @@ -41,7 +40,7 @@ public static function getSubscribedEvents() { return array( FormEvents::PRE_SET_DATA => 'preSetData', - FormEvents::PRE_BIND => 'preBind' + FormEvents::PRE_SUBMIT => 'preSubmit' ); } @@ -98,7 +97,7 @@ public function preSetData(FormEvent $event) * * @param FormEvent $event */ - public function preBind(FormEvent $event) + public function preSubmit(FormEvent $event) { $data = $event->getData(); $form = $event->getForm(); diff --git a/src/Oro/Bundle/AddressBundle/Form/EventListener/FixAddressesPrimaryAndTypesSubscriber.php b/src/Oro/Bundle/AddressBundle/Form/EventListener/FixAddressesPrimaryAndTypesSubscriber.php new file mode 100644 index 00000000000..652e90d60f6 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Form/EventListener/FixAddressesPrimaryAndTypesSubscriber.php @@ -0,0 +1,101 @@ +getOwner()->getAddresses()) + * + * @var string + */ + protected $addressesProperty; + + /** + * @var PropertyAccess + */ + protected $addressAccess; + + public function __construct($addressesProperty) + { + $this->addressesAccess = PropertyAccess::createPropertyAccessor(); + $this->addressesProperty = $addressesProperty; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + FormEvents::POST_SUBMIT => 'postSubmit' + ); + } + + /** + * Removes empty collection elements. + * + * @param FormEvent $event + */ + public function postSubmit(FormEvent $event) + { + /** @var AbstractTypedAddress $address */ + $address = $event->getData(); + + /** @var AbstractTypedAddress[] $allAddresses */ + $allAddresses = $this->addressesAccess->getValue($address, $this->addressesProperty); + + $this->handlePrimary($address, $allAddresses); + $this->handleType($address, $allAddresses); + } + + /** + * Only one address must be primary. + * + * @param AbstractTypedAddress $address + * @param AbstractTypedAddress[] $allAddresses + */ + protected function handlePrimary(AbstractTypedAddress $address, $allAddresses) + { + if ($address->isPrimary()) { + /** @var AbstractTypedAddress[] $allAddresses */ + foreach ($allAddresses as $otherAddresses) { + $otherAddresses->setPrimary(false); + } + $address->setPrimary(true); + } elseif (count($allAddresses) == 1) { + $address->setPrimary(true); + } + } + + /** + * Two addresses must not have same types + * + * @param AbstractTypedAddress $address + * @param AbstractTypedAddress[] $allAddresses + */ + protected function handleType(AbstractTypedAddress $address, $allAddresses) + { + $types = $address->getTypes()->toArray(); + if (count($types)) { + foreach ($allAddresses as $otherAddresses) { + foreach ($types as $type) { + $otherAddresses->removeType($type); + } + } + foreach ($types as $type) { + $address->addType($type); + } + } + } +} diff --git a/src/Oro/Bundle/AddressBundle/Form/EventListener/ItemCollectionTypeSubscriber.php b/src/Oro/Bundle/AddressBundle/Form/EventListener/ItemCollectionTypeSubscriber.php new file mode 100644 index 00000000000..1a93304ceb6 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Form/EventListener/ItemCollectionTypeSubscriber.php @@ -0,0 +1,117 @@ + 'postSubmit', + FormEvents::PRE_SUBMIT => 'preSubmit' + ); + } + + /** + * Removes empty collection elements. + * + * @param FormEvent $event + */ + public function postSubmit(FormEvent $event) + { + /** @var Collection $items */ + $items = $event->getData(); + + if (!$items || !$items instanceof Collection) { + return; + } + + foreach ($items as $item) { + if ($item instanceof EmptyItem && $item->isEmpty()) { + $items->removeElement($item); + } + } + } + + /** + * Remove empty addresses to prevent validation. + * + * @param FormEvent $event + */ + public function preSubmit(FormEvent $event) + { + $items = $event->getData(); + + if (!$items || !is_array($items)) { + return; + } + + $notEmptyItems = array(); + $hasPrimary = false; + + // Remove empty items + foreach ($items as $index => $item) { + if (!$this->isArrayEmpty($item)) { + $hasPrimary = $hasPrimary || (array_key_exists('primary', $item) && $item['primary']); + $notEmptyItems[$index] = $item; + } + } + + $items = $notEmptyItems; + + // Set first non empty address for new item as primary + if ($items && !$hasPrimary && $this->isParentFormDataNew($event->getForm()) || count($items) == 1) { + $items[current(array_keys($items))]['primary'] = true; + } + + $event->setData($items); + } + + protected function isParentFormDataNew(FormInterface $form) + { + $result = false; + $parent = $form->getParent(); + if ($parent) { + $data = $parent->getData(); + if (is_object($data)) { + if (method_exists($data, 'getId')) { + $result = !$data->getId(); + } + } + } + return $result; + + } + + /** + * Check if array is empty + * + * @param array $array + * @return bool + */ + protected function isArrayEmpty($array) + { + foreach ($array as $val) { + if (is_array($val)) { + if (!$this->isArrayEmpty($val)) { + return false; + } + } elseif (!empty($val)) { + return false; + } + } + return true; + } +} diff --git a/src/Oro/Bundle/AddressBundle/Form/Handler/AddressHandler.php b/src/Oro/Bundle/AddressBundle/Form/Handler/AddressHandler.php index 0974c2814e2..e58d283b535 100644 --- a/src/Oro/Bundle/AddressBundle/Form/Handler/AddressHandler.php +++ b/src/Oro/Bundle/AddressBundle/Form/Handler/AddressHandler.php @@ -2,12 +2,12 @@ namespace Oro\Bundle\AddressBundle\Form\Handler; -use Oro\Bundle\AddressBundle\Entity\AbstractAddress; +use Doctrine\Common\Persistence\ObjectManager; use Symfony\Component\Form\FormInterface; use Symfony\Component\HttpFoundation\Request; -use Doctrine\Common\Persistence\ObjectManager; +use Oro\Bundle\AddressBundle\Entity\AbstractAddress; class AddressHandler { @@ -42,7 +42,7 @@ public function __construct(FormInterface $form, Request $request, ObjectManager /** * Process form * - * @param AbstractAddress $entity + * @param AbstractAddress $entity * @return bool True on successful processing, false otherwise */ public function process(AbstractAddress $entity) @@ -64,11 +64,11 @@ public function process(AbstractAddress $entity) /** * "Success" form handler * - * @param AbstractAddress $entity + * @param AbstractAddress $address */ - protected function onSuccess(AbstractAddress $entity) + protected function onSuccess(AbstractAddress $address) { - $this->manager->persist($entity); + $this->manager->persist($address); $this->manager->flush(); } } diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AbstractAddressType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AbstractAddressType.php deleted file mode 100644 index 17f7067b303..00000000000 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AbstractAddressType.php +++ /dev/null @@ -1,78 +0,0 @@ -eventListener = $eventListener; - parent::__construct($flexibleManager, $valueFormAlias); - } - - /** - * {@inheritdoc} - */ - public function addEntityFields(FormBuilderInterface $builder) - { - // add default flexible fields - parent::addEntityFields($builder); - - $builder->addEventSubscriber($this->eventListener); - - // address fields - $builder - ->add('label', 'text', array('required' => false, 'label' => 'Label')) - ->add('firstName', 'text', array('required' => false, 'label' => 'First Name')) - ->add('lastName', 'text', array('required' => false, 'label' => 'Last Name')) - ->add('street', 'text', array('required' => true, 'label' => 'Street')) - ->add('street2', 'text', array('required' => false, 'label' => 'Street 2')) - ->add('city', 'text', array('required' => true, 'label' => 'City')) - ->add('state', 'oro_region', array('required' => false, 'label' => 'State')) - ->add('state_text', 'hidden', array('required' => false, 'label' => 'Custom State')) - ->add('country', 'oro_country', array('required' => true, 'label' => 'Country')) - ->add('postalCode', 'text', array('required' => true, 'label' => 'ZIP/Postal code')); - } - - /** - * {@inheritdoc} - */ - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setDefaults( - array( - 'data_class' => $this->flexibleClass, - 'intention' => 'address', - 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', - ) - ); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - throw new \BadMethodCallException(sprintf('Method %s::getName() must be overridden.', get_called_class())); - } -} diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AbstractTypedAddressType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AbstractTypedAddressType.php deleted file mode 100644 index 61f4e3b4ce9..00000000000 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AbstractTypedAddressType.php +++ /dev/null @@ -1,37 +0,0 @@ -add( - 'types', - 'translatable_entity', - array( - 'class' => 'OroAddressBundle:AddressType', - 'property' => 'label', - 'required' => false, - 'multiple' => true, - 'expanded' => true, - ) - ); - - $builder->add( - 'primary', - 'checkbox', - array( - 'label' => 'Primary', - 'required' => false - ) - ); - - parent::addEntityFields($builder); - } -} diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiType.php index 3b8ecc60b33..e9b6bc7f4e9 100644 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiType.php +++ b/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiType.php @@ -2,18 +2,19 @@ namespace Oro\Bundle\AddressBundle\Form\Type; -use Oro\Bundle\UserBundle\Form\EventListener\PatchSubscriber; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\Form\AbstractType; + +use Oro\Bundle\UserBundle\Form\EventListener\PatchSubscriber; -class AddressApiType extends AddressType +class AddressApiType extends AbstractType { /** * {@inheritdoc} */ - public function addEntityFields(FormBuilderInterface $builder) + public function buildForm(FormBuilderInterface $builder, array $options) { - parent::addEntityFields($builder); $builder->addEventSubscriber(new PatchSubscriber()); } @@ -24,14 +25,19 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults( array( - 'data_class' => $this->flexibleClass, - 'intention' => 'address', - 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', - 'csrf_protection' => false, + 'csrf_protection' => false, ) ); } + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'oro_address'; + } + /** * {@inheritdoc} */ diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiValueType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiValueType.php deleted file mode 100644 index ab0271676f3..00000000000 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AddressApiValueType.php +++ /dev/null @@ -1,16 +0,0 @@ -setDefaults( + $resolver->setNormalizers( array( - 'allow_add' => true, - 'allow_delete' => true, - 'by_reference' => false, - 'prototype' => true, - 'prototype_name' => '__name__', - 'label' => ' ', - 'validation_groups' => function (FormInterface $form) { - /** @var AbstractAddress[] $data */ - $data = $form->getData(); - $hasAddress = false; - foreach ($data as $item) { - if (!$item->isEmpty()) { - $hasAddress = true; - break; - } - } - if ($hasAddress) { - return array('Default'); - } else { - return array(); + 'options' => function (Options $options, $options) { + if (!$options) { + $options = array(); } - }, + $options['single_form'] = false; + return $options; + } ) ); - $resolver->setRequired(array('type')); } /** @@ -48,7 +31,7 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) */ public function getParent() { - return 'collection'; + return 'oro_item_collection'; } /** diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AddressType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AddressType.php index 26cfa9b49c7..8f4fda52553 100644 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AddressType.php +++ b/src/Oro/Bundle/AddressBundle/Form/Type/AddressType.php @@ -2,8 +2,63 @@ namespace Oro\Bundle\AddressBundle\Form\Type; -class AddressType extends AbstractAddressType +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; +use Symfony\Component\Form\AbstractType; + +use Oro\Bundle\AddressBundle\Form\EventListener\AddressCountryAndRegionSubscriber; + +class AddressType extends AbstractType { + /** + * @var AddressCountryAndRegionSubscriber + */ + private $countryAndRegionSubscriber; + + /** + * @param AddressCountryAndRegionSubscriber $eventListener + */ + public function __construct(AddressCountryAndRegionSubscriber $eventListener) + { + $this->countryAndRegionSubscriber = $eventListener; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + $builder->addEventSubscriber($this->countryAndRegionSubscriber); + + $builder + ->add('id', 'hidden') + ->add('label', 'text', array('required' => false, 'label' => 'Label')) + ->add('firstName', 'text', array('required' => false, 'label' => 'First Name')) + ->add('lastName', 'text', array('required' => false, 'label' => 'Last Name')) + ->add('street', 'text', array('required' => true, 'label' => 'Street')) + ->add('street2', 'text', array('required' => false, 'label' => 'Street 2')) + ->add('city', 'text', array('required' => true, 'label' => 'City')) + ->add('state', 'oro_region', array('required' => false, 'label' => 'State')) + ->add('state_text', 'hidden', array('required' => false, 'label' => 'Custom State')) + ->add('country', 'oro_country', array('required' => true, 'label' => 'Country')) + ->add('postalCode', 'text', array('required' => true, 'label' => 'ZIP/Postal code')); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\AddressBundle\Entity\Address', + 'intention' => 'address', + 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', + 'single_form' => true + ) + ); + } + /** * {@inheritdoc} */ diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/AddressValueType.php b/src/Oro/Bundle/AddressBundle/Form/Type/AddressValueType.php deleted file mode 100644 index ee155c4328d..00000000000 --- a/src/Oro/Bundle/AddressBundle/Form/Type/AddressValueType.php +++ /dev/null @@ -1,16 +0,0 @@ -add('id', 'hidden') + ->add( + 'email', + 'email', + array( + 'label' => 'Email', + 'required' => true + ) + ) + ->add( + 'primary', + 'radio', + array( + 'label' => 'Primary', + 'required' => false + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_email'; + } +} diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/ItemCollectionType.php b/src/Oro/Bundle/AddressBundle/Form/Type/ItemCollectionType.php new file mode 100644 index 00000000000..6231831fb7f --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Form/Type/ItemCollectionType.php @@ -0,0 +1,72 @@ +addEventSubscriber( + new ItemCollectionTypeSubscriber() + ); + } + + /** + * {@inheritdoc} + */ + public function buildView(FormView $view, FormInterface $form, array $options) + { + $view->vars = array_replace( + $view->vars, + array( + 'show_form_when_empty' => $options['show_form_when_empty'] + ) + ); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + 'prototype' => true, + 'prototype_name' => '__name__', + 'extra_fields_message' => 'This form should not contain extra fields: "{{ extra_fields }}"', + 'show_form_when_empty' => true + ) + ); + $resolver->setRequired(array('type')); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'collection'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_item_collection'; + } +} diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/PhoneCollectionType.php b/src/Oro/Bundle/AddressBundle/Form/Type/PhoneCollectionType.php new file mode 100644 index 00000000000..80ff828184b --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Form/Type/PhoneCollectionType.php @@ -0,0 +1,24 @@ +add('id', 'hidden') + ->add( + 'phone', + 'text', + array( + 'label' => 'Phone', + 'required' => true + ) + ) + ->add( + 'primary', + 'radio', + array( + 'label' => 'Primary', + 'required' => false + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_phone'; + } +} diff --git a/src/Oro/Bundle/AddressBundle/Form/Type/TypedAddressType.php b/src/Oro/Bundle/AddressBundle/Form/Type/TypedAddressType.php new file mode 100644 index 00000000000..6f8cb751ad6 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Form/Type/TypedAddressType.php @@ -0,0 +1,74 @@ +addEventSubscriber( + new FixAddressesPrimaryAndTypesSubscriber($options['all_addresses_property_path']) + ); + } + + $builder + ->add( + 'types', + 'translatable_entity', + array( + 'class' => 'OroAddressBundle:AddressType', + 'property' => 'label', + 'required' => false, + 'multiple' => true, + 'expanded' => true, + ) + ) + ->add( + 'primary', + 'checkbox', + array( + 'label' => 'Primary', + 'required' => false + ) + ); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\AddressBundle\Entity\AbstractTypedAddress', + 'all_addresses_property_path' => 'owner.addresses' + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'oro_address'; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_typed_address'; + } +} diff --git a/src/Oro/Bundle/AddressBundle/Resources/config/assets.yml b/src/Oro/Bundle/AddressBundle/Resources/config/assets.yml index 05b5834c52b..0cb9cb03c9e 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/AddressBundle/Resources/config/assets.yml @@ -1,5 +1,18 @@ js: - 'address': + 'address_region': - '@OroAddressBundle/Resources/public/js/views/region.updater.js' - '@OroAddressBundle/Resources/public/js/models/region.updater.js' - - '@OroAddressBundle/Resources/public/js/collections/region.updater.js' \ No newline at end of file + - '@OroAddressBundle/Resources/public/js/collections/region.updater.js' + + 'map': + - '@OroAddressBundle/Resources/public/js/views/map.googlemaps.js' + + 'addressbook': + - '@OroAddressBundle/Resources/public/js/models/address.js' + - '@OroAddressBundle/Resources/public/js/collections/address.js' + - '@OroAddressBundle/Resources/public/js/views/address.js' + - '@OroAddressBundle/Resources/public/js/views/address_book.js' + +css: + 'address': + - 'bundles/oroaddress/css/address.css' diff --git a/src/Oro/Bundle/AddressBundle/Resources/config/flexibleentity.yml b/src/Oro/Bundle/AddressBundle/Resources/config/flexibleentity.yml deleted file mode 100644 index ce1cb487c69..00000000000 --- a/src/Oro/Bundle/AddressBundle/Resources/config/flexibleentity.yml +++ /dev/null @@ -1,6 +0,0 @@ -entities_config: - Oro\Bundle\AddressBundle\Entity\Address: - flexible_manager: oro_address.address.manager.flexible - flexible_class: %oro_address.address.entity.class% - flexible_value_class: %oro_address.address.entity.value.class% - flexible_init_mode: all_attributes diff --git a/src/Oro/Bundle/AddressBundle/Resources/config/form_types.yml b/src/Oro/Bundle/AddressBundle/Resources/config/form_types.yml index 4959643b710..1ca13e91349 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/config/form_types.yml +++ b/src/Oro/Bundle/AddressBundle/Resources/config/form_types.yml @@ -1,75 +1,96 @@ parameters: - oro_address.form.listener.address.class: Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener + oro_address.form.listener.address.class: Oro\Bundle\AddressBundle\Form\EventListener\AddressCountryAndRegionSubscriber + + oro_address.form.type.item_collection.class: Oro\Bundle\AddressBundle\Form\Type\ItemCollectionType + oro_address.form.type.address.class: Oro\Bundle\AddressBundle\Form\Type\AddressType - oro_address.form.type.address_value.class: Oro\Bundle\AddressBundle\Form\Type\AddressValueType - oro_address.form.type.address_collection.class: Oro\Bundle\AddressBundle\Form\Type\AddressCollectionType oro_address.form.type.address_api.class: Oro\Bundle\AddressBundle\Form\Type\AddressApiType - oro_address.form.type.address_api_value.class: Oro\Bundle\AddressBundle\Form\Type\AddressApiValueType + oro_address.form.type.address_collection.class: Oro\Bundle\AddressBundle\Form\Type\AddressCollectionType + oro_address.form.type.typed_address.class: Oro\Bundle\AddressBundle\Form\Type\TypedAddressType + + oro_address.form.type.email.class: Oro\Bundle\AddressBundle\Form\Type\EmailType + oro_address.form.type.email_collection.class: Oro\Bundle\AddressBundle\Form\Type\EmailCollectionType + oro_address.form.type.country.class: Oro\Bundle\AddressBundle\Form\Type\CountryType oro_address.form.type.region.class: Oro\Bundle\AddressBundle\Form\Type\RegionType + + oro_address.form.type.phone.class: Oro\Bundle\AddressBundle\Form\Type\PhoneType + oro_address.form.type.phone_collection.class: Oro\Bundle\AddressBundle\Form\Type\PhoneCollectionType + oro_address.form.handler.address.class: Oro\Bundle\AddressBundle\Form\Handler\AddressHandler services: + # Addresses form listeners oro_address.form.listener.address: class: %oro_address.form.listener.address.class% arguments: - @doctrine.orm.entity_manager - @form.factory + # Collections form types + oro_address.type.item_collection: + class: %oro_address.form.type.item_collection.class% + tags: + - { name: form.type, alias: oro_item_collection } + + # Addresses form types oro_address.form.type.address: class: %oro_address.form.type.address.class% arguments: - - @oro_address.address.manager.flexible - - "oro_address_value" - @oro_address.form.listener.address tags: - { name: form.type, alias: oro_address } - oro_address.form.type.address_value: - class: %oro_address.form.type.address_value.class% - arguments: - - @oro_address.address.manager.flexible - - @oro_flexibleentity.value_form.value_subscriber + oro_address.form.type.address.api: + class: %oro_address.form.type.address_api.class% tags: - - { name: form.type, alias: oro_address_value } + - { name: form.type, alias: address } oro_address.type.address_collection: class: %oro_address.form.type.address_collection.class% tags: - { name: form.type, alias: oro_address_collection } - oro_address.form.type.address.api: - class: %oro_address.form.type.address_api.class% - arguments: - - @oro_address.address.manager.flexible - - "oro_address_value" - - @oro_address.form.listener.address + oro_address.form.type.typed_address: + class: %oro_address.form.type.typed_address.class% tags: - - { name: form.type, alias: address } + - { name: form.type, alias: oro_typed_address } - oro_address.form.type.address_api_value: - class: %oro_address.form.type.address_api_value.class% - arguments: - - @oro_address.address.manager.flexible - - @oro_flexibleentity.value_form.value_subscriber + # Emails form types + oro_address.form.type.email: + class: %oro_address.form.type.email.class% + tags: + - { name: form.type, alias: oro_email } + + oro_address.type.email_collection: + class: %oro_address.form.type.email_collection.class% + tags: + - { name: form.type, alias: oro_email_collection } + + # Phones form types + oro_address.form.type.phone: + class: %oro_address.form.type.phone.class% + tags: + - { name: form.type, alias: oro_phone } + + oro_address.type.phone_collection: + class: %oro_address.form.type.phone_collection.class% tags: - - { name: form.type, alias: oro_address_api_value } + - { name: form.type, alias: oro_phone_collection } + # Countries form types oro_address.form.type.country: class: %oro_address.form.type.country.class% tags: - { name: form.type, alias: oro_country } + # Regions form types oro_address.form.type.region: class: %oro_address.form.type.region.class% tags: - { name: form.type, alias: oro_region } - oro_address.form.type.region: - class: %oro_address.form.type.region.class% - tags: - - { name: form.type, alias: oro_region } - + # Addresses forms oro_address.form.address: class: Symfony\Component\Form\Form factory_method: createNamed @@ -88,6 +109,7 @@ services: - "address" - ~ + # Addresses form handlers oro_address.form.handler.address: class: %oro_address.form.handler.address.class% scope: request @@ -103,5 +125,3 @@ services: - @oro_address.form.address.api - @request - @doctrine.orm.entity_manager - - diff --git a/src/Oro/Bundle/AddressBundle/Resources/config/routing.yml b/src/Oro/Bundle/AddressBundle/Resources/config/routing.yml index 90f783e3830..01768aecead 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/config/routing.yml +++ b/src/Oro/Bundle/AddressBundle/Resources/config/routing.yml @@ -2,8 +2,6 @@ oro_address_api: resource: Oro\Bundle\AddressBundle\Controller\Api\Rest\AddressController type: rest prefix: api/rest/{version}/ - requirements: - version: latest|v1 defaults: version: latest @@ -11,8 +9,6 @@ oro_address_type_api: resource: Oro\Bundle\AddressBundle\Controller\Api\Rest\AddressTypeController type: rest prefix: api/rest/{version}/ - requirements: - version: latest|v1 defaults: version: latest @@ -20,8 +16,6 @@ oro_countries_api: resource: Oro\Bundle\AddressBundle\Controller\Api\Rest\CountryController type: rest prefix: api/rest/{version}/ - requirements: - version: latest|v1 defaults: version: latest @@ -29,8 +23,6 @@ oro_regions_api: resource: Oro\Bundle\AddressBundle\Controller\Api\Rest\RegionController type: rest prefix: api/rest/{version}/ - requirements: - version: latest|v1 defaults: version: latest @@ -38,7 +30,5 @@ oro_country_regions_api: resource: Oro\Bundle\AddressBundle\Controller\Api\Rest\CountryRegionsController type: rest prefix: api/rest/{version}/ - requirements: - version: latest|v1 defaults: version: latest diff --git a/src/Oro/Bundle/AddressBundle/Resources/config/services.yml b/src/Oro/Bundle/AddressBundle/Resources/config/services.yml index 82bf5dd124c..f92a62916f2 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/AddressBundle/Resources/config/services.yml @@ -3,65 +3,26 @@ parameters: oro_address.address.type.entity.class: Oro\Bundle\AddressBundle\Entity\AddressType oro_address.address.manager.class: Oro\Bundle\AddressBundle\Entity\Manager\AddressManager - oro_address.address.manager.api.class: Oro\Bundle\SoapBundle\Entity\Manager\ApiFlexibleEntityManager - oro_address.address.manager.flexible.class: Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager - - oro_address.address.entity.value.class: Oro\Bundle\AddressBundle\Entity\Value\AddressValue + oro_address.address.manager.api.class: Oro\Bundle\SoapBundle\Entity\Manager\ApiEntityManager oro_address.provider.address.class: Oro\Bundle\AddressBundle\Provider\AddressProvider - oro_address.entity.value.class: Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityFlexibleValue - - oro_address.attribute.address.class: Oro\Bundle\AddressBundle\AttributeType\AddressType - - oro_address.twig.hasAddress.extension.class: Oro\Bundle\AddressBundle\Twig\HasAddressExtension services: oro_address.address.provider: class: %oro_address.provider.address.class% - ##### STORAGE DEFINITION ###### - oro_address.address.manager.flexible: - class: %oro_address.address.manager.flexible.class% - arguments: - - %oro_address.address.entity.class% - - %oro_flexibleentity.flexible_config% - - @doctrine.orm.entity_manager - - @event_dispatcher - - @oro_flexibleentity.attributetype.factory - tags: - - { name: oro_flexibleentity_manager, entity: %oro_address.address.entity.class%} - calls: - - [ addAttributeType, ['oro_flexibleentity_text'] ] - oro_address.address.manager: class: %oro_address.address.manager.class% arguments: - %oro_address.address.entity.class% - @doctrine.orm.entity_manager - - @oro_address.address.manager.flexible tags: - - { name: oro_address.storage} + - { name: oro_address.storage } oro_address.address.manager.api: class: %oro_address.address.manager.api.class% arguments: - %oro_address.address.entity.class% - @doctrine.orm.entity_manager - - @oro_address.address.manager.flexible - ##### END OF STORAGE DEFINITION ###### - - # Flexible attribute - oro_address.attribute_type.address: - class: %oro_address.attribute.address.class% - arguments: - - "address" - - "oro_address" - - @oro_flexibleentity.validator.attribute_constraint_guesser - tags: - - { name: oro_flexibleentity.attributetype, alias: oro_address } - oro_address.twig.hasAddress.extension: - class: %oro_address.twig.hasAddress.extension.class% - tags: - - { name: twig.extension } diff --git a/src/Oro/Bundle/AddressBundle/Resources/config/validation.yml b/src/Oro/Bundle/AddressBundle/Resources/config/validation.yml index 708496b5795..7950c4197dd 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/config/validation.yml +++ b/src/Oro/Bundle/AddressBundle/Resources/config/validation.yml @@ -32,3 +32,15 @@ Oro\Bundle\AddressBundle\Entity\AbstractAddress: constraints: - Callback: methods: [isStateValid] + + +Oro\Bundle\AddressBundle\Entity\AbstractEmail: + properties: + email: + - NotBlank: ~ + - Email: ~ + +Oro\Bundle\AddressBundle\Entity\AbstractPhone: + properties: + phone: + - NotBlank: ~ diff --git a/src/Oro/Bundle/AddressBundle/Resources/doc/reference/entities.md b/src/Oro/Bundle/AddressBundle/Resources/doc/reference/entities.md index ac86ddded75..8e112eca7a1 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/doc/reference/entities.md +++ b/src/Oro/Bundle/AddressBundle/Resources/doc/reference/entities.md @@ -8,7 +8,6 @@ OroAddressBunle provides several entities to work with addresses. * **AbstractAddress** - encapsulates basic address attributes (label, street, city, country, first and last name etc.); * **AbstractTypedAddress** - extends AbstractAddress and adds flag "primary" and collection of address types; * **Address** - basic implementation of AbstractAddress; -* **AddressSoap** - extends Address entity to work with SOAP API; * **Country** - encapsulates country attributes (ISO2 and ISO3 codes, name, collection of regions); * **CountryTranslation** - translation entity for Country entity; * **Region** - encapsulates region attributes (combined code "country+region", code, name, country entity) ; diff --git a/src/Oro/Bundle/AddressBundle/Resources/doc/reference/form_types.md b/src/Oro/Bundle/AddressBundle/Resources/doc/reference/form_types.md index cbac40291bd..33eee309de1 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/doc/reference/form_types.md +++ b/src/Oro/Bundle/AddressBundle/Resources/doc/reference/form_types.md @@ -6,30 +6,25 @@ OroAddressBundle provides form types to render address entities on forms. ### Form Types Description * **oro\_address** - encapsulates form fields for Address entity; -* **oro\_address\_value** - form type for flexible entity values fields; * **address** - form type for API for Address entity; -* **oro\_address\_api\_value** - form type for API for flexible entity values fields; * **oro\_address\_collection** - collection of form types for address entities; * **oro\_country** - encapsulates form fields for Country entity; * **oro\_region** - encapsulates form fields for Region entity. ### Classes Description -* **Form \ Type \ AbstractAddressType** - abstract class for address form type, includes form fields -for address attributes; -* **Form \ Type \ AbstractTypedAddressType** - extends AbstractAddressType, adds functionality -to work with address types; +* **Form \ Type \ AddressType** - base form for Address, includes form fields for address attributes; +* **Form \ Type \ TypedAddressType** - extends AddressType, adds functionality to work with address types; * **Form \ Type \ AddressType** - implementation of AbstractAddressType, name is "oro_address"; -* **Form \ Type \ AddressValueType** - form type for flexible attribute values, name is "oro_address_value"; * **Form \ Type \ AddressApiType** - extends AddressType, used in API, name is "address"; -* **Form \ Type \ AddressApiValueType** - form type for API for flexible attribute values, -name is "oro_address_api_value"; * **Form \ Type\ AddressCollectionType** - provides functionality to work with address collections, name is "oro_address_collection"; * **Form \ Type \ CountryType** - form type for Country entity, name is "oro_country"; * **Form \ Type \ RegionType** - form type fot Region entity, name is "oro_region"; -* **Form \ EventListener \ BuildAddressFormListener** - responsible for processing relation +* **Form \ EventListener \ AddressCountryAndRegionSubscriber** - responsible for processing relation between countries and regions on address form; +* **Form \ EventListener \ FixAddressPrimaryAndTypesSubscriber** - responsible for processing single address submit +to respect rules of single primary address and uniqueness types of addresses; * **Form \ EventListener \ AddressCollectionTypeSubscriber** - responsible for processing of address elements at address collection form; -* **Form \ Handler \ AddressHandler** - processes save for AbstractAddress entity using specified form. +* **Form \ Handler \ AddressHandler** - processes save for Address entity using specified form. diff --git a/src/Oro/Bundle/AddressBundle/Resources/doc/reference/installation.md b/src/Oro/Bundle/AddressBundle/Resources/doc/reference/installation.md index 6ba15776dc0..d42f65dc94e 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/doc/reference/installation.md +++ b/src/Oro/Bundle/AddressBundle/Resources/doc/reference/installation.md @@ -36,6 +36,5 @@ public function registerBundles() * FOSJsRoutingBundle https://github.com/FriendsOfSymfony/FOSJsRoutingBundle * BeSimpleSoapBundle, https://github.com/BeSimple/BeSimpleSoapBundle * NelmioApiDocBundle https://github.com/nelmio/NelmioApiDocBundle -* OroFlexibleEntityBundle, https://github.com/orocrm/platform/tree/master/src/Oro/Bundle/FlexibleEntityBundle * OroFormBundle, https://github.com/orocrm/platform/tree/master/src/Oro/Bundle/FormBundle * OroTranslationBundle, https://github.com/orocrm/platform/tree/master/src/Oro/Bundle/TranslationBundle diff --git a/src/Oro/Bundle/AddressBundle/Resources/doc/reference/usage.md b/src/Oro/Bundle/AddressBundle/Resources/doc/reference/usage.md index b3593e613a6..b11f1e03a79 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/doc/reference/usage.md +++ b/src/Oro/Bundle/AddressBundle/Resources/doc/reference/usage.md @@ -12,7 +12,7 @@ OroAddressBundle provides PHP/REST/SOAP API for address CRUD operations. $addressManager = $this->get('oro_address.address.provider')->getStorage(); //create empty address entity - $address = $addressManager->createFlexible(); + $address = $addressManager->createAddress(); //process insert/update $this->get('oro_address.form.handler.address')->process($entity) diff --git a/src/Oro/Bundle/AddressBundle/Resources/public/css/address.css b/src/Oro/Bundle/AddressBundle/Resources/public/css/address.css new file mode 100644 index 00000000000..50679a728f7 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Resources/public/css/address.css @@ -0,0 +1,4 @@ +/* TODO: fix form styles to let specifying of custom element style without full CSS path */ +.form-horizontal .span6 .oro-multiselect-holder .collection-element-primary input { + margin: 0; +} diff --git a/src/Oro/Bundle/AddressBundle/Resources/public/js/collections/address.js b/src/Oro/Bundle/AddressBundle/Resources/public/js/collections/address.js new file mode 100644 index 00000000000..0e268563cd9 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Resources/public/js/collections/address.js @@ -0,0 +1,20 @@ +var OroAddressCollection = Backbone.Collection.extend({ + model: OroAddress, + + initialize: function() { + this.on('change:active', this.onActiveChange, this); + }, + + onActiveChange: function(item) { + // Only 1 item allowed to be active + if (item.get('active')) { + var activeItems = this.where({active: true}); + _.each(activeItems, function(activeItem) { + if (activeItem.get('id') != item.get('id')) { + activeItem.set('active', false); + } + }); + this.trigger('activeChange', item); + } + } +}); diff --git a/src/Oro/Bundle/AddressBundle/Resources/public/js/models/address.js b/src/Oro/Bundle/AddressBundle/Resources/public/js/models/address.js new file mode 100644 index 00000000000..675d7d22281 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Resources/public/js/models/address.js @@ -0,0 +1,23 @@ +var OroAddress = Backbone.Model.extend({ + defaults: { + 'label': '', + 'firstName': '', + 'lastName': '', + 'street': '', + 'street2': '', + 'city': '', + 'country': '', + 'postalCode': '', + 'state': '', + 'primary': false, + 'types': [], + + 'active': false + }, + + getSearchableString: function() { + return this.get('country') + ', ' + + this.get('city') + ', ' + + this.get('street') + ' ' + (this.get('street2') || ''); + } +}); diff --git a/src/Oro/Bundle/AddressBundle/Resources/public/js/views/address.js b/src/Oro/Bundle/AddressBundle/Resources/public/js/views/address.js new file mode 100644 index 00000000000..c0157dfd88e --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Resources/public/js/views/address.js @@ -0,0 +1,54 @@ +var OroAddressView = Backbone.View.extend({ + tagName: 'div', + + attributes: { + 'class': 'map-item' + }, + + events: { + 'click': 'activate', + 'click button:has(.icon-remove)': 'close', + 'click button:has(.icon-edit)': 'edit' + }, + + initialize: function() { + this.template = _.template($("#template-contact-address").html()); + this.listenTo(this.model, 'destroy', this.remove) + this.listenTo(this.model, 'change:active', this.toggleActive) + }, + + activate: function() { + this.model.set('active', true); + }, + + toggleActive: function() { + if (this.model.get('active')) { + this.$el.addClass('active'); + } else { + this.$el.removeClass('active'); + } + }, + + edit: function(e) { + this.trigger('edit', this, this.model); + }, + + close: function() + { + if (this.model.get('primary')) { + alert(_.__('Primary address can not be removed')); + } else { + this.model.destroy({wait: true}); + } + }, + + render: function () { + this.$el.append( + this.template(this.model.toJSON()) + ); + if (this.model.get('primary')) { + this.activate(); + } + return this; + } +}); diff --git a/src/Oro/Bundle/AddressBundle/Resources/public/js/views/address_book.js b/src/Oro/Bundle/AddressBundle/Resources/public/js/views/address_book.js new file mode 100644 index 00000000000..d5371bf3641 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Resources/public/js/views/address_book.js @@ -0,0 +1,139 @@ +var OroAddressBook = Backbone.View.extend({ + options: { + 'mapOptions': { + zoom: 17 + }, + 'template': null, + 'addressListUrl': null, + 'addressCreateUrl': null, + 'addressUpdateUrl': null, + 'mapView': OroMapView.googlemaps + }, + + attributes: { + 'class': 'map-box' + }, + + initialize: function() { + this.options.collection = this.options.collection || new OroAddressCollection(); + this.options.collection.url = this._getUrl('addressListUrl'); + + this.listenTo(this.getCollection(), 'activeChange', this.activateAddress); + this.listenTo(this.getCollection(), 'add', this.addAddress); + this.listenTo(this.getCollection(), 'reset', this.addAll); + this.listenTo(this.getCollection(), 'remove', this.onAddressRemove); + + this.$adressesContainer = Backbone.$('
').appendTo(this.$el); + this.$mapContainerFrame = Backbone.$('
').appendTo(this.$el); + this.mapView = new this.options.mapView({ + 'mapOptions': this.options.mapOptions, + 'el': this.$mapContainerFrame + }); + }, + + _getUrl: function(optionsKey) { + if (_.isFunction(this.options[optionsKey])) { + return this.options[optionsKey].apply(this, Array.prototype.slice.call(arguments, 1)); + } + return this.options[optionsKey]; + }, + + getCollection: function() { + return this.options.collection; + }, + + onAddressRemove: function() { + if (!this.getCollection().where({active: true}).length) { + var primaryAddress = this.getCollection().where({primary: true}); + if (primaryAddress.length) { + primaryAddress[0].set('active', true); + } else if (this.getCollection().length) { + this.getCollection().at(0).set('active', true); + } + } + }, + + addAll: function(items) { + this.$adressesContainer.empty(); + items.each(function(item) { + this.addAddress(item); + }, this); + this._activatePreviousAddress(); + }, + + _activatePreviousAddress: function() { + if (this.activeAddress !== undefined) { + var previouslyActive = this.getCollection().where({id: this.activeAddress.get('id')}); + if (previouslyActive.length) { + previouslyActive[0].set('active', true); + } + } + }, + + addAddress: function(address) { + var addressView = new OroAddressView({ + model: address + }); + addressView.on('edit', _.bind(this.editAddress, this)); + this.$adressesContainer.append(addressView.render().$el); + }, + + editAddress: function(addressView, address) { + this._openAddressEditForm( + _.__('Update Address'), + this._getUrl('addressUpdateUrl', address) + ); + }, + + createAddress: function() { + this._openAddressEditForm( + _.__('Add Address'), + this._getUrl('addressCreateUrl') + ); + }, + + _openAddressEditForm: function(title, url) { + if (!this.addressEditDialog) { + this.addressEditDialog = Oro.widget.Manager.createWidget('dialog', { + 'url': url, + 'title': title, + 'stateEnabled': false, + 'incrementalPosition': false, + 'dialogOptions': { + 'modal': true, + 'resizable': false, + 'width': 400, + 'autoResize':true, + 'close': _.bind(function() { + delete this.addressEditDialog; + }, this) + } + }); + this.addressEditDialog.render(); + Oro.Events.bind( + "hash_navigation_request:start", + _.bind(function () { + if (this.addressEditDialog) { + this.addressEditDialog.remove(); + } + }, this) + ); + this.addressEditDialog.on('formSave', _.bind(function() { + this.addressEditDialog.remove(); + Oro.NotificationFlashMessage('success', _.__('Address successfully saved')); + this.reloadAddresses(); + }, this)); + } + }, + + reloadAddresses: function() { + this.getCollection().fetch({reset: true}); + }, + + activateAddress: function(address) { + if (!address.get('primary')) { + this.activeAddress = address; + } + this.mapView.updateMap(address.getSearchableString(), address.get('label')); + } +}); diff --git a/src/Oro/Bundle/AddressBundle/Resources/public/js/views/map.googlemaps.js b/src/Oro/Bundle/AddressBundle/Resources/public/js/views/map.googlemaps.js new file mode 100644 index 00000000000..82f3a7d85a7 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Resources/public/js/views/map.googlemaps.js @@ -0,0 +1,141 @@ +var OroMapView = OroMapView || {}; +OroMapView.googlemaps = Backbone.View.extend({ + options: { + mapOptions: { + zoom: 17, + mapTypeControl: true, + panControl: false, + zoomControl: true + }, + apiVersion: '3.exp', + sensor: false, + apiKey: null + }, + + mapLocationCache: {}, + mapsLoadExecuted: false, + + initialize: function() { + this.$mapContainer = Backbone.$('
') + .appendTo(this.$el); + this.$unknownAddress = Backbone.$('
' + _.__('map.unknown.location') + '
') + .appendTo(this.$el); + this.mapLocationUnknown(); + }, + + _initMapOptions: function() { + if (_.isUndefined(this.options.mapOptions.mapTypeControlOptions)) { + this.options.mapOptions.mapTypeControlOptions = { + style: google.maps.MapTypeControlStyle.DROPDOWN_MENU + }; + } + if (_.isUndefined(this.options.mapOptions.zoomControlOptions)) { + this.options.mapOptions.zoomControlOptions = { + style: google.maps.ZoomControlStyle.SMALL + }; + } + if (_.isUndefined(this.options.mapOptions.mapTypeId)) { + this.options.mapOptions.mapTypeId = google.maps.MapTypeId.ROADMAP; + } + }, + + _initMap: function(location) { + this._initMapOptions(); + this.map = new google.maps.Map( + this.$mapContainer[0], + _.extend({}, this.options.mapOptions, {center: location}) + ); + + this.mapLocationMarker = new google.maps.Marker({ + draggable: false, + map: this.map, + position: location + }); + }, + + loadGoogleMaps: function() { + var googleMapsSettings = 'sensor=' + (this.options.sensor ? 'true' : 'false'); + if (this.options.apiKey) { + googleMapsSettings += '&key=' + this.options.apiKey + } + + Backbone.$.ajax({ + url: window.location.protocol + "//www.google.com/jsapi", + dataType: "script", + cache: true, + success: _.bind(function() { + google.load('maps', this.options.apiVersion, { + other_params: googleMapsSettings, + callback: _.bind(this.onGoogleMapsInit, this) + }); + }, this) + }); + }, + + updateMap: function(address, label) { + // Load google maps js + if (!this.hasGoogleMaps() && !this.mapsLoadExecuted) { + this.mapsLoadExecuted = true; + this.requestedLocation = { + 'address': address, + 'label': label + }; + this.loadGoogleMaps(); + + return; + } + + if (this.mapLocationCache.hasOwnProperty(address)) { + this.updateMapLocation(this.mapLocationCache[address], label); + } else { + this.getGeocoder().geocode({'address': address}, _.bind(function(results, status) { + if(status == google.maps.GeocoderStatus.OK) { + this.mapLocationCache[address] = results[0].geometry.location; + //Move location marker and map center to new coordinates + this.updateMapLocation(results[0].geometry.location, label); + } else { + this.mapLocationUnknown(); + } + }, this)); + } + }, + + onGoogleMapsInit: function() { + if (!_.isUndefined(this.requestedLocation)) { + this.updateMap(this.requestedLocation.address, this.requestedLocation.label); + delete this.requestedLocation; + } + }, + + hasGoogleMaps: function() { + return !_.isUndefined(window.google) && google.hasOwnProperty('maps'); + }, + + mapLocationUnknown: function() { + this.$mapContainer.hide(); + this.$unknownAddress.show(); + }, + + mapLocationKnown: function() { + this.$mapContainer.show(); + this.$unknownAddress.hide(); + }, + + updateMapLocation: function(location, label) { + this.mapLocationKnown(); + if (location && (!this.location || location.toString() != this.location.toString())) { + this._initMap(location); + this.map.setCenter(location); + this.mapLocationMarker.setPosition(location); + this.mapLocationMarker.setTitle(label); + this.location = location; + } + }, + + getGeocoder: function() { + if (_.isUndefined(this.geocoder)) { + this.geocoder = new google.maps.Geocoder(); + } + return this.geocoder; + } +}); diff --git a/src/Oro/Bundle/AddressBundle/Resources/public/js/views/region.updater.js b/src/Oro/Bundle/AddressBundle/Resources/public/js/views/region.updater.js index 621cbbdbffd..2106ea7ed1e 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/public/js/views/region.updater.js +++ b/src/Oro/Bundle/AddressBundle/Resources/public/js/views/region.updater.js @@ -12,16 +12,18 @@ Oro.RegionUpdater.View = Backbone.View.extend({ * @param options {Object} */ initialize: function (options) { - this.target = $(options.target); - this.$simpleEl = $(options.simpleEl); + this.target = Backbone.$(options.target); + this.$simpleEl = Backbone.$(options.simpleEl); this.target.closest('.controls').append(this.$simpleEl); + this.uniform = Backbone.$('#uniform-' + this.target[0].id); this.$simpleEl.attr('type', 'text'); this.showSelect = options.showSelect; - this.template = $('#region-chooser-template').html(); + this.template = Backbone.$('#region-chooser-template').html(); + this.displaySelect2(this.showSelect); this.target.on('select2-init', _.bind(function() { this.displaySelect2(this.showSelect); }, this)); @@ -36,18 +38,42 @@ Oro.RegionUpdater.View = Backbone.View.extend({ */ displaySelect2: function(display) { if (display) { + this.addRequiredFlag(this.$simpleEl); this.target.select2('container').show(); } else { this.target.select2('container').hide(); + this.removeRequiredFlag(this.$simpleEl); } }, + addRequiredFlag: function(el) { + var label = this.getInputLabel(el); + if (!label.hasClass('required')) { + label + .addClass('required') + .prepend('*'); + } + }, + + removeRequiredFlag: function(el) { + var label = this.getInputLabel(el); + if (label.hasClass('required')) { + label + .removeClass('required') + .find('em').remove(); + } + }, + + getInputLabel: function(el) { + return el.parent().parent().find('label'); + }, + /** * Trigger change event */ sync: function () { - if (this.target.val() == '' && $(this.el).val() != '') { - $(this.el).trigger('change'); + if (this.target.val() == '' && this.$el.val() != '') { + this.$el.trigger('change'); } }, @@ -57,7 +83,7 @@ Oro.RegionUpdater.View = Backbone.View.extend({ * @param e {Object} */ selectionChanged: function (e) { - var countryId = $(e.currentTarget).val(); + var countryId = Backbone.$(e.currentTarget).val(); this.collection.setCountryId(countryId); this.collection.fetch(); }, @@ -66,7 +92,7 @@ Oro.RegionUpdater.View = Backbone.View.extend({ if (this.collection.models.length > 0) { this.target.show(); this.displaySelect2(true); - $('#uniform-' + this.target[0].id).show(); + this.uniform.show(); this.target.val('').trigger('change'); this.target.find('option[value!=""]').remove(); @@ -78,7 +104,7 @@ Oro.RegionUpdater.View = Backbone.View.extend({ this.target.hide(); this.target.val(''); this.displaySelect2(false); - $('#uniform-' + this.target[0].id).hide(); + this.uniform.hide(); this.$simpleEl.show(); } } diff --git a/src/Oro/Bundle/AddressBundle/Resources/translations/jsmessages.en.yml b/src/Oro/Bundle/AddressBundle/Resources/translations/jsmessages.en.yml new file mode 100644 index 00000000000..7cb43fae227 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Resources/translations/jsmessages.en.yml @@ -0,0 +1,6 @@ +'map.unknown.location': 'Sorry, location is unavailable' +'Edit': 'Edit' +'Remove': 'Remove' +'Primary': 'Primary' +'Create Address': 'Create Address' +'Update Address': 'Update Address' diff --git a/src/Oro/Bundle/AddressBundle/Resources/translations/validators.en.yml b/src/Oro/Bundle/AddressBundle/Resources/translations/validators.en.yml index 5fef5dfe260..942579bc01a 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/translations/validators.en.yml +++ b/src/Oro/Bundle/AddressBundle/Resources/translations/validators.en.yml @@ -1,2 +1,2 @@ -"One of addresses must be set as primary.": "One of addresses must be set as primary." +"One of items must be set as primary.": "One of items must be set as primary." "Several addresses have the same type {{ types }}.": "Several addresses have the same type {{ types }}." diff --git a/src/Oro/Bundle/AddressBundle/Resources/views/Include/fields.html.twig b/src/Oro/Bundle/AddressBundle/Resources/views/Include/fields.html.twig index 8420be877ae..fb6254bf832 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/views/Include/fields.html.twig +++ b/src/Oro/Bundle/AddressBundle/Resources/views/Include/fields.html.twig @@ -51,20 +51,55 @@ {% endblock oro_region_widget %} +{% block oro_email_widget %} + + {{ form_rest(form) }} +{% endblock %} + +{% block oro_phone_widget %} + + {{ form_rest(form) }} +{% endblock %} + {% block oro_address_widget %} + {{ form_errors(form) }} + {{ form_row(form.id) }} {{ form_row(form.label) }} {{ form_row(form.firstName) }} {{ form_row(form.lastName) }} + {{ form_row(form.country) }} {{ form_row(form.street) }} {{ form_row(form.street2) }} {{ form_row(form.city) }} {{ form_row(form.state_text) }} {{ form_row(form.state) }} - {{ form_row(form.country) }} {{ form_row(form.postalCode) }} -{% endblock oro_address_widget %} + {{ form_rest(form) }} +{% endblock %} + +{% block oro_typed_address_widget %} + {{ form_row(form.types) }} + {{ form_row(form.primary) }} + {{ block('oro_address_widget') }} +{% endblock %} -{% macro address_collection_prototype(widget) %} +{% macro oro_collection_item_prototype(widget) %} {% if 'prototype' in widget.vars|keys %} {% set form = widget.vars.prototype %} {% set name = widget.vars.prototype.vars.name %} @@ -74,52 +109,69 @@ {% endif %}
- {{ form_errors(form) }} - {{ form_row(form.id) }} - {{ form_row(form.types) }} - {{ form_row(form.primary) }} - {{ block('oro_address_widget') }} + {{ form_widget(form) }}
{% endmacro %} -{% block oro_address_collection_widget %} +{% block oro_item_collection_widget %} {% spaceless %} + {% set prototypeHtml = _self.oro_collection_item_prototype(form) %}
-
- {% for field in form.children %} - {{ _self.address_collection_prototype(field) }} - {% endfor %} +
+ {% if form.children|length %} + {% for child in form.children %} + {{ _self.oro_collection_item_prototype(child) }} + {% endfor %} + {% elseif show_form_when_empty %} + {{ prototypeHtml|replace({'__name__': '0'})|raw }} + {% endif %}
{{ 'Add'|trans }}
+ {{ _self.oro_item_collection_validate_primary_js(_context) }} + {% endspaceless %} +{% endblock %} + +{% macro oro_item_collection_validate_primary_js(context) %} + {% set form = context.form %} + {% set show_form_when_empty = context.show_form_when_empty %} + {% set id = context.id %} + {% set has_primary = form.vars.prototype.primary is defined %} + {% set has_types = form.vars.prototype.types is defined %} + {% if has_primary or has_types or show_form_when_empty %} - {% endspaceless %} -{% endblock oro_address_collection_widget %} + {% endif %} +{% endmacro %} diff --git a/src/Oro/Bundle/AddressBundle/Resources/views/Include/viewMacro.html.twig b/src/Oro/Bundle/AddressBundle/Resources/views/Include/viewMacro.html.twig index 1513fd46edf..156fc302be5 100644 --- a/src/Oro/Bundle/AddressBundle/Resources/views/Include/viewMacro.html.twig +++ b/src/Oro/Bundle/AddressBundle/Resources/views/Include/viewMacro.html.twig @@ -16,20 +16,10 @@ {{ __.renderAddressView(address, fields) }} {% endmacro %} -{% macro renderAddressView(address, fields, renderFlexibleAttributes) %} - {# Render flexible attributes by default #} - {% set renderFlexibleAttributes = renderFlexibleAttributes|default(true) %} - - {# Render static attributes #} +{% macro renderAddressView(address, fields) %} {% for field in fields %} {{ block('addressStaticAttribute') }} {% endfor %} - - {# Render flexible attributes #} - {% import 'OroUIBundle::macros.html.twig' as UI %} - {% for value in address|getAttributes() %} - {{ UI.flexibleAttributeRow(value) }} - {% endfor %} {% endmacro %} {% block addressStaticAttribute %} diff --git a/src/Oro/Bundle/AddressBundle/Resources/views/Js/address.js.twig b/src/Oro/Bundle/AddressBundle/Resources/views/Js/address.js.twig new file mode 100644 index 00000000000..567be1b1367 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Resources/views/Js/address.js.twig @@ -0,0 +1,22 @@ + diff --git a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestAddressTypeApiTest.php b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestAddressTypeApiTest.php index 364118f64b3..9f87311a5f8 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestAddressTypeApiTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestAddressTypeApiTest.php @@ -16,11 +16,7 @@ class RestAddressTypeApiTest extends WebTestCase public function setUp() { - if (!isset($this->client)) { - $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); - } else { - $this->client->restart(); - } + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); } /** diff --git a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestApiTest.php b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestApiTest.php index 4fe28e10a70..1a82081ea12 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestApiTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/RestApiTest.php @@ -18,11 +18,7 @@ class RestApiTest extends WebTestCase public function setUp() { - if (!isset($this->client)) { - $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); - } else { - $this->client->restart(); - } + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); } /** @@ -85,6 +81,31 @@ public function testGetAddress($id) $this->assertEquals($id, $resultJson['id']); } + /** + * Test GET + * + * @depends testCreateAddress + */ + public function testGetAddresses($id) + { + $this->client->request( + 'GET', + $this->client->generate('oro_api_get_addresses') + ); + + /** @var $result Response */ + $result = $this->client->getResponse(); + + ToolsAPI::assertJsonResponse($result, 200); + $resultJson = json_decode($result->getContent(), true); + + $this->assertNotEmpty($resultJson); + $this->assertArrayHasKey(0, $resultJson); + $this->assertArrayHasKey('id', $resultJson[0]); + + $this->assertEquals($id, $resultJson[0]['id']); + } + /** * Test PUT * diff --git a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapAddressTypeApiTest.php b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapAddressTypeApiTest.php index 591ac0972e8..956e791de61 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapAddressTypeApiTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapAddressTypeApiTest.php @@ -16,19 +16,14 @@ class SoapAddressTypeApiTest extends WebTestCase public function setUp() { - if (!isset($this->client)) { - $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); - $this->client->soap( - "http://localhost/api/soap", - array( - 'location' => 'http://localhost/api/soap', - 'soap_version' => SOAP_1_2 - ) - ); - } else { - $this->client->restart(); - } - + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); + $this->client->soap( + "http://localhost/api/soap", + array( + 'location' => 'http://localhost/api/soap', + 'soap_version' => SOAP_1_2 + ) + ); } /** @@ -36,7 +31,7 @@ public function setUp() */ public function testGetAddressTypes() { - $result = $this->client->soapClient->getAddressTypes(); + $result = $this->client->getSoap()->getAddressTypes(); $result = ToolsAPI::classToArray($result); if (is_array(reset($result['item']))) { $actualData = $result['item']; @@ -55,7 +50,7 @@ public function testGetAddressTypes() public function testGetAddressType($expected) { foreach ($expected as $addrType) { - $result = $this->client->soapClient->getAddressType($addrType['name']); + $result = $this->client->getSoap()->getAddressType($addrType['name']); $result = ToolsAPI::classToArray($result); $this->assertNotEmpty($result); $this->assertEquals($addrType, $result); diff --git a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapApiTest.php b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapApiTest.php index d51e2b69265..fa6aa1c03b7 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapApiTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Functional/API/SoapApiTest.php @@ -68,24 +68,21 @@ class SoapApiTest extends WebTestCase public function setUp() { - if (!isset($this->client)) { - $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); - $this->client->soap( - "http://localhost/api/soap", - array( - 'location' => 'http://localhost/api/soap', - 'soap_version' => SOAP_1_2 - ) - ); - } else { - $this->client->restart(); - } - + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); + $this->client->soap( + "http://localhost/api/soap", + array( + 'location' => 'http://localhost/api/soap', + 'soap_version' => SOAP_1_2 + ) + ); } public function testCreateAddress() { - $this->assertTrue($this->client->soapClient->createAddress($this->addressData['Create Address Data'])); + $id = $this->client->getSoap()->createAddress($this->addressData['Create Address Data']); + $this->assertInternalType('int', $id); + $this->assertGreaterThan(0, $id); } /** @@ -94,7 +91,7 @@ public function testCreateAddress() */ public function testGetAddresses() { - $result = $this->client->soapClient->getAddresses(); + $result = $this->client->getSoap()->getAddresses(); $result = ToolsAPI::classToArray($result); if (is_array(reset($result['item']))) { $actualData = $result['item']; @@ -132,7 +129,7 @@ function ($a) { */ public function testGetAddress($id) { - $result = $this->client->soapClient->getAddress($id); + $result = $this->client->getSoap()->getAddress($id); $actualData = ToolsAPI::classToArray($result); unset($actualData['id']); $this->assertEquals($this->addressData['Expected Address Data'], $actualData); @@ -144,8 +141,8 @@ public function testGetAddress($id) */ public function testUpdateAddress($id) { - $this->assertTrue($this->client->soapClient->updateAddress($id, $this->addressData['Update Address Data'])); - $result = $this->client->soapClient->getAddress($id); + $this->assertTrue($this->client->getSoap()->updateAddress($id, $this->addressData['Update Address Data'])); + $result = $this->client->getSoap()->getAddress($id); $actualData = ToolsAPI::classToArray($result); unset($actualData['id']); $this->assertEquals($this->addressData['Expected Updated Address Data'], $actualData); @@ -157,9 +154,9 @@ public function testUpdateAddress($id) */ public function testDeleteAddress($id) { - $this->assertTrue($this->client->soapClient->deleteAddress($id)); + $this->assertTrue($this->client->getSoap()->deleteAddress($id)); $this->setExpectedException('SoapFault'); - $this->client->soapClient->getAddress($id); + $this->client->getSoap()->getAddress($id); } /** @@ -167,7 +164,7 @@ public function testDeleteAddress($id) */ public function testGetCountries() { - $result = $this->client->soapClient->getCountries(); + $result = $this->client->getSoap()->getCountries(); $result = ToolsAPI::classToArray($result); return array_slice($result['item'], 0, 5); } @@ -179,7 +176,7 @@ public function testGetCountries() public function testGetCountry($countries) { foreach ($countries as $country) { - $result = $this->client->soapClient->getCountry($country['iso2Code']); + $result = $this->client->getSoap()->getCountry($country['iso2Code']); $result = ToolsAPI::classToArray($result); $this->assertEquals($country, $result); } @@ -190,7 +187,7 @@ public function testGetCountry($countries) */ public function testGetRegions() { - $result = $this->client->soapClient->getRegions(); + $result = $this->client->getSoap()->getRegions(); $result = ToolsAPI::classToArray($result); return array_slice($result['item'], 0, 5); } @@ -202,7 +199,7 @@ public function testGetRegions() public function testGetRegion($regions) { foreach ($regions as $region) { - $result = $this->client->soapClient->getRegion($region['combinedCode']); + $result = $this->client->getSoap()->getRegion($region['combinedCode']); $result = ToolsAPI::classToArray($result); $this->assertEquals($region, $result); } @@ -213,11 +210,11 @@ public function testGetRegion($regions) */ public function testGetCountryRegion() { - $result = $this->client->soapClient->getRegionByCountry('US'); + $result = $this->client->getSoap()->getRegionByCountry('US'); $result = ToolsAPI::classToArray($result); foreach ($result['item'] as $region) { $region['country'] = $region['country']['name']; - $expectedResult = $this->client->soapClient->getRegion($region['combinedCode']); + $expectedResult = $this->client->getSoap()->getRegion($region['combinedCode']); $expectedResult = ToolsAPI::classToArray($expectedResult); $this->assertEquals($expectedResult, $region); } diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php index d7155df5e3d..c22ae22c6a7 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractAddressTest.php @@ -13,7 +13,7 @@ class AbstractAddressTest extends \PHPUnit_Framework_TestCase */ public function testSettersAndGetters($property, $value) { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); call_user_func_array(array($obj, 'set' . ucfirst($property)), array($value)); $this->assertEquals($value, call_user_func_array(array($obj, 'get' . ucfirst($property)), array())); @@ -44,14 +44,14 @@ public function propertiesDataProvider() 'stateText' => array('stateText', 'test state'), 'postalCode' => array('postalCode', '12345'), 'country' => array('country', $countryMock), - 'created' => array('created', new \DateTime()), - 'updated' => array('updated', new \DateTime()), + 'created' => array('createdAt', new \DateTime()), + 'updated' => array('updatedAt', new \DateTime()), ); } public function testBeforeSave() { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $obj->beforeSave(); $this->assertNotNull($obj->getCreatedAt()); @@ -65,7 +65,7 @@ public function testBeforeSave() */ public function testToString(array $actualData, $expected) { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); foreach ($actualData as $key => $value) { $setter = 'set' . ucfirst($key); @@ -129,7 +129,7 @@ protected function createMockRegion($name) public function testStateText() { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $region = $this->getMockBuilder('Oro\Bundle\AddressBundle\Entity\Region') ->disableOriginalConstructor() ->getMock(); @@ -147,7 +147,7 @@ public function testIsStateValidNoCountry() $context->expects($this->never()) ->method('addViolationAt'); - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $obj->isStateValid($context); } @@ -166,7 +166,7 @@ public function testIsStateValidNoRegion() $context->expects($this->never()) ->method('addViolationAt'); - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $obj->setCountry($country); $obj->isStateValid($context); } @@ -197,14 +197,14 @@ public function testIsStateValid() array('%country%' => 'Country') ); - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $obj->setCountry($country); $obj->isStateValid($context); } public function testIsEmpty() { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); $this->assertTrue($obj->isEmpty()); } @@ -215,7 +215,7 @@ public function testIsEmpty() */ public function testIsNotEmpty($property, $value) { - $obj = $this->createAbstractAddress(); + $obj = $this->createAddress(); call_user_func_array(array($obj, 'set' . ucfirst($property)), array($value)); $this->assertFalse($obj->isEmpty()); } @@ -244,34 +244,46 @@ public function emptyCheckPropertiesDataProvider() ); } - public function testIsNotEmptyFlexible() + /** + * @dataProvider isEqualDataProvider + * + * @param AbstractAddress $one + * @param mixed $two + * @param bool $expectedResult + */ + public function testIsEqual(AbstractAddress $one, $two, $expectedResult) { - $value = $this->getMock('Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityFlexibleValue'); - $value->expects($this->once()) - ->method('getData') - ->will($this->returnValue('not empty')); - - $obj = $this->createAbstractAddress(); - $obj->addValue($value); - $this->assertFalse($obj->isEmpty()); + $this->assertEquals($expectedResult, $one->isEqual($two)); } - public function testIsEmptyFlexible() + /** + * @return array + */ + public function isEqualDataProvider() { - $value = $this->getMock('Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityFlexibleValue'); - $value->expects($this->once()) - ->method('getData'); + $one = $this->createAddress(); - $obj = $this->createAbstractAddress(); - $obj->addValue($value); - $this->assertTrue($obj->isEmpty()); + return array( + array($one, $one, true), + array($this->createAddress(100), $this->createAddress(100), true), + array($this->createAddress(), $this->createAddress(), false), + array($this->createAddress(100), $this->createAddress(), false), + array($this->createAddress(), null, false), + ); } /** + * @param int|null $id * @return AbstractAddress|\PHPUnit_Framework_MockObject_MockObject */ - protected function createAbstractAddress() + protected function createAddress($id = null) { - return $this->getMockForAbstractClass('Oro\Bundle\AddressBundle\Entity\AbstractAddress'); + $result = $this->getMockForAbstractClass('Oro\Bundle\AddressBundle\Entity\AbstractAddress'); + + if (null !== $id) { + $result->setId($id); + } + + return $result; } } diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractEmailTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractEmailTest.php new file mode 100644 index 00000000000..98c20e5dabe --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractEmailTest.php @@ -0,0 +1,77 @@ +email = $this->createEmail(); + } + + protected function tearDown() + { + unset($this->email); + } + + public function testConstructor() + { + $this->email = $this->createEmail('email@example.com'); + + $this->assertEquals('email@example.com', $this->email->getEmail()); + } + + public function testId() + { + $this->assertNull($this->email->getId()); + $this->email->setId(100); + $this->assertEquals(100, $this->email->getId()); + } + + public function testEmail() + { + $this->assertNull($this->email->getEmail()); + $this->email->setEmail('email@example.com'); + $this->assertEquals('email@example.com', $this->email->getEmail()); + } + + public function testToString() + { + $this->assertEquals('', (string)$this->email); + $this->email->setEmail('email@example.com'); + $this->assertEquals('email@example.com', (string)$this->email); + } + + public function testPrimary() + { + $this->assertFalse($this->email->isPrimary()); + $this->email->setPrimary(true); + $this->assertTrue($this->email->isPrimary()); + } + + public function testIsEmpty() + { + $this->assertTrue($this->createEmail()->isEmpty()); + $this->assertFalse($this->createEmail('foo@example.com')->isEmpty()); + } + + /** + * @param string|null $email + * @return AbstractEmail|\PHPUnit_Framework_MockObject_MockObject + */ + protected function createEmail($email = null) + { + $arguments = array(); + if ($email) { + $arguments[] = $email; + } + return $this->getMockForAbstractClass('Oro\Bundle\AddressBundle\Entity\AbstractEmail', $arguments); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractPhoneTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractPhoneTest.php new file mode 100644 index 00000000000..f34db5ddd3d --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractPhoneTest.php @@ -0,0 +1,77 @@ +phone = $this->createPhone(); + } + + protected function tearDown() + { + unset($this->phone); + } + + public function testConstructor() + { + $this->phone = $this->createPhone('080011223355'); + + $this->assertEquals('080011223355', $this->phone->getPhone()); + } + + public function testId() + { + $this->assertNull($this->phone->getId()); + $this->phone->setId(100); + $this->assertEquals(100, $this->phone->getId()); + } + + public function testPhone() + { + $this->assertNull($this->phone->getPhone()); + $this->phone->setPhone('080011223355'); + $this->assertEquals('080011223355', $this->phone->getPhone()); + } + + public function testToString() + { + $this->assertEquals('', (string)$this->phone); + $this->phone->setPhone('080011223355'); + $this->assertEquals('080011223355', (string)$this->phone); + } + + public function testPrimary() + { + $this->assertFalse($this->phone->isPrimary()); + $this->phone->setPrimary(true); + $this->assertTrue($this->phone->isPrimary()); + } + + public function testIsEmpty() + { + $this->assertTrue($this->createPhone()->isEmpty()); + $this->assertFalse($this->createPhone('00110011')->isEmpty()); + } + + /** + * @param string|null $phone + * @return AbstractPhone|\PHPUnit_Framework_MockObject_MockObject + */ + protected function createPhone($phone = null) + { + $arguments = array(); + if ($phone) { + $arguments[] = $phone; + } + return $this->getMockForAbstractClass('Oro\Bundle\AddressBundle\Entity\AbstractPhone', $arguments); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractTypedAddressTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractTypedAddressTest.php index 9121f36146e..ea30d1a29e1 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractTypedAddressTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AbstractTypedAddressTest.php @@ -51,6 +51,21 @@ public function testGetTypeNames() $this->assertEquals(array('billing', 'shipping'), $this->address->getTypeNames()); } + public function testGetTypeLabels() + { + $this->assertEquals(array(), $this->address->getTypeLabels()); + + $billing = new AddressType('billing'); + $billing->setLabel('Billing'); + $this->address->addType($billing); + + $shipping = new AddressType('shipping'); + $shipping->setLabel('Shipping'); + $this->address->addType($shipping); + + $this->assertEquals(array('Billing', 'Shipping'), $this->address->getTypeLabels()); + } + public function testGetTypeByName() { $addressType = new AddressType('billing'); @@ -68,6 +83,15 @@ public function testHasTypeWithName() $this->assertFalse($this->address->hasTypeWithName('shipping')); } + public function testPrimary() + { + $this->assertFalse($this->address->isPrimary()); + + $this->address->setPrimary(true); + + $this->assertTrue($this->address->isPrimary()); + } + public function testRemoveType() { $type = new AddressType('testAddressType'); @@ -77,4 +101,14 @@ public function testRemoveType() $this->address->removeType($type); $this->assertEmpty($this->address->getTypes()->toArray()); } + + public function testIsEmpty() + { + $this->assertTrue($this->address->isEmpty()); + $this->address->setPrimary(true); + $this->assertFalse($this->address->isEmpty()); + $this->address->setPrimary(false); + $this->address->addType(new AddressType('billing')); + $this->assertFalse($this->address->isEmpty()); + } } diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AddressTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AddressTypeTest.php index 2ef7c2a70bd..145c36767ef 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AddressTypeTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/AddressTypeTest.php @@ -7,30 +7,44 @@ class AddressTypeTest extends \PHPUnit_Framework_TestCase { /** - * @dataProvider provider - * @param string $property + * @var AddressType */ - public function testSettersAndGetters($property) + protected $type; + + protected function setUp() + { + $this->type = new AddressType('billing'); + } + + public function testName() { - $name = 'testName'; - $obj = new AddressType($name); + $this->assertEquals('billing', $this->type->getName()); + } + + public function testLabel() + { + $this->assertNull($this->type->getLabel()); - call_user_func_array(array($obj, 'set' . ucfirst($property)), array($name)); - $this->assertEquals($name, call_user_func_array(array($obj, 'get' . ucfirst($property)), array())); + $this->type->setLabel('Billing'); - $this->assertEquals($name, $obj->getName()); + $this->assertEquals('Billing', $this->type->getLabel()); } - /** - * Data provider - * - * @return array - */ - public function provider() + public function testLocale() + { + $this->assertNull($this->type->getLocale()); + + $this->type->setLocale('en'); + + $this->assertEquals('en', $this->type->getLocale()); + } + + public function testToString() { - return array( - array('label'), - array('locale') - ); + $this->assertEquals('', $this->type); + + $this->type->setLabel('Shipping'); + + $this->assertEquals('Shipping', (string)$this->type); } } diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/Manager/AddressManagerTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/Manager/AddressManagerTest.php index e8c3bc1bd5f..463a20998ed 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/Manager/AddressManagerTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Entity/Manager/AddressManagerTest.php @@ -4,25 +4,18 @@ use Oro\Bundle\AddressBundle\Entity\Manager\AddressManager; use Oro\Bundle\AddressBundle\Entity\Address; -use Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager; -use Doctrine\Common\Persistence\ObjectManager; class AddressManagerTest extends \PHPUnit_Framework_TestCase { /** - * @var ObjectManager + * @var \PHPUnit_Framework_MockObject_MockObject */ protected $om; - /** - * @var FlexibleManager - */ - protected $fm; - /** * @var string */ - protected $class; + protected $class = 'Oro\Bundle\AddressBundle\Entity\Address'; /** * @var AddressManager @@ -35,10 +28,6 @@ class AddressManagerTest extends \PHPUnit_Framework_TestCase public function setUp() { $this->om = $this->getMock('Doctrine\Common\Persistence\ObjectManager'); - $this->fm = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager') - ->disableOriginalConstructor() - ->getMock(); - $this->class = 'Oro\Bundle\AddressBundle\Entity\Address'; $classMetaData = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') ->disableOriginalConstructor() @@ -55,7 +44,7 @@ public function setUp() ->with($this->equalTo($this->class)) ->will($this->returnValue($classMetaData)); - $this->addressManager = new AddressManager($this->class, $this->om, $this->fm); + $this->addressManager = new AddressManager($this->class, $this->om); } /** @@ -126,54 +115,6 @@ public function testQueryMethods() $this->assertEquals($this->addressManager->findAddressBy($addressCriteria), $address); } - /** - * Test magic call methods for existing flexible manager methods - */ - public function testCall() - { - $this->fm - ->expects($this->once()) - ->method('getFlexibleName') - ->will($this->returnValue(1)); - - $this->assertEquals($this->addressManager->getFlexibleName(), 1); - } - - /** - * Testing exception on not existing method in address manager - * - * @expectedException \RuntimeException - */ - public function testCallException() - { - $this->addressManager->NotExistingMethod(); - } - - public function testListQuery() - { - $limit = 1; - $offset = 10; - $paginator = $this->getMockBuilder('Doctrine\ORM\Tools\Pagination\Paginator') - ->disableOriginalConstructor() - ->getMock(); - - $repo = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Entity\Repository\FlexibleEntityRepository') - ->disableOriginalConstructor() - ->getMock(); - - $repo->expects($this->once()) - ->method('findByWithAttributesQB') - ->with(array(), null, array('id' => 'ASC'), $limit, $offset) - ->will($this->returnValue($paginator)); - - $this->fm->expects($this->once()) - ->method('getFlexibleRepository') - ->will($this->returnValue($repo)); - - - $this->assertSame($paginator, $this->addressManager->getListQuery($limit, $offset)); - } - /** * Return repository mock * diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/AddressCollectionTypeSubscriberTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/AddressCollectionTypeSubscriberTest.php deleted file mode 100644 index 517edb9c7b0..00000000000 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/AddressCollectionTypeSubscriberTest.php +++ /dev/null @@ -1,215 +0,0 @@ -typedAddressClass = $this->getMockClass('Oro\Bundle\AddressBundle\Entity\AbstractTypedAddress'); - $this->subscriber = new AddressCollectionTypeSubscriber('test', $this->typedAddressClass); - } - - public function testGetSubscribedEvents() - { - $result = $this->subscriber->getSubscribedEvents(); - - $this->assertInternalType('array', $result); - $this->assertArrayHasKey(FormEvents::PRE_SET_DATA, $result); - $this->assertArrayHasKey(FormEvents::PRE_BIND, $result); - $this->assertArrayHasKey(FormEvents::POST_BIND, $result); - } - - public function testPreSetNotEmpty() - { - $addresses = $this->getMockBuilder('Doctrine\Common\Collections\Collection') - ->disableOriginalConstructor() - ->getMock(); - $addresses->expects($this->once()) - ->method('isEmpty') - ->will($this->returnValue(false)); - $addresses->expects($this->never()) - ->method('add'); - $this->subscriber->preSet($this->getEvent($addresses)); - } - - public function testPreSetEmpty() - { - $addresses = $this->getMockBuilder('Doctrine\Common\Collections\Collection') - ->disableOriginalConstructor() - ->getMock(); - $addresses->expects($this->once()) - ->method('isEmpty') - ->will($this->returnValue(true)); - - $this->subscriber->preSet( - $this->getEvent( - $addresses, - array($this->getMock('Oro\Bundle\AddressBundle\Entity\AbstractTypedAddress')) - ) - ); - } - - public function testPostBind() - { - $addressEmpty = $this->getMock('Oro\Bundle\AddressBundle\Entity\AbstractTypedAddress'); - $addressEmpty->expects($this->once()) - ->method('isEmpty') - ->will($this->returnValue(true)); - $addressNotEmpty = $this->getMock('Oro\Bundle\AddressBundle\Entity\AbstractTypedAddress'); - $addressNotEmpty->expects($this->once()) - ->method('isEmpty') - ->will($this->returnValue(false)); - - $addresses = new \Doctrine\Common\Collections\ArrayCollection(array($addressEmpty, $addressNotEmpty)); - $this->subscriber->postBind( - $this->getEvent( - $addresses, - new \Doctrine\Common\Collections\ArrayCollection(array(1 => $addressNotEmpty)) - ) - ); - } - - protected function getEvent($expectedGetDataValue, $expectedSetDataValue = null) - { - $data = $this->getMockBuilder('\stdClass') - ->setMethods(array('getTest', 'setTest')) - ->getMock(); - - $data->expects($this->once()) - ->method('getTest') - ->will($this->returnValue($expectedGetDataValue)); - - if (null !== $expectedSetDataValue) { - $data->expects($this->once()) - ->method('setTest') - ->with($expectedSetDataValue); - } - - $event = $this->getMockBuilder('Symfony\Component\Form\FormEvent') - ->disableOriginalConstructor() - ->getMock(); - $event->expects($this->once()) - ->method('getData') - ->will($this->returnValue($data)); - - return $event; - } - - /** - * @dataProvider noDataPreBindDataProvider - * @param array|null $data - */ - public function testPreBindNoData($data) - { - $event = $this->getMockBuilder('Symfony\Component\Form\FormEvent') - ->disableOriginalConstructor() - ->getMock(); - $event->expects($this->once()) - ->method('getData') - ->will($this->returnValue($data)); - $event->expects($this->never()) - ->method('setData'); - $this->subscriber->preBind($event); - } - - /** - * @return array - */ - public function noDataPreBindDataProvider() - { - return array( - array( - null, array() - ), - array( - array(), array() - ) - ); - } - - /** - * @dataProvider preBindDataProvider - * @param array|null $data - * @param array $expected - */ - public function testPreBind($data, $expected) - { - $event = $this->getMockBuilder('Symfony\Component\Form\FormEvent') - ->disableOriginalConstructor() - ->getMock(); - $event->expects($this->once()) - ->method('getData') - ->will($this->returnValue($data)); - $event->expects($this->once()) - ->method('setData') - ->with($expected); - $this->subscriber->preBind($event); - } - - public function preBindDataProvider() - { - return array( - array( - array('key' => 'value', 'test' => array(array(), array('k' => 'v'))), - array('key' => 'value', 'test' => array(array('k' => 'v', 'primary' => true))) - ), - array( - array('key' => 'value', 'test' => array(array(array()), array('k' => 'v'))), - array('key' => 'value', 'test' => array(array('k' => 'v', 'primary' => true))) - ), - array( - array('key' => 'value', 'test' => array(array(array('k2' => 'v')), array('k' => 'v'))), - array('key' => 'value', 'test' => array(array(array('k2' => 'v'), 'primary' => true), array('k' => 'v'))) - ), - ); - } - - /** - * @dataProvider preBindNoResetDataProvider - * @param array $data - */ - public function testPreBindNoReset($data) - { - $event = $this->getMockBuilder('Symfony\Component\Form\FormEvent') - ->disableOriginalConstructor() - ->getMock(); - $event->expects($this->once()) - ->method('getData') - ->will($this->returnValue($data)); - $event->expects($this->never()) - ->method('setData'); - $this->subscriber->preBind($event); - } - - /** - * @return array - */ - public function preBindNoResetDataProvider() - { - return array( - array( - array('key' => 'value') - ), - array( - array('key' => 'value', 'test' => array()) - ) - ); - } -} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/BuildAddressFormListenerTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/AddressCountryAndRegionSubscriberTest.php similarity index 91% rename from src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/BuildAddressFormListenerTest.php rename to src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/AddressCountryAndRegionSubscriberTest.php index 861e69c1425..a31bdcf963b 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/BuildAddressFormListenerTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/AddressCountryAndRegionSubscriberTest.php @@ -2,10 +2,10 @@ namespace Oro\Bundle\AddressBundle\Tests\Unit\EventListener; -use Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener; +use Oro\Bundle\AddressBundle\Form\EventListener\AddressCountryAndRegionSubscriber; use Symfony\Component\Form\FormEvents; -class BuildAddressFormListenerTest extends \PHPUnit_Framework_TestCase +class AddressCountryAndRegionSubscriberTest extends \PHPUnit_Framework_TestCase { /** @var \Doctrine\Common\Persistence\ObjectManager */ protected $om; @@ -14,9 +14,9 @@ class BuildAddressFormListenerTest extends \PHPUnit_Framework_TestCase protected $formBuilder; /** - * @var BuildAddressFormListener + * @var AddressCountryAndRegionSubscriber */ - protected $listener; + protected $subscriber; /** * SetUp test environment @@ -26,12 +26,12 @@ public function setUp() $this->om = $this->getMock('Doctrine\Common\Persistence\ObjectManager'); $this->formBuilder = $this->getMock('Symfony\Component\Form\FormFactoryInterface'); - $this->listener = new BuildAddressFormListener($this->om, $this->formBuilder); + $this->subscriber = new AddressCountryAndRegionSubscriber($this->om, $this->formBuilder); } public function testGetSubscribedEvents() { - $result = $this->listener->getSubscribedEvents(); + $result = $this->subscriber->getSubscribedEvents(); $this->assertInternalType('array', $result); $this->assertArrayHasKey(FormEvents::PRE_SET_DATA, $result); @@ -50,7 +50,7 @@ public function testPreSetDataEmptyAddress() $eventMock->expects($this->once()) ->method('getForm'); - $this->assertEquals(null, $this->listener->preSetData($eventMock)); + $this->assertEquals(null, $this->subscriber->preSetData($eventMock)); } public function testPreSetDataEmptyCountry() @@ -70,7 +70,7 @@ public function testPreSetDataEmptyCountry() $eventMock->expects($this->once()) ->method('getForm'); - $this->assertEquals(null, $this->listener->preSetData($eventMock)); + $this->assertEquals(null, $this->subscriber->preSetData($eventMock)); } public function testPreSetDataHasState() @@ -133,7 +133,7 @@ public function testPreSetDataHasState() ->method('getForm') ->will($this->returnValue($formMock)); - $this->assertNull($this->listener->preSetData($eventMock)); + $this->assertNull($this->subscriber->preSetData($eventMock)); } public function testPreSetDataNoState() @@ -181,10 +181,10 @@ public function testPreSetDataNoState() ->method('getForm') ->will($this->returnValue($formMock)); - $this->assertNull($this->listener->preSetData($eventMock)); + $this->assertNull($this->subscriber->preSetData($eventMock)); } - public function testPreBindData() + public function testPreSubmitData() { $eventMock = $this->getMockBuilder('Symfony\Component\Form\FormEvent') ->disableOriginalConstructor() @@ -243,6 +243,6 @@ public function testPreBindData() ->method('getForm') ->will($this->returnValue($formMock)); - $this->assertEquals(null, $this->listener->preBind($eventMock)); + $this->assertEquals(null, $this->subscriber->preSubmit($eventMock)); } } diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/FixAddressesPrimaryAndTypesSubscriberTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/FixAddressesPrimaryAndTypesSubscriberTest.php new file mode 100644 index 00000000000..90535b0251f --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/FixAddressesPrimaryAndTypesSubscriberTest.php @@ -0,0 +1,33 @@ +subscriber = new FixAddressesPrimaryAndTypesSubscriber('owner.address'); + } + + + public function testGetSubscribedEvents() + { + $this->assertEquals( + array(FormEvents::POST_SUBMIT => 'postSubmit'), + $this->subscriber->getSubscribedEvents() + ); + } + + public function testPostSubmit() + { + $this->markTestIncomplete('Implement tests for primary and types'); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/ItemCollectionTypeSubscriberTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/ItemCollectionTypeSubscriberTest.php new file mode 100644 index 00000000000..88c5a984781 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/EventListener/ItemCollectionTypeSubscriberTest.php @@ -0,0 +1,211 @@ +typedAddressClass = $this->getMockClass('Oro\Bundle\AddressBundle\Entity\AbstractTypedAddress'); + $this->subscriber = new ItemCollectionTypeSubscriber(); + } + + public function testGetSubscribedEvents() + { + $result = $this->subscriber->getSubscribedEvents(); + + $this->assertInternalType('array', $result); + $this->assertArrayHasKey(FormEvents::POST_SUBMIT, $result); + $this->assertArrayHasKey(FormEvents::PRE_SUBMIT, $result); + } + + public function testPostSubmit() + { + $itemEmpty = $this->getMock('Oro\Bundle\AddressBundle\Entity\EmptyItem'); + $itemEmpty->expects($this->once()) + ->method('isEmpty') + ->will($this->returnValue(true)); + $itemNotEmpty = $this->getMock('Oro\Bundle\AddressBundle\Entity\EmptyItem'); + $itemNotEmpty->expects($this->once()) + ->method('isEmpty') + ->will($this->returnValue(false)); + $itemNotEmptyType = $this->getMock('SomeClass'); + $itemNotEmptyType->expects($this->never())->method($this->anything()); + + $data = new ArrayCollection(array($itemEmpty, $itemNotEmpty, $itemNotEmptyType)); + $this->subscriber->postSubmit($this->createEvent($data)); + + $this->assertEquals( + array( + 1 => $itemNotEmpty, + 2 => $itemNotEmptyType + ), + $data->toArray() + ); + } + + public function testPostSubmitNotCollectionData() + { + $data = $this->getMock('SomeClass'); + $data->expects($this->never())->method($this->anything()); + + $this->subscriber->postSubmit($this->createEvent($data)); + } + + /** + * @dataProvider preSubmitNoDataDataProvider + * @param array|null $data + */ + public function testPreSubmitNoData($data) + { + $event = $this->getMockBuilder('Symfony\Component\Form\FormEvent') + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->once()) + ->method('getData') + ->will($this->returnValue($data)); + $event->expects($this->never()) + ->method('setData'); + + $this->subscriber->preSubmit($event); + } + + /** + * @return array + */ + public function preSubmitNoDataDataProvider() + { + return array( + array( + null, array() + ), + array( + array(), array() + ) + ); + } + + /** + * @dataProvider preSubmitDataProvider + * + * @param array $data + * @param array $expected + * @param bool $checkIsNew + * @param mixed $parentDataId + */ + public function testPreSubmit(array $data, array $expected, $checkIsNew = false, $parentDataId = null) + { + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + if ($checkIsNew) { + $parentForm = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $parentFormData = $this->getMock('SomeClass', array('getId')); + + $form->expects($this->once())->method('getParent') + ->will($this->returnValue($parentForm)); + + $parentForm->expects($this->once())->method('getData') + ->will($this->returnValue($parentFormData)); + + $parentFormData->expects($this->once())->method('getId') + ->will($this->returnValue($parentDataId)); + } else { + $form->expects($this->never())->method($this->anything()); + } + + $event = $this->createEvent($data, $form); + $this->subscriber->preSubmit($event); + $this->assertEquals($expected, $event->getData()); + } + + public function preSubmitDataProvider() + { + return array( + 'set_primary_for_new_data' => array( + 'data' => array(array('k' => 'v')), + 'expected' => array(array('k' => 'v', 'primary' => true)), + 'check_is_new' => true, + 'parent_data_id' => null + ), + 'set_primary_for_one_item' => array( + 'data' => array(array('k' => 'v')), + 'expected' => array(array('k' => 'v', 'primary' => true)), + 'check_is_new' => true, + 'parent_data_id' => 1 + ), + 'not_set_primary_for_not_new_data' => array( + 'data' => array(array('k' => 'v'), array('k2' => 'v2')), + 'expected' => array(array('k' => 'v'), array('k2' => 'v2')), + 'check_is_new' => true, + 'parent_data_id' => 1 + ), + 'primary_is_already_set' => array( + 'data' => array(array('primary' => true), array(array('k' => 'v'))), + 'expected' => array(array('primary' => true), array(array('k' => 'v'))) + ), + 'skip_empty_data_array' => array( + 'data' => array(array(array()), array(), array('k' => 'v', 'primary' => true), array()), + 'expected' => array('2' => array('k' => 'v', 'primary' => true)) + ) + ); + } + + /** + * @dataProvider preSubmitNoResetDataProvider + * @param array $data + */ + public function testPreSubmitNoReset($data) + { + $event = $this->getMockBuilder('Symfony\Component\Form\FormEvent') + ->disableOriginalConstructor() + ->getMock(); + $event->expects($this->once()) + ->method('getData') + ->will($this->returnValue($data)); + $event->expects($this->never()) + ->method('setData'); + $this->subscriber->preSubmit($event); + } + + /** + * @return array + */ + public function preSubmitNoResetDataProvider() + { + return array( + array(array()), + array('foo') + ); + } + + /** + * @param mixed $data + * @param FormInterface|null $form + * @return FormEvent + */ + protected function createEvent($data, FormInterface $form = null) + { + $form = $form ? $form : $this->getMock('Symfony\Component\Form\Test\FormInterface'); + return new FormEvent($form, $data); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractAddressTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractAddressTypeTest.php deleted file mode 100644 index 49c248cf30c..00000000000 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractAddressTypeTest.php +++ /dev/null @@ -1,127 +0,0 @@ -getMockBuilder('Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener') - ->disableOriginalConstructor() - ->getMock(); - $flexibleManager = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager') - ->disableOriginalConstructor() - ->getMock(); - - $this->type = $this->createTestAddress($flexibleManager, 'oro_address_value', $buildAddressFormListener); - } - - protected function createTestAddress( - FlexibleManager $flexibleManager, - $valueFormAlias, - BuildAddressFormListener $eventListener - ) { - return $this->getMockForAbstractClass( - 'Oro\Bundle\AddressBundle\Form\Type\AbstractAddressType', - array($flexibleManager, $valueFormAlias, $eventListener) - ); - } - - public function testAddEntityFields() - { - $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') - ->disableOriginalConstructor() - ->getMock(); - - $builder->expects($this->exactly(11)) - ->method('add') - ->will($this->returnSelf()); - - $builder->expects($this->at(0)) - ->method('add') - ->with('id', 'hidden'); - - $builder->expects($this->at(1)) - ->method('addEventSubscriber') - ->with($this->isInstanceOf('Symfony\Component\EventDispatcher\EventSubscriberInterface')); - - $builder->expects($this->at(2)) - ->method('add') - ->with('label', 'text'); - - $builder->expects($this->at(3)) - ->method('add') - ->with('firstName', 'text'); - - $builder->expects($this->at(4)) - ->method('add') - ->with('lastName', 'text'); - - $builder->expects($this->at(5)) - ->method('add') - ->with('street', 'text'); - - $builder->expects($this->at(6)) - ->method('add') - ->with('street2', 'text'); - - $builder->expects($this->at(7)) - ->method('add') - ->with('city', 'text'); - - $builder->expects($this->at(8)) - ->method('add') - ->with('state', 'oro_region'); - - $builder->expects($this->at(9)) - ->method('add') - ->with('state_text', 'hidden'); - - $builder->expects($this->at(10)) - ->method('add') - ->with('country', 'oro_country'); - - $builder->expects($this->at(11)) - ->method('add') - ->with('postalCode', 'text'); - - $builder->expects($this->once()) - ->method('addEventSubscriber') - ->with($this->isInstanceOf('Symfony\Component\EventDispatcher\EventSubscriberInterface')); - - $this->type->addEntityFields($builder); - } - - public function testSetDefaultOptions() - { - $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); - $resolver->expects($this->once()) - ->method('setDefaults') - ->with($this->isType('array')); - - $this->type->setDefaultOptions($resolver); - } - - public function testGetName() - { - $this->setExpectedException( - 'BadMethodCallException', - sprintf('Method %s::getName() must be overridden.', get_class($this->type)) - ); - $this->type->getName(); - } -} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractTypedAddressTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractTypedAddressTypeTest.php deleted file mode 100644 index d99b7bba924..00000000000 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AbstractTypedAddressTypeTest.php +++ /dev/null @@ -1,71 +0,0 @@ -getMockBuilder('Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener') - ->disableOriginalConstructor() - ->getMock(); - $flexibleManager = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager') - ->disableOriginalConstructor() - ->getMock(); - - $this->type = $this->getMockForAbstractClass( - 'Oro\Bundle\AddressBundle\Form\Type\AbstractTypedAddressType', - array( - $flexibleManager, - 'oro_address_value', - $buildAddressFormListener - ) - ); - } - - public function testAddEntityFields() - { - $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') - ->disableOriginalConstructor() - ->getMock(); - $builder->expects($this->any()) - ->method('add') - ->will($this->returnSelf()); - $builder->expects($this->at(0)) - ->method('add') - ->with( - 'types', - 'translatable_entity', - $this->callback( - function ($options) { - \PHPUnit_Framework_TestCase::assertArrayHasKey('class', $options); - \PHPUnit_Framework_TestCase::assertArrayHasKey('property', $options); - \PHPUnit_Framework_TestCase::assertEquals('OroAddressBundle:AddressType', $options['class']); - \PHPUnit_Framework_TestCase::assertEquals('label', $options['property']); - return true; - } - ) - ); - - $builder->expects($this->at(1)) - ->method('add') - ->with( - 'primary', - 'checkbox', - $this->isType('array') - ); - - $this->type->addEntityFields($builder); - } -} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressApiTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressApiTypeTest.php index 7d269a05399..ba8bccaec07 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressApiTypeTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressApiTypeTest.php @@ -20,45 +20,32 @@ class AddressApiTypeTest extends \PHPUnit_Framework_TestCase */ public function setUp() { - $buildAddressFormListener = $this->getMockBuilder('Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener') - ->disableOriginalConstructor() - ->getMock(); - $flexibleManager = $this->getMockBuilder('Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager') - ->disableOriginalConstructor() - ->getMock(); - - $this->type = new AddressApiType( - $flexibleManager, - 'oro_address_value', - $buildAddressFormListener - ); + $this->type = new AddressApiType(); } - public function testAddEntityFields() + public function testBuildForm() { - /** @var \Symfony\Component\Form\FormBuilderInterface $builder */ $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') ->disableOriginalConstructor() ->getMock(); - $builder->expects($this->exactly(11)) - ->method('add') - ->will($this->returnSelf()); - - $builder->expects($this->exactly(2)) + $builder->expects($this->once()) ->method('addEventSubscriber') ->with($this->isInstanceOf('Symfony\Component\EventDispatcher\EventSubscriberInterface')); - $this->type->addEntityFields($builder); + $this->type->buildForm($builder, array()); } public function testSetDefaultOptions() { - /** @var OptionsResolverInterface $resolver */ $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); $resolver->expects($this->once()) ->method('setDefaults') - ->with($this->isType('array')); + ->with( + array( + 'csrf_protection' => false, + ) + ); $this->type->setDefaultOptions($resolver); } @@ -66,4 +53,9 @@ public function testGetName() { $this->assertEquals('address', $this->type->getName()); } + + public function testGetParent() + { + $this->assertEquals('oro_address', $this->type->getParent()); + } } diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressCollectionTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressCollectionTypeTest.php index 9c527548c29..08cb573e3d1 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressCollectionTypeTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressCollectionTypeTest.php @@ -1,8 +1,6 @@ type = new AddressCollectionType(); } - public function testSetDefaultOptions() - { - /** @var OptionsResolverInterface $resolver */ - $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); - $resolver->expects($this->once()) - ->method('setDefaults') - ->with($this->isType('array')); - $this->type->setDefaultOptions($resolver); - } - public function testGetParent() { - $this->assertEquals('collection', $this->type->getParent()); + $this->assertEquals('oro_item_collection', $this->type->getParent()); } public function testGetName() diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressTypeTest.php index 2c02a7cceb5..d744a5292c6 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressTypeTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/AddressTypeTest.php @@ -2,22 +2,96 @@ namespace Oro\Bundle\AddressBundle\Tests\Unit\Form\Type; use Oro\Bundle\AddressBundle\Form\Type\AddressType; -use Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager; -use Oro\Bundle\AddressBundle\Form\EventListener\BuildAddressFormListener; -class AddressTypeTest extends AbstractAddressTypeTest +class AddressTypeTest extends \PHPUnit_Framework_TestCase { /** - * @var AbstractAddressTypeTest + * @var AddressType */ protected $type; - protected function createTestAddress( - FlexibleManager $flexibleManager, - $valueFormAlias, - BuildAddressFormListener $eventListener - ) { - return new AddressType($flexibleManager, $valueFormAlias, $eventListener); + /** + * Setup test env + */ + public function setUp() + { + $buildAddressFormListener = $this->getMockBuilder( + 'Oro\Bundle\AddressBundle\Form\EventListener\AddressCountryAndRegionSubscriber' + )->disableOriginalConstructor()->getMock(); + + $this->type = new AddressType($buildAddressFormListener); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $builder->expects($this->exactly(11)) + ->method('add') + ->will($this->returnSelf()); + + $builder->expects($this->at(0)) + ->method('addEventSubscriber') + ->with($this->isInstanceOf('Symfony\Component\EventDispatcher\EventSubscriberInterface')) + ->will($this->returnSelf()); + + $builder->expects($this->at(1)) + ->method('add') + ->with('id', 'hidden'); + + $builder->expects($this->at(2)) + ->method('add') + ->with('label', 'text'); + + $builder->expects($this->at(3)) + ->method('add') + ->with('firstName', 'text'); + + $builder->expects($this->at(4)) + ->method('add') + ->with('lastName', 'text'); + + $builder->expects($this->at(5)) + ->method('add') + ->with('street', 'text'); + + $builder->expects($this->at(6)) + ->method('add') + ->with('street2', 'text'); + + $builder->expects($this->at(7)) + ->method('add') + ->with('city', 'text'); + + $builder->expects($this->at(8)) + ->method('add') + ->with('state', 'oro_region'); + + $builder->expects($this->at(9)) + ->method('add') + ->with('state_text', 'hidden'); + + $builder->expects($this->at(10)) + ->method('add') + ->with('country', 'oro_country'); + + $builder->expects($this->at(11)) + ->method('add') + ->with('postalCode', 'text'); + + $this->type->buildForm($builder, array()); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + + $this->type->setDefaultOptions($resolver); } public function testGetName() diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/EmailCollectionTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/EmailCollectionTypeTest.php new file mode 100644 index 00000000000..132006651a8 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/EmailCollectionTypeTest.php @@ -0,0 +1,30 @@ +type = new EmailCollectionType(); + } + + public function testGetParent() + { + $this->assertEquals('oro_item_collection', $this->type->getParent()); + } + + public function testGetName() + { + $this->assertEquals('oro_email_collection', $this->type->getName()); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/EmailTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/EmailTypeTest.php new file mode 100644 index 00000000000..965b67997ae --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/EmailTypeTest.php @@ -0,0 +1,47 @@ +type = new EmailType(); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $builder->expects($this->exactly(3)) + ->method('add') + ->will($this->returnSelf()); + + $builder->expects($this->at(0)) + ->method('add') + ->with('id', 'hidden'); + + $builder->expects($this->at(1)) + ->method('add') + ->with('email', 'email'); + + $builder->expects($this->at(2)) + ->method('add') + ->with('primary', 'radio'); + + $this->type->buildForm($builder, array()); + } + + public function testGetName() + { + $this->assertEquals('oro_email', $this->type->getName()); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/ItemCollectionTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/ItemCollectionTypeTest.php new file mode 100644 index 00000000000..d8c41208386 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/ItemCollectionTypeTest.php @@ -0,0 +1,66 @@ +type = new ItemCollectionType(); + } + + public function testBuildForm() + { + $builder = $this->getMock('Symfony\Component\Form\Test\FormBuilderInterface'); + + $builder->expects($this->once()) + ->method('addEventSubscriber') + ->with($this->isInstanceOf('Oro\Bundle\AddressBundle\Form\EventListener\ItemCollectionTypeSubscriber')); + + $options = array(); + $this->type->buildForm($builder, $options); + } + + public function testBuildView() + { + $form = $this->getMock('Symfony\Component\Form\Test\FormInterface'); + $view = new FormView(); + + $options = array( + 'show_form_when_empty' => true + ); + $this->type->buildView($view, $form, $options); + + $this->assertArrayHasKey('show_form_when_empty', $view->vars); + $this->assertEquals($options['show_form_when_empty'], $view->vars['show_form_when_empty']); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + $this->type->setDefaultOptions($resolver); + } + + public function testGetParent() + { + $this->assertEquals('collection', $this->type->getParent()); + } + + public function testGetName() + { + $this->assertEquals('oro_item_collection', $this->type->getName()); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/PhoneCollectionTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/PhoneCollectionTypeTest.php new file mode 100644 index 00000000000..eb2f0d19a0c --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/PhoneCollectionTypeTest.php @@ -0,0 +1,30 @@ +type = new PhoneCollectionType(); + } + + public function testGetParent() + { + $this->assertEquals('oro_item_collection', $this->type->getParent()); + } + + public function testGetName() + { + $this->assertEquals('oro_phone_collection', $this->type->getName()); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/PhoneTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/PhoneTypeTest.php new file mode 100644 index 00000000000..42e95fe1fd8 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/PhoneTypeTest.php @@ -0,0 +1,50 @@ +type = new PhoneType(); + } + + public function testBuildForm() + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $builder->expects($this->exactly(3)) + ->method('add') + ->will($this->returnSelf()); + + $builder->expects($this->at(0)) + ->method('add') + ->with('id', 'hidden'); + + $builder->expects($this->at(1)) + ->method('add') + ->with('phone', 'text'); + + $builder->expects($this->at(2)) + ->method('add') + ->with('primary', 'radio'); + + $this->type->buildForm($builder, array()); + } + + public function testGetName() + { + $this->assertEquals('oro_phone', $this->type->getName()); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/TypedAddressTypeTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/TypedAddressTypeTest.php new file mode 100644 index 00000000000..e5b072150a7 --- /dev/null +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Form/Type/TypedAddressTypeTest.php @@ -0,0 +1,113 @@ +type = new TypedAddressType(); + } + + /** + * @dataProvider buildFormDataProvider + * + * @param array $options + * @param bool $expectAddSubscriber + */ + public function testBuildForm(array $options, $expectAddSubscriber) + { + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') + ->disableOriginalConstructor() + ->getMock(); + + $at = 0; + + if ($expectAddSubscriber) { + $builder->expects($this->at($at++)) + ->method('addEventSubscriber') + ->with( + $this->isInstanceOf( + 'Oro\Bundle\AddressBundle\Form\EventListener\FixAddressesPrimaryAndTypesSubscriber' + ) + ) + ->will($this->returnSelf()); + } + + $builder->expects($this->at($at++)) + ->method('add') + ->with( + 'types', + 'translatable_entity', + array( + 'class' => 'OroAddressBundle:AddressType', + 'property' => 'label', + 'required' => false, + 'multiple' => true, + 'expanded' => true, + ) + ) + ->will($this->returnSelf()); + + $builder->expects($this->at($at++)) + ->method('add') + ->with( + 'primary', + 'checkbox', + array( + 'label' => 'Primary', + 'required' => false + ) + ) + ->will($this->returnSelf()); + + $this->type->buildForm($builder, $options); + } + + public function buildFormDataProvider() + { + return array( + array( + 'options' => array( + 'single_form' => false, + 'all_addresses_property_path' => null, + ), + 'expectAddSubscriber' => false + ), + array( + 'options' => array( + 'single_form' => true, + 'all_addresses_property_path' => null, + ), + 'expectAddSubscriber' => false + ), + array( + 'options' => array( + 'single_form' => true, + 'all_addresses_property_path' => 'owner.addresses', + ), + 'expectAddSubscriber' => true + ) + ); + } + + public function testGetParent() + { + $this->assertEquals('oro_address', $this->type->getParent()); + } + + public function testGetName() + { + $this->assertEquals('oro_typed_address', $this->type->getName()); + } +} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Twig/HasAddressExtensionTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Twig/HasAddressExtensionTest.php deleted file mode 100644 index b30a6db8d04..00000000000 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Twig/HasAddressExtensionTest.php +++ /dev/null @@ -1,112 +0,0 @@ -extension = new HasAddressExtension(); - $this->entityMock = $this->getMock('Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityFlexible'); - $this->valuesMock = $this->getMock('Doctrine\Common\Collections\ArrayCollection'); - $this->valueMock = $this->getMock('Oro\Bundle\FlexibleEntityBundle\Model\FlexibleValueInterface'); - } - - public function testGetName() - { - $this->assertEquals('oro_address_hasAddress', $this->extension->getName()); - } - - public function testGetFilters() - { - $filters = $this->extension->getFilters(); - - $this->assertArrayHasKey('hasAddress', $filters); - $this->assertInstanceOf('\Twig_Filter_Method', $filters['hasAddress']); - } - - public function testHasAddressPositiveScenario() - { - $this->valueMock->expects($this->once()) - ->method('getData') - ->will($this->returnValue(true)); - - $this->entityMock->expects($this->once()) - ->method('getValue') - ->with($this->equalTo('address')) - ->will($this->returnValue($this->valueMock)); - - $this->assertTrue($this->extension->hasAddress($this->entityMock, 'address')); - } - - public function testHasAddressNegativeScenario() - { - $this->valueMock->expects($this->once()) - ->method('getData') - ->will($this->returnValue(null)); - - $this->entityMock->expects($this->once()) - ->method('getValue') - ->with($this->equalTo('address')) - ->will($this->returnValue($this->valueMock)); - - $this->assertFalse($this->extension->hasAddress($this->entityMock, 'address')); - } - - public function testHasAddressPositiveScenarioWithoutCode() - { - $this->entityMock->expects($this->once()) - ->method('getValues') - ->will($this->returnValue($this->valuesMock)); - - $this->valuesMock->expects($this->once()) - ->method('filter') - ->will($this->returnValue($this->valuesMock)); - $this->valuesMock->expects($this->once()) - ->method('isEmpty') - ->will($this->returnValue(false)); - - $this->assertTrue($this->extension->hasAddress($this->entityMock)); - } - - public function testHasAddressNegativeScenarioWithoutCode() - { - $this->entityMock->expects($this->once()) - ->method('getValues') - ->will($this->returnValue($this->valuesMock)); - - $this->valuesMock->expects($this->once()) - ->method('filter') - ->will($this->returnValue($this->valuesMock)); - $this->valuesMock->expects($this->once()) - ->method('isEmpty') - ->will($this->returnValue(true)); - - $this->assertFalse($this->extension->hasAddress($this->entityMock)); - } -} diff --git a/src/Oro/Bundle/AddressBundle/Tests/Unit/Validator/Constraints/ContainsPrimaryValidatorTest.php b/src/Oro/Bundle/AddressBundle/Tests/Unit/Validator/Constraints/ContainsPrimaryValidatorTest.php index a186e03121b..ce475989003 100644 --- a/src/Oro/Bundle/AddressBundle/Tests/Unit/Validator/Constraints/ContainsPrimaryValidatorTest.php +++ b/src/Oro/Bundle/AddressBundle/Tests/Unit/Validator/Constraints/ContainsPrimaryValidatorTest.php @@ -18,10 +18,10 @@ public function testValidateException() } /** - * @dataProvider validAddressesDataProvider - * @param array $addresses + * @dataProvider validItemsDataProvider + * @param array $items */ - public function testValidateValid(array $addresses) + public function testValidateValid(array $items) { $context = $this->getMockBuilder('Symfony\Component\Validator\ExecutionContext') ->disableOriginalConstructor() @@ -33,89 +33,84 @@ public function testValidateValid(array $addresses) $validator = new ContainsPrimaryValidator(); $validator->initialize($context); - $validator->validate($addresses, $constraint); + $validator->validate($items, $constraint); } /** * @return array */ - public function validAddressesDataProvider() + public function validItemsDataProvider() { return array( - 'no addresses' => array( + 'no items' => array( array() ), - 'one address primary' => array( - array($this->getTypedAddressMock(true)) + 'one item primary' => array( + array($this->getPrimaryItemMock(true)) ), - 'more than one address with primary' => array( - array($this->getTypedAddressMock(false), $this->getTypedAddressMock(true)) + 'more than one item with primary' => array( + array($this->getPrimaryItemMock(false), $this->getPrimaryItemMock(true)) ), - 'empty address' => array( - array($this->getTypedAddressMock(false, true), $this->getTypedAddressMock(false, true)) - ), - 'empty address and primary' => array( - array($this->getTypedAddressMock(false, true), $this->getTypedAddressMock(true), $this->getTypedAddressMock(false, true)) + 'empty item and primary' => array( + array($this->getPrimaryItemMock(false, true), $this->getPrimaryItemMock(true), $this->getPrimaryItemMock(false, true)) ) ); } /** - * @dataProvider invalidAddressesDataProvider - * @param array $addresses + * @dataProvider invalidItemsDataProvider + * @param array $items */ - public function testValidateInvalid($addresses) + public function testValidateInvalid($items) { $context = $this->getMockBuilder('Symfony\Component\Validator\ExecutionContext') ->disableOriginalConstructor() ->getMock(); $context->expects($this->once()) ->method('addViolation') - ->with('One of addresses must be set as primary.'); + ->with('One of items must be set as primary.'); $constraint = $this->getMock('Oro\Bundle\AddressBundle\Validator\Constraints\ContainsPrimary'); $validator = new ContainsPrimaryValidator(); $validator->initialize($context); - $validator->validate($addresses, $constraint); + $validator->validate($items, $constraint); } /** * @return array */ - public function invalidAddressesDataProvider() + public function invalidItemsDataProvider() { return array( - 'one address' => array( - array($this->getTypedAddressMock(false)) + 'one item' => array( + array($this->getPrimaryItemMock(false)) ), - 'more than one address no primary' => array( - array($this->getTypedAddressMock(false), $this->getTypedAddressMock(false)) + 'more than one item no primary' => array( + array($this->getPrimaryItemMock(false), $this->getPrimaryItemMock(false)) ), - 'more than one address more than one primary' => array( - array($this->getTypedAddressMock(true), $this->getTypedAddressMock(true)) + 'more than one item more than one primary' => array( + array($this->getPrimaryItemMock(true), $this->getPrimaryItemMock(true)) ), ); } /** - * Get address mock. + * Get primary item mock. * * @param bool $isPrimary - * @param bool $isEmpty * @return \PHPUnit_Framework_MockObject_MockObject */ - protected function getTypedAddressMock($isPrimary, $isEmpty = false) + protected function getPrimaryItemMock($isPrimary) { - $address = $this->getMockBuilder('Oro\Bundle\AddressBundle\Entity\AbstractTypedAddress') + $item = $this->getMockBuilder('Oro\Bundle\AddressBundle\Entity\PrimaryItem') ->disableOriginalConstructor() ->getMock(); - $address->expects($this->any()) + + $item->expects($this->any()) ->method('isPrimary') ->will($this->returnValue($isPrimary)); - $address->expects($this->once()) - ->method('isEmpty') - ->will($this->returnValue($isEmpty)); - return $address; + + return $item; } } diff --git a/src/Oro/Bundle/AddressBundle/Twig/HasAddressExtension.php b/src/Oro/Bundle/AddressBundle/Twig/HasAddressExtension.php deleted file mode 100644 index bdb44400038..00000000000 --- a/src/Oro/Bundle/AddressBundle/Twig/HasAddressExtension.php +++ /dev/null @@ -1,55 +0,0 @@ - new \Twig_Filter_Method($this, 'hasAddress') - ); - } - - /** - * Check whenever flexible entity contains not empty address attribute - * - * @param \Oro\Bundle\FlexibleEntityBundle\Entity\Mapping\AbstractEntityFlexible $entity - * @param null $addressCode - * @return bool - */ - public function hasAddress(AbstractEntityFlexible $entity, $addressCode = null) - { - if ($addressCode !== null) { - $address = $entity->getValue($addressCode); - - return $address->getData() != null; - } - - /** @var \Doctrine\Common\Collections\ArrayCollection $values **/ - $values = $entity->getValues(); - $values = $values->filter( - function ($value) { - if (strpos($value->getAttribute()->getCode(), 'address') !== false) { - return $value->getData() != null; - } - - return false; - } - ); - return !$values->isEmpty(); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'oro_address_hasAddress'; - } -} diff --git a/src/Oro/Bundle/AddressBundle/Validator/Constraints/ContainsPrimary.php b/src/Oro/Bundle/AddressBundle/Validator/Constraints/ContainsPrimary.php index ac362a26674..28d18657db6 100644 --- a/src/Oro/Bundle/AddressBundle/Validator/Constraints/ContainsPrimary.php +++ b/src/Oro/Bundle/AddressBundle/Validator/Constraints/ContainsPrimary.php @@ -6,5 +6,5 @@ class ContainsPrimary extends Constraint { - public $message = 'One of addresses must be set as primary.'; + public $message = 'One of items must be set as primary.'; } diff --git a/src/Oro/Bundle/AddressBundle/Validator/Constraints/ContainsPrimaryValidator.php b/src/Oro/Bundle/AddressBundle/Validator/Constraints/ContainsPrimaryValidator.php index 01beb7db9d1..e765e3a8f37 100644 --- a/src/Oro/Bundle/AddressBundle/Validator/Constraints/ContainsPrimaryValidator.php +++ b/src/Oro/Bundle/AddressBundle/Validator/Constraints/ContainsPrimaryValidator.php @@ -2,11 +2,12 @@ namespace Oro\Bundle\AddressBundle\Validator\Constraints; -use Oro\Bundle\AddressBundle\Entity\AbstractTypedAddress; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Oro\Bundle\AddressBundle\Entity\PrimaryItem; + class ContainsPrimaryValidator extends ConstraintValidator { public function validate($value, Constraint $constraint) @@ -17,14 +18,12 @@ public function validate($value, Constraint $constraint) $primaryItemsNumber = 0; $totalItemsNumber = 0; - /** @var AbstractTypedAddress $item */ + /** @var PrimaryItem $item */ foreach ($value as $item) { - if (!$item->isEmpty()) { - if ($item instanceof AbstractTypedAddress && $item->isPrimary()) { - $primaryItemsNumber++; - } - $totalItemsNumber++; + if ($item instanceof PrimaryItem && $item->isPrimary()) { + $primaryItemsNumber++; } + $totalItemsNumber++; } if ($totalItemsNumber > 0 && $primaryItemsNumber != 1) { diff --git a/src/Oro/Bundle/AddressBundle/composer.json b/src/Oro/Bundle/AddressBundle/composer.json index 02065ac4879..77fdf8a6dfd 100644 --- a/src/Oro/Bundle/AddressBundle/composer.json +++ b/src/Oro/Bundle/AddressBundle/composer.json @@ -13,7 +13,6 @@ "friendsofsymfony/rest-bundle": "0.11.*", "friendsofsymfony/jsrouting-bundle": "1.1.*@dev", "nelmio/api-doc-bundle": "dev-master", - "oro/flexible-entity-bundle": "dev-master", "oro/form-bundle": "dev-master", "oro/translation-bundle": "dev-master", "besimple/soap-bundle": "dev-master" diff --git a/src/Oro/Bundle/AsseticBundle/Command/OroAsseticDumpCommand.php b/src/Oro/Bundle/AsseticBundle/Command/OroAsseticDumpCommand.php index 5a71c4d8a3d..686ac7e1f65 100644 --- a/src/Oro/Bundle/AsseticBundle/Command/OroAsseticDumpCommand.php +++ b/src/Oro/Bundle/AsseticBundle/Command/OroAsseticDumpCommand.php @@ -3,14 +3,17 @@ namespace Oro\Bundle\AsseticBundle\Command; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; -use Assetic\Asset\AssetInterface; -use Assetic\Util\VarUtils; 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 Symfony\Component\HttpFoundation\Request; +use Doctrine\Common\Cache\CacheProvider; + +use Assetic\Asset\AssetInterface; +use Assetic\Util\VarUtils; + use Oro\Bundle\AsseticBundle\Factory\OroAssetManager; class OroAsseticDumpCommand extends ContainerAwareCommand @@ -20,6 +23,11 @@ class OroAsseticDumpCommand extends ContainerAwareCommand */ protected $am; + /** + * @var CacheProvider + */ + protected $cache; + protected function configure() { $this @@ -31,8 +39,9 @@ protected function configure() protected function initialize(InputInterface $input, OutputInterface $output) { - $this->basePath = $input->getArgument('write_to') ?: $this->getContainer()->getParameter('assetic.write_to'); + $this->basePath = $input->getArgument('write_to') ? : $this->getContainer()->getParameter('assetic.write_to'); $this->am = $this->getContainer()->get('oro_assetic.asset_manager'); + $this->cache = $this->getContainer()->get('oro_assetic.cache'); } protected function execute(InputInterface $input, OutputInterface $output) @@ -45,6 +54,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln(sprintf('Debug mode is %s.', 'off')); $output->writeln(''); + $this->cache->flushAll(); $this->dumpAssets($output); } } @@ -67,15 +77,19 @@ protected function writeGroups($groups, $compiledGroups, $output) { foreach ($groups as $group) { if (in_array($group, $compiledGroups)) { - $output->writeln(sprintf( - '%s (compiled)', - $group - )); + $output->writeln( + sprintf( + '%s (compiled)', + $group + ) + ); } else { - $output->writeln(sprintf( - '%s', - $group - )); + $output->writeln( + sprintf( + '%s', + $group + ) + ); } } } @@ -104,30 +118,34 @@ private function doDump(AssetInterface $asset, OutputInterface $output) $asset->setValues($combination); // resolve the target path - $target = rtrim($this->basePath, '/').'/'.$asset->getTargetPath(); + $target = rtrim($this->basePath, '/') . '/' . $asset->getTargetPath(); $target = str_replace('_controller/', '', $target); $target = VarUtils::resolve($target, $asset->getVars(), $asset->getValues()); if (!is_dir($dir = dirname($target))) { - $output->writeln(sprintf( - '%s [dir+] %s', - date('H:i:s'), - $dir - )); + $output->writeln( + sprintf( + '%s [dir+] %s', + date('H:i:s'), + $dir + ) + ); if (false === @mkdir($dir, 0777, true)) { - throw new \RuntimeException('Unable to create directory '.$dir); + throw new \RuntimeException('Unable to create directory ' . $dir); } } - $output->writeln(sprintf( - '%s [file+] %s', - date('H:i:s'), - $target - )); + $output->writeln( + sprintf( + '%s [file+] %s', + date('H:i:s'), + $target + ) + ); if (false === @file_put_contents($target, $asset->dump())) { - throw new \RuntimeException('Unable to write file '.$target); + throw new \RuntimeException('Unable to write file ' . $target); } } } diff --git a/src/Oro/Bundle/AsseticBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/AsseticBundle/DependencyInjection/Configuration.php index 25c3c358c29..2497973e9fa 100644 --- a/src/Oro/Bundle/AsseticBundle/DependencyInjection/Configuration.php +++ b/src/Oro/Bundle/AsseticBundle/DependencyInjection/Configuration.php @@ -5,11 +5,6 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -/** - * This is the class that validates and merges configuration from your app/config files - * - * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class} - */ class Configuration implements ConfigurationInterface { /** @@ -22,13 +17,14 @@ public function getConfigTreeBuilder() $rootNode ->children() - ->arrayNode('uncompress_js') - ->prototype('scalar') - ->end() - ->end() - ->arrayNode('uncompress_css') - ->prototype('scalar') - ->end() + ->arrayNode('js_debug') + ->prototype('scalar')->end() + ->end() + ->booleanNode('js_debug_all')->defaultValue(false)->end() + ->arrayNode('css_debug') + ->prototype('scalar')->end() + ->end() + ->booleanNode('css_debug_all')->defaultValue(false)->end() ->end(); return $treeBuilder; diff --git a/src/Oro/Bundle/AsseticBundle/DependencyInjection/OroAsseticExtension.php b/src/Oro/Bundle/AsseticBundle/DependencyInjection/OroAsseticExtension.php index 9134301c6e5..689bb4e38ae 100644 --- a/src/Oro/Bundle/AsseticBundle/DependencyInjection/OroAsseticExtension.php +++ b/src/Oro/Bundle/AsseticBundle/DependencyInjection/OroAsseticExtension.php @@ -29,7 +29,8 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('oro_assetic.assets', $this->getAssets($container, $config)); // choose dynamic or static - if (!$container->getParameterBag()->resolveValue($container->getParameterBag()->get('assetic.use_controller'))) { + $useController = $container->getParameterBag()->get('assetic.use_controller'); + if (!$container->getParameterBag()->resolveValue($useController)) { $loader->load('assetic_controller_service.yml'); } } @@ -83,23 +84,23 @@ public function getAssets(ContainerBuilder $container, $config) $container->setParameter( 'oro_assetic.compiled_assets_groups', array( - 'js' => $config['uncompress_js'], - 'css' => $config['uncompress_css'] + 'js' => $config['js_debug'], + 'css' => $config['css_debug'] ) ); return array( - 'css' => $this->getAssetics($css, $config['uncompress_css']), - 'js' => $this->getAssetics($js, $config['uncompress_js']), + 'js' => $this->getAssetics($js, $config['js_debug'], $config['js_debug_all']), + 'css' => $this->getAssetics($css, $config['css_debug'], $config['css_debug_all']), ); } - protected function getAssetics($assetsArray, $uncompressBlocks) + protected function getAssetics($assetsArray, $debugBlocks, $debugAll) { $compressAssets = array(); $uncompressAssets = array(); foreach ($assetsArray as $blockName => $files) { - if (in_array($blockName, $uncompressBlocks)) { + if ($debugAll || in_array($blockName, $debugBlocks)) { $uncompressAssets = array_merge($uncompressAssets, $files); } else { $compressAssets = array_merge($compressAssets, $files); diff --git a/src/Oro/Bundle/AsseticBundle/Factory/OroAssetManager.php b/src/Oro/Bundle/AsseticBundle/Factory/OroAssetManager.php index a56c3758794..122d7adc15f 100644 --- a/src/Oro/Bundle/AsseticBundle/Factory/OroAssetManager.php +++ b/src/Oro/Bundle/AsseticBundle/Factory/OroAssetManager.php @@ -2,14 +2,23 @@ namespace Oro\Bundle\AsseticBundle\Factory; +use Symfony\Bundle\AsseticBundle\Factory\Resource\FileResource; + use Assetic\Factory\Resource\IteratorResourceInterface; use Assetic\Asset\AssetInterface; use Assetic\Factory\LazyAssetManager; +use Doctrine\Common\Cache\CacheProvider; + use Oro\Bundle\AsseticBundle\Node\OroAsseticNode; class OroAssetManager { + /** + * @var CacheProvider + */ + protected $cache; + /** * @var LazyAssetManager */ @@ -20,25 +29,47 @@ class OroAssetManager */ protected $twig; + /** + * @var array + */ protected $assetGroups; + + /** + * @var array + */ protected $compiledGroups; + /** + * @var OroAsseticNode[] + */ protected $assets; - protected $loaded; - protected $loading; - + /** + * Constructor + * + * @param LazyAssetManager $am + * @param \Twig_Environment $twig + * @param array $assetGroups + * @param array $compiledGroups + */ public function __construct(LazyAssetManager $am, \Twig_Environment $twig, $assetGroups, $compiledGroups) { - $this->loaded = false; - $this->loading = false; $this->am = $am; - $this->assets = array(); $this->twig = $twig; $this->assetGroups = $assetGroups; $this->compiledGroups = $compiledGroups; } + /** + * Set cache instance + * + * @param \Doctrine\Common\Cache\CacheProvider $cache + */ + public function setCache(CacheProvider $cache) + { + $this->cache = $cache; + } + /** * @return array */ @@ -56,11 +87,11 @@ public function getCompiledGroups() } /** - * @return array + * @return OroAsseticNode[] */ public function getAssets() { - $this->checkLoad(); + $this->load(); return $this->assets; } @@ -71,7 +102,7 @@ public function getAssets() */ public function get($name) { - $this->checkLoad(); + $this->load(); return $this->assets[$name]->getUnCompressAsset(); } @@ -82,41 +113,62 @@ public function get($name) */ public function has($name) { - $this->checkLoad(); + $this->load(); return isset($this->assets[$name]); } /** - * @return array + * Load assets */ public function load() { - if ($this->loading) { - return; + if (null === $this->assets) { + $this->assets = $this->cache ? $this->loadAssetsFromCache() : $this->loadAssets(); } - $this->loading = true; + } - $assets = array(); - foreach ($this->am->getResources() as $resources) { + /** + * Load using cache + * + * @return OroAsseticNode[] + */ + protected function loadAssetsFromCache() + { + $cacheKey = 'assets'; + $assets = $this->cache->fetch($cacheKey); + if (false === $assets) { + $assets = $this->loadAssets(); + $this->cache->save($cacheKey, serialize($assets)); + } else { + $assets = unserialize($assets); + } + return $assets; + } + + /** + * Analyze resources and collect nodes of OroAsseticNode + * + * @return OroAsseticNode[] + */ + protected function loadAssets() + { + $result = array(); + foreach ($this->am->getResources() as $resources) { if (!$resources instanceof IteratorResourceInterface) { $resources = array($resources); } + /**@var $resource FileResource */ foreach ($resources as $resource) { - /**@var $resource \Symfony\Bundle\AsseticBundle\Factory\Resource\FileResource */ $tokens = $this->twig->tokenize($resource->getContent(), (string)$resource); $nodes = $this->twig->parse($tokens); - $assets += $this->loadNode($nodes); + $result += $this->loadNode($nodes); } } - $this->assets = $assets; - $this->loaded = true; - $this->loading = false; - - return $this->assets; + return $result; } /** @@ -134,8 +186,6 @@ public function getLastModified(AssetInterface $asset) */ public function hasFormula($name) { - $this->checkLoad(); - return true; } @@ -145,21 +195,11 @@ public function hasFormula($name) */ public function getFormula($name) { - $this->checkLoad(); + $this->load(); return array($this->assets[$name]->getAttribute('inputs')); } - /** - * Check if assets was loaded - */ - private function checkLoad() - { - if (!$this->loaded) { - $this->load(); - } - } - /** * Loads assets from the supplied node. * diff --git a/src/Oro/Bundle/AsseticBundle/README.md b/src/Oro/Bundle/AsseticBundle/README.md index 6f3b2f608db..dbc1745d6df 100644 --- a/src/Oro/Bundle/AsseticBundle/README.md +++ b/src/Oro/Bundle/AsseticBundle/README.md @@ -1,9 +1,13 @@ OroAsseticBundle ======================== -To implement hashtag navigation all basic javascript and css files should be loaded in main template. -Each bundle can have a config file assets.yml with the list of js and css files. +OroAssetic enables expandable and optimized way to manage CSS and JS assets that are distributed across many bundles. +With OroAssetic developer can define CSS and JavaScript files groups in assets.yml configuration of the bundle. Defined +files will be automatically merged and optimized for web presentation. For development and debug purposes some files can +be excluded from optimization process. + +Example of assets.yml file: ```yaml js: 'some_group' @@ -17,23 +21,37 @@ css: - 'Assets/Path/To/Css/third.css' ``` -Js and css sections contain groups of files. This groups can be uncompressed for debugging. +Js and css sections contain groups of files. This groups can be excluded from optimization process debugging purposes. + +The path to file can be defined as @BundleName/Resources/puclic/path/to/file.ext or bundles/bundle/path/to/file.ext. +If the file path contains @, then in debug mode it will be taken via controller. If path doesn't contain @, then file +will be taken via request to web folder. -The path to file can be in @BundleName/Resources/puclic/path/to/file.ext or bundles/bundle/path/to/file.ext. If the file path -contains @, then in uncompiled mode it will be taken via controller. If path doesn't contain @, then file will be taken -via request to web folder. +For example, to turn off compression of css files in 'css_group' group the following configuration should be added +to app/config/config.yml (or app/config/config_{mode}.yml) file: -For example, to turn off compression of css files from 'css_group' the next configuration mut be added in config.yml file: +```yaml +oro_assetic: + js_debug: ~ + css_debug: [css_group] +``` +In order to anable debug mode for all CSS and JS files following configuration can be applied: ```yaml oro_assetic: - uncompress_js: ~ - uncompress_css: [css_group] + js_debug_all: true + css_debug_all: true +``` + +Cache cleanup and Oro assetics dump required after: + +```php +php app/console cache:clear +php app/console oro:assetic:dump ``` -and cache must be cleaned. -To get list of all available assets groups next command should be used: +To get list of all available asset groups next command should be used: ```php php app/console oro:assetic:dump show-groups diff --git a/src/Oro/Bundle/AsseticBundle/Resources/config/services.yml b/src/Oro/Bundle/AsseticBundle/Resources/config/services.yml index 6f134ce3784..7cd15ad1b49 100644 --- a/src/Oro/Bundle/AsseticBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/AsseticBundle/Resources/config/services.yml @@ -12,6 +12,13 @@ services: - @twig - %oro_assetic.assets_groups% - %oro_assetic.compiled_assets_groups% + calls: + - [setCache, [@oro_assetic.cache]] + + oro_assetic.cache: + parent: oro.cache.abstract + calls: + - [setNamespace, ['oro_assetic.cache']] oro_assetic.twig.extension: class: %oro_assetic.twig_extension.class% @@ -39,4 +46,4 @@ services: - %assetic.enable_profiler% scope: prototype calls: - - [setValueSupplier, [@assetic.value_supplier]] \ No newline at end of file + - [setValueSupplier, [@assetic.value_supplier]] diff --git a/src/Oro/Bundle/AsseticBundle/Tests/Unit/DependencyInjection/OroAsseticExtensionTest.php b/src/Oro/Bundle/AsseticBundle/Tests/Unit/DependencyInjection/OroAsseticExtensionTest.php index 0248706570b..7263544213f 100644 --- a/src/Oro/Bundle/AsseticBundle/Tests/Unit/DependencyInjection/OroAsseticExtensionTest.php +++ b/src/Oro/Bundle/AsseticBundle/Tests/Unit/DependencyInjection/OroAsseticExtensionTest.php @@ -5,7 +5,31 @@ class OroAsseticExtensionTest extends \PHPUnit_Framework_TestCase { - public function testGetAssets() + /** + * Data provider for testGetAssets + * + * @return array + */ + public function getAssetsDataProvider() + { + return array( + array( + array('css_debug' => array(), 'js_debug' => array(), 'css_debug_all' => true, 'js_debug_all' => true), + array('compress' => array(array()), 'uncompress' => array(array('first.css', 'second.css'))), + array('compress' => array(array()), 'uncompress' => array(array('first.js', 'second.js'))), + ), + array( + array('css_debug' => array(), 'js_debug' => array(), 'css_debug_all' => false, 'js_debug_all' => false), + array('compress' => array(array('first.css', 'second.css')), 'uncompress' => array(array())), + array('compress' => array(array('first.js', 'second.js')), 'uncompress' => array(array())), + ), + ); + } + + /** + * @dataProvider getAssetsDataProvider + */ + public function testGetAssets($config, $expectedCss, $expectedJs) { $extension = new OroAsseticExtension(); @@ -21,9 +45,8 @@ public function testGetAssets() ) ); - $assets = $extension->getAssets($container, array('uncompress_css' => array(), 'uncompress_js' => array())); - - $this->assertEquals('second.css', $assets['css']['compress'][0][1]); - $this->assertEquals('first.js', $assets['js']['compress'][0][0]); + $assets = $extension->getAssets($container, $config); + $this->assertEquals($expectedCss, $assets['css']); + $this->assertEquals($expectedJs, $assets['js']); } } diff --git a/src/Oro/Bundle/AsseticBundle/Tests/Unit/Factory/OroAssetManagerTest.php b/src/Oro/Bundle/AsseticBundle/Tests/Unit/Factory/OroAssetManagerTest.php index fd35995c3e7..1631e3e0595 100644 --- a/src/Oro/Bundle/AsseticBundle/Tests/Unit/Factory/OroAssetManagerTest.php +++ b/src/Oro/Bundle/AsseticBundle/Tests/Unit/Factory/OroAssetManagerTest.php @@ -14,17 +14,15 @@ class OroAssetManagerTest extends \PHPUnit_Framework_TestCase */ private $manager; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ private $am; - private $twig; - - private $resource; - private $token; - private $node; - private $compressAsset; - private $unCompressAsset; - - private $assetFile; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $twig; public function setUp() { @@ -36,95 +34,220 @@ public function setUp() ->disableOriginalConstructor() ->getMock(); - $this->resource = $this->getMock('Assetic\Factory\Resource\ResourceInterface'); - - $this->resource->expects($this->any()) - ->method('isFresh') - ->will($this->returnValue(true)); - - $this->am->expects($this->any()) - ->method('getResources') - ->will($this->returnValue(array($this->resource))); + $this->manager = new OroAssetManager($this->am, $this->twig, array('assetGroups'), array('compiledGroup')); + } - $this->token = $this->getMockBuilder('\Twig_TokenStream') - ->disableOriginalConstructor() - ->getMock(); + public function testGetGroups() + { + $data = $this->manager->getAssetGroups(); + $this->assertEquals('assetGroups', $data[0]); + } - $this->assetFile = new FileAsset('test.css'); + public function testCompiledGroups() + { + $data = $this->manager->getCompiledGroups(); + $this->assertEquals('compiledGroup', $data[0]); + } - $this->compressAsset = new AssetCollection(array($this->assetFile)); + public function testGetAssets() + { + $resource = $this->createMockResource('resource_name', 'resource_content'); + $token = $this->getMockBuilder('Twig_TokenStream')->disableOriginalConstructor()->getMock(); - $this->unCompressAsset = new AssetCollection(array($this->assetFile)); + $barAsset = $this->createMockOroAsseticNode('uncompress_bar_asset'); + $fooAsset = $this->createMockOroAsseticNode('uncompress_foo_asset', array($barAsset)); - $this->node = new OroAsseticNode( + $this->addMockExpectedCalls( array( - 'compress' => $this->compressAsset, - 'un_compress' => $this->unCompressAsset + 'mock' => $this->am, + 'expectedCalls' => array( + array('getResources', array(), $this->returnValue(array($resource))) + ) ), array( - 'un_compress' => 'uncompress_test_asset', - 'compress' => 'compress_test_asset' + 'mock' => $this->twig, + 'expectedCalls' => array( + array('tokenize', array('resource_content', 'resource_name'), $this->returnValue($token)), + array('parse', array($token), $this->returnValue($fooAsset)) + ) + ) + ); + + $this->assertEquals( + array( + 'uncompress_foo_asset' => $fooAsset, + 'uncompress_bar_asset' => $barAsset, ), - array(), - array('test.css'), - new \Twig_Node(), - array(), - 10, - 'oro_css' + $this->manager->getAssets() ); + } - $this->twig->expects($this->any()) - ->method('tokenize') - ->will($this->returnValue($this->token)); + public function testSaveAssetsToCache() + { + $cache = $this->getMockBuilder('Doctrine\Common\Cache\CacheProvider') + ->setMethods(array('fetch', 'save')) + ->getMockForAbstractClass(); + $this->manager->setCache($cache); - $this->twig->expects($this->any()) - ->method('parse') - ->will($this->returnValue($this->node)); + $resource = $this->createMockResource('resource_name', 'resource_content'); + $token = $this->getMockBuilder('Twig_TokenStream')->disableOriginalConstructor()->getMock(); + $asset = $this->createMockOroAsseticNode('uncompress_test_asset'); + $this->addMockExpectedCalls( + array( + 'mock' => $this->am, + 'expectedCalls' => array( + array('getResources', array(), $this->returnValue(array($resource))) + ) + ), + array( + 'mock' => $this->twig, + 'expectedCalls' => array( + array('tokenize', array('resource_content', 'resource_name'), $this->returnValue($token)), + array('parse', array($token), $this->returnValue($asset)) + ) + ), + array( + 'mock' => $cache, + 'expectedCalls' => array( + array('fetch', array('assets'), $this->returnValue(false)), + array( + 'save', + array('assets', $this->stringStartsWith('a:1:{s:21:"uncompress_test_asset"')), + $this->returnValue(false) + ), + ) + ) + ); - $this->manager = new OroAssetManager($this->am, $this->twig, array('assetGroups'), array('compiledGroup')); + $this->assertEquals( + array('uncompress_test_asset' => $asset), + $this->manager->getAssets() + ); } - public function testGetGroups() + public function testFetchAssetsFromCache() { - $data = $this->manager->getAssetGroups(); - $this->assertEquals('assetGroups', $data[0]); - } + $cache = $this->getMockBuilder('Doctrine\Common\Cache\CacheProvider') + ->setMethods(array('fetch', 'save')) + ->getMockForAbstractClass(); + $this->manager->setCache($cache); - public function testCompiledGroups() - { - $data = $this->manager->getCompiledGroups(); - $this->assertEquals('compiledGroup', $data[0]); - } + $cachedAssets = array( + 'foo' => new \stdClass() + ); - public function testGetAssets() - { - $assets = $this->manager->getAssets(); - $this->assertEquals($this->node, $assets['uncompress_test_asset']); + $this->addMockExpectedCalls( + array( + 'mock' => $cache, + 'expectedCalls' => array( + array('fetch', array('assets'), $this->returnValue(serialize($cachedAssets))), + ) + ) + ); + + $this->assertEquals($cachedAssets, $this->manager->getAssets()); } public function testGet() { - $assets = $this->manager->get('uncompress_test_asset'); - foreach ($assets as $asset) { - $this->assertEquals($this->assetFile->getSourcePath(), $asset->getSourcePath()); - } + $resource = $this->createMockResource('resource_name', 'resource_content'); + $token = $this->getMockBuilder('Twig_TokenStream')->disableOriginalConstructor()->getMock(); + $asset = $this->createMockOroAsseticNode('uncompress_test_asset'); + + $assetFile = new FileAsset('test.css'); + $asset->expects($this->once())->method('getUnCompressAsset')->will($this->returnValue($assetFile)); + + $this->addMockExpectedCalls( + array( + 'mock' => $this->am, + 'expectedCalls' => array( + array('getResources', array(), $this->returnValue(array($resource))) + ) + ), + array( + 'mock' => $this->twig, + 'expectedCalls' => array( + array('tokenize', array('resource_content', 'resource_name'), $this->returnValue($token)), + array('parse', array($token), $this->returnValue($asset)) + ) + ) + ); + + $this->assertEquals( + $assetFile, + $this->manager->get('uncompress_test_asset') + ); } public function testHas() { + $resource = $this->createMockResource('resource_name', 'resource_content'); + $token = $this->getMockBuilder('Twig_TokenStream')->disableOriginalConstructor()->getMock(); + $asset = $this->createMockOroAsseticNode('uncompress_test_asset'); + + $this->addMockExpectedCalls( + array( + 'mock' => $this->am, + 'expectedCalls' => array( + array('getResources', array(), $this->returnValue(array($resource))) + ) + ), + array( + 'mock' => $this->twig, + 'expectedCalls' => array( + array('tokenize', array('resource_content', 'resource_name'), $this->returnValue($token)), + array('parse', array($token), $this->returnValue($asset)) + ) + ) + ); + $this->assertTrue($this->manager->has('uncompress_test_asset')); } public function testHasFormula() { + $this->addMockExpectedCalls( + array( + 'mock' => $this->am, + 'expectedCalls' => array() + ), + array( + 'mock' => $this->twig, + 'expectedCalls' => array() + ) + ); + $this->assertTrue($this->manager->hasFormula('uncompress_test_asset')); } public function testGetFormula() { - $formula = $this->manager->getFormula('uncompress_test_asset'); - $this->assertEquals($formula[0][0], 'test.css'); + $resource = $this->createMockResource('resource_name', 'resource_content'); + $token = $this->getMockBuilder('Twig_TokenStream')->disableOriginalConstructor()->getMock(); + $asset = $this->createMockOroAsseticNode('uncompress_test_asset'); + + $inputsAttribute = array('foo'); + $asset->expects($this->once())->method('getAttribute') + ->with('inputs')->will($this->returnValue($inputsAttribute)); + + $this->addMockExpectedCalls( + array( + 'mock' => $this->am, + 'expectedCalls' => array( + array('getResources', array(), $this->returnValue(array($resource))) + ) + ), + array( + 'mock' => $this->twig, + 'expectedCalls' => array( + array('tokenize', array('resource_content', 'resource_name'), $this->returnValue($token)), + array('parse', array($token), $this->returnValue($asset)) + ) + ) + ); + + $this->assertEquals(array($inputsAttribute), $this->manager->getFormula('uncompress_test_asset')); } public function testGetLastModified() @@ -137,4 +260,60 @@ public function testGetLastModified() $this->assertEquals(123, $this->manager->getLastModified($asset)); } + + protected function createMockResource($name, $content) + { + $result = $this->getMock('Assetic\Factory\Resource\ResourceInterface'); + + $result->expects($this->once()) + ->method('getContent') + ->will($this->returnValue($content)); + + $result->expects($this->once()) + ->method('__toString') + ->will($this->returnValue($name)); + + return $result; + } + + protected function createMockOroAsseticNode($nameUnCompress, array $children = array()) + { + $result = $this->getMockBuilder('Oro\Bundle\AsseticBundle\Node\OroAsseticNode') + ->disableOriginalConstructor() + ->setMethods(array('getNameUnCompress', 'getUnCompressAsset', 'getAttribute', 'getIterator')) + ->getMock(); + + $result->expects($this->any()) + ->method('getIterator') + ->will($this->returnValue(new \ArrayIterator($children))); + + $result->expects($this->once()) + ->method('getNameUnCompress') + ->will($this->returnValue($nameUnCompress)); + + return $result; + } + + protected function addMockExpectedCalls() + { + $mocksExpectedCalls = func_get_args(); + foreach ($mocksExpectedCalls as $mockExpectedCalls) { + /** @var \PHPUnit_Framework_MockObject_MockObject $mock */ + list($mock, $expectedCalls) = array_values($mockExpectedCalls); + if ($expectedCalls) { + $index = 0; + foreach ($expectedCalls as $expectedCall) { + $expectedCall = array_pad($expectedCall, 3, null); + list($method, $arguments, $result) = $expectedCall; + $methodExpectation = $mock->expects(\PHPUnit_Framework_TestCase::at($index++))->method($method); + $methodExpectation = call_user_func_array(array($methodExpectation, 'with'), $arguments); + if ($expectedCall) { + $methodExpectation->will($result); + } + } + } else { + $mock->expects(\PHPUnit_Framework_TestCase::never())->method(\PHPUnit_Framework_TestCase::anything()); + } + }; + } } diff --git a/src/Oro/Bundle/AsseticBundle/Tests/Unit/Routing/OroasseticLoaderTest.php b/src/Oro/Bundle/AsseticBundle/Tests/Unit/Routing/OroasseticLoaderTest.php index ef5074d79a8..fa179bd66d5 100644 --- a/src/Oro/Bundle/AsseticBundle/Tests/Unit/Routing/OroasseticLoaderTest.php +++ b/src/Oro/Bundle/AsseticBundle/Tests/Unit/Routing/OroasseticLoaderTest.php @@ -46,10 +46,7 @@ public function testLoad() '"/\.[^.]+\.twig$/"' ); - $resource = new CoalescingDirectoryResource(array( - $dir, - $dir, - )); + $resource = new CoalescingDirectoryResource(array($dir, $dir)); $this->am->am->expects($this->any()) ->method('getResources') diff --git a/src/Oro/Bundle/AsseticBundle/Tests/Unit/twig/AsseticExtensionTest.php b/src/Oro/Bundle/AsseticBundle/Tests/Unit/Twig/AsseticExtensionTest.php similarity index 89% rename from src/Oro/Bundle/AsseticBundle/Tests/Unit/twig/AsseticExtensionTest.php rename to src/Oro/Bundle/AsseticBundle/Tests/Unit/Twig/AsseticExtensionTest.php index f4d13d5b508..b63970b73e9 100644 --- a/src/Oro/Bundle/AsseticBundle/Tests/Unit/twig/AsseticExtensionTest.php +++ b/src/Oro/Bundle/AsseticBundle/Tests/Unit/Twig/AsseticExtensionTest.php @@ -38,7 +38,12 @@ public function setUp() $this->enabledBundles = array('testBundle'); - $this->extension = new AsseticExtension($this->assetsFactory, $this->assets, $this->templateNameParser, $this->enabledBundles); + $this->extension = new AsseticExtension( + $this->assetsFactory, + $this->assets, + $this->templateNameParser, + $this->enabledBundles + ); } public function testGetTokenParsers() diff --git a/src/Oro/Bundle/ConfigBundle/DependencyInjection/SettingsBuilder.php b/src/Oro/Bundle/ConfigBundle/DependencyInjection/SettingsBuilder.php index f4f402df898..1f71fa47fc3 100644 --- a/src/Oro/Bundle/ConfigBundle/DependencyInjection/SettingsBuilder.php +++ b/src/Oro/Bundle/ConfigBundle/DependencyInjection/SettingsBuilder.php @@ -15,7 +15,7 @@ class SettingsBuilder public static function append(ArrayNodeDefinition $root, $settings) { $builder = new TreeBuilder(); - $node = $builder + $node = $builder ->root('settings') ->addDefaultsIfNotSet() ->children(); diff --git a/src/Oro/Bundle/CronBundle/Command/CronCommand.php b/src/Oro/Bundle/CronBundle/Command/CronCommand.php new file mode 100644 index 00000000000..07666b17239 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Command/CronCommand.php @@ -0,0 +1,104 @@ +setName('oro:cron') + ->setDescription('Cron commands launcher'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $commands = $this->getApplication()->all('oro:cron'); + $em = $this->getContainer()->get('doctrine.orm.entity_manager'); + $daemon = $this->getContainer()->get('oro_cron.job_daemon'); + $dbCommands = $em->getRepository('OroCronBundle:Schedule')->findAll(); + + // check if daemon is running + if (!$daemon->getPid()) { + $output->writeln(''); + $output->write('Daemon process not found, running.. '); + + if ($pid = $daemon->run()) { + $output->writeln(sprintf('OK (pid: %u)', $pid)); + } else { + $output->writeln('failed. Cron jobs can\'t be launched.'); + + return; + } + } + + foreach ($commands as $name => $command) { + $output->writeln(''); + $output->write(sprintf('Processing command "%s": ', $name)); + + $dbCommand = array_filter( + $dbCommands, + function ($element) use ($name) { + return $element->getCommand() == $name; + } + ); + + if (empty($dbCommand)) { + $output->writeln('new command found, setting up schedule..'); + + if (!$command instanceof CronCommandInterface) { + $output->writeln('Unable to setup, command must be instance of CronCommandInterface'); + + continue; + } + + $schedule = new Schedule(); + + $schedule + ->setCommand($name) + ->setDefinition($command->getDefaultDefinition()); + + $em->persist($schedule); + + continue; + } + + $dbCommand = current($dbCommand); + + if (!$dbCommand->getDefinition()) { + $output->writeln('no cron definition found, check db record'); + + continue; + } + + $cron = \Cron\CronExpression::factory($dbCommand->getDefinition()); + + /** + * @todo Add "Oro timezone" setting as parameter to isDue method + */ + if ($cron->isDue()) { + $job = new Job($name); + + $em->persist($job); + + $output->writeln('added to job queue'); + } else { + $output->writeln('skipped'); + } + } + + $em->flush(); + + $output->writeln(''); + $output->writeln('All commands finished'); + } +} diff --git a/src/Oro/Bundle/CronBundle/Command/CronCommandInterface.php b/src/Oro/Bundle/CronBundle/Command/CronCommandInterface.php new file mode 100644 index 00000000000..d5a8b7cca6d --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Command/CronCommandInterface.php @@ -0,0 +1,15 @@ + $this->get('oro_cron.job_daemon')->getPid(), + 'pager' => $this->get('knp_paginator')->paginate( + $this->get('oro_cron.job_manager')->getListQuery(), + (int) $page, + (int) $limit + ), + ); + } + + /** + * @Route("/view/{id}", name="oro_cron_job_view", requirements={"id"="\d+"}) + * @Template + * @Acl( + * id="oro_cron_job_view", + * name="View job", + * description="View job details", + * parent="oro_cron_job" + * ) + */ + public function viewAction(Job $job) + { + $manager = $this->get('oro_cron.job_manager'); + $statistics = $this->statisticsEnabled + ? $manager->getJobStatistics($job) + : array(); + + return array( + 'job' => $job, + 'pid' => $this->get('oro_cron.job_daemon')->getPid(), + 'relatedEntities' => $manager->getRelatedEntities($job), + 'statistics' => $statistics, + 'dependencies' => $this->getDoctrine() + ->getRepository('JMSJobQueueBundle:Job') + ->getIncomingDependencies($job), + ); + } + + /** + * @Route("/run-daemon", name="oro_cron_job_run_daemon") + * @Acl( + * id="oro_cron_job_run", + * name="Run daemon", + * description="Run job queue daemon in background", + * parent="oro_cron_job" + * ) + */ + public function runDaemonAction() + { + $daemon = $this->get('oro_cron.job_daemon'); + $translator = $this->get('translator'); + $ret = array('error' => 1); + + try { + if ($pid = $daemon->run()) { + $ret['error'] = 0; + $ret['message'] = $pid; + } else { + $ret['message'] = $translator->trans('oro.cron.message.start.fail'); + } + } catch (\RuntimeException $e) { + $ret['message'] = $e->getMessage(); + } + + if ($this->getRequest()->isXmlHttpRequest()) { + return new Response(json_encode($ret)); + } else { + if ($ret['error']) { + $this->get('session')->getFlashBag()->add('error', $ret['message']); + } else { + $this->get('session')->getFlashBag()->add('success', $translator->trans('oro.cron.message.start.success')); + } + + return $this->redirect($this->generateUrl('oro_cron_job_index')); + } + } + + /** + * @Route("/stop-daemon", name="oro_cron_job_stop_daemon") + * @Acl( + * id="oro_cron_job_stop", + * name="Stop daemon", + * description="Stop job queue daemon", + * parent="oro_cron_job" + * ) + */ + public function stopDaemonAction() + { + $daemon = $this->get('oro_cron.job_daemon'); + $translator = $this->get('translator'); + $ret = array('error' => 1); + + try { + if ($daemon->stop()) { + $ret['error'] = 0; + $ret['message'] = $translator->trans('oro.cron.message.stop.success'); + } else { + $ret['message'] = $translator->trans('oro.cron.message.stop.fail'); + } + } catch (\RuntimeException $e) { + $ret['message'] = $e->getMessage(); + } + + if ($this->getRequest()->isXmlHttpRequest()) { + return new Response(json_encode($ret)); + } else { + $this->get('session')->getFlashBag()->add($ret['error'] ? 'error' : 'success', $ret['message']); + + return $this->redirect($this->generateUrl('oro_cron_job_index')); + } + } + + /** + * @Route("/status", name="oro_cron_job_status") + * @AclAncestor("oro_cron_job_list") + */ + public function statusAction() + { + return $this->getRequest()->isXmlHttpRequest() + ? new Response($this->get('oro_cron.job_daemon')->getPid()) + : $this->redirect($this->generateUrl('oro_cron_job_index')); + } +} diff --git a/src/Oro/Bundle/CronBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/CronBundle/DependencyInjection/Configuration.php new file mode 100644 index 00000000000..8880dc1ad4a --- /dev/null +++ b/src/Oro/Bundle/CronBundle/DependencyInjection/Configuration.php @@ -0,0 +1,27 @@ +root('oro_cron') + ->children() + ->scalarNode('max_concurrent_jobs') + ->defaultValue(5) + ->end() + ->end(); + + return $builder; + } +} diff --git a/src/Oro/Bundle/CronBundle/DependencyInjection/OroCronExtension.php b/src/Oro/Bundle/CronBundle/DependencyInjection/OroCronExtension.php new file mode 100644 index 00000000000..ca13adb279a --- /dev/null +++ b/src/Oro/Bundle/CronBundle/DependencyInjection/OroCronExtension.php @@ -0,0 +1,30 @@ +processConfiguration($configuration, $configs); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.yml'); + + $container->setParameter('oro_cron.max_jobs', (int) $config['max_concurrent_jobs']); + } +} diff --git a/src/Oro/Bundle/CronBundle/Entity/Manager/JobManager.php b/src/Oro/Bundle/CronBundle/Entity/Manager/JobManager.php new file mode 100644 index 00000000000..dd3b99d7170 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Entity/Manager/JobManager.php @@ -0,0 +1,98 @@ +em = $em; + } + + /** + * Returns basic query instance to get collection with all job instances + * + * @return QueryBuilder + */ + public function getListQuery() + { + $qb = $this->em->createQueryBuilder(); + + return $qb + ->select('j') + ->from('JMSJobQueueBundle:Job', 'j') + ->where($qb->expr()->isNull('j.originalJob')) + ->orderBy('j.createdAt', 'DESC'); + } + + public function getRelatedEntities(Job $job) + { + $related = array(); + + foreach ($job->getRelatedEntities() as $entity) { + $class = ClassUtils::getClass($entity); + + $related[] = array( + 'class' => $class, + 'id' => json_encode($this->em->getClassMetadata($class)->getIdentifierValues($entity)), + 'raw' => $entity, + ); + } + + return $related; + } + + public function getJobStatistics(Job $job) + { + $statisticData = array(); + $dataPerCharacteristic = array(); + $statistics = $this->em->getConnection() + ->query("SELECT * FROM jms_job_statistics WHERE job_id = " . $job->getId()); + + foreach ($statistics as $row) { + $dataPerCharacteristic[$row['characteristic']][] = array( + $row['createdAt'], + $row['charValue'], + ); + } + + if ($dataPerCharacteristic) { + $statisticData = array(array_merge(array('Time'), $chars = array_keys($dataPerCharacteristic))); + $startTime = strtotime($dataPerCharacteristic[$chars[0]][0][0]); + $endTime = strtotime($dataPerCharacteristic[$chars[0]][count($dataPerCharacteristic[$chars[0]])-1][0]); + $scaleFactor = $endTime - $startTime > 300 ? 1/60 : 1; + + // This assumes that we have the same number of rows for each characteristic. + for ($i = 0, $c = count(reset($dataPerCharacteristic)); $i < $c; $i++) { + $row = array((strtotime($dataPerCharacteristic[$chars[0]][$i][0]) - $startTime) * $scaleFactor); + + foreach ($chars as $name) { + $value = (float) $dataPerCharacteristic[$name][$i][1]; + + switch ($name) { + case 'memory': + $value /= 1024 * 1024; + break; + } + + $row[] = $value; + } + + $statisticData[] = $row; + } + } + + return $statisticData; + } +} diff --git a/src/Oro/Bundle/CronBundle/Entity/Schedule.php b/src/Oro/Bundle/CronBundle/Entity/Schedule.php new file mode 100644 index 00000000000..0a7941e9d5d --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Entity/Schedule.php @@ -0,0 +1,109 @@ +id; + } + + /** + * Get command name + * + * @return string + */ + public function getCommand() + { + return $this->command; + } + + /** + * Set command name + * + * @param string $command + * @return Shedule + */ + public function setCommand($command) + { + $this->command = $command; + + return $this; + } + + /** + * Returns cron definition string + * + * @return string + */ + public function getDefinition() + { + return $this->definition; + } + + /** + * Set cron definition string + * + * General format: + * * * * * * + * ┬ ┬ ┬ ┬ ┬ + * │ │ │ │ │ + * │ │ │ │ │ + * │ │ │ │ └───── day of week (0 - 6) (0 to 6 are Sunday to Saturday, or use names) + * │ │ │ └────────── month (1 - 12) + * │ │ └─────────────── day of month (1 - 31) + * │ └──────────────────── hour (0 - 23) + * └───────────────────────── min (0 - 59) + * + * Predefined values are: + * @yearly (or @annually) Run once a year at midnight in the morning of January 1 0 0 1 1 * + * @monthly Run once a month at midnight in the morning of the first of the month 0 0 1 * * + * @weekly Run once a week at midnight in the morning of Sunday 0 0 * * 0 + * @daily Run once a day at midnight 0 0 * * * + * @hourly Run once an hour at the beginning of the hour 0 * * * * + * + * @param string $definition New cron definition + * @return Shedule + */ + public function setDefinition($definition) + { + $this->definition = $definition; + + return $this; + } +} diff --git a/src/Oro/Bundle/CronBundle/Job/Daemon.php b/src/Oro/Bundle/CronBundle/Job/Daemon.php new file mode 100644 index 00000000000..18d3d6cc934 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Job/Daemon.php @@ -0,0 +1,154 @@ +rootDir = $rootDir; + $this->maxJobs = (int) $maxJobs; + } + + /** + * Run daemon in background + * + * @throws \RuntimeException + * @return int|null The process id if running successfully, null otherwise + */ + public function run() + { + if ($this->getPid()) { + throw new \RuntimeException('Daemon process already started'); + } + + // workaround for Windows + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $wsh = new \COM('WScript.shell'); + + $wsh->Run($this->getQueueRunCmd(), 0, false); + + return $this->getPid(); + } + + $process = $this->getQueueRunProcess(); + + $process->start(); + + return $process->getPid(); + } + + /** + * Stop daemon + * + * @throws \RuntimeException + * @return bool True if success, false otherwise + */ + public function stop() + { + $pid = $this->getPid(); + + if (!$pid) { + throw new \RuntimeException('Daemon process not found'); + } + + $process = $this->getQueueStopProcess($pid); + + $process->run(); + + return $process->isSuccessful(); + } + + /** + * Check if jobs queue daemon is running + * + * @return int|null Daemon process id on success, null otherwise + */ + public function getPid() + { + // workaround for Windows + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $output = shell_exec('WMIC path win32_process get Processid,Commandline'); + + return preg_match('#console jms-job-queue:run.+(\d+)\s*$#Usm', $output, $matches) + ? (int) $matches[1] + : null; + } + + $process = $this->getPidProcess(); + + $process->run(); + + return preg_match('#^.+console jms-job-queue:run#Usm', $process->getOutput(), $matches) + ? (int) $matches[0] + : null; + } + + /** + * Instantiate "ps" *nix command to catch running job queue + * + * @return Process + */ + protected function getPidProcess() + { + return new Process('ps ax | grep "[j]ms-job-queue:run"'); + } + + /** + * Instantiate "ps" *nix command to catch running job queue + * + * @return Process + */ + protected function getQueueRunProcess() + { + return new Process($this->getQueueRunCmd()); + } + + /** + * Instantiate "kill" (*nix) / "taskkill" (Windows) command to terminate job queue + * + * @param int $pid Process id to kill + * @return Process + */ + protected function getQueueStopProcess($pid) + { + $cmd = defined('PHP_WINDOWS_VERSION_BUILD') ? 'taskkill /PID %u' : 'kill -9 %u'; + + return new Process(sprintf($cmd, $pid)); + } + + /** + * Get command line to run job queue + * + * @return string + */ + protected function getQueueRunCmd() + { + return sprintf( + 'php %sconsole jms-job-queue:run --max-runtime=999999999 --max-concurrent-jobs=%u', + $this->rootDir . DIRECTORY_SEPARATOR, + max($this->maxJobs, 1) + ); + } +} diff --git a/src/Oro/Bundle/CronBundle/OroCronBundle.php b/src/Oro/Bundle/CronBundle/OroCronBundle.php new file mode 100644 index 00000000000..449f2fc07a3 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/OroCronBundle.php @@ -0,0 +1,9 @@ +> /dev/null +``` + +On Windows you can use Task Scheduler from Control Panel. \ No newline at end of file diff --git a/src/Oro/Bundle/CronBundle/Resources/config/assets.yml b/src/Oro/Bundle/CronBundle/Resources/config/assets.yml new file mode 100644 index 00000000000..900eae1cfb6 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Resources/config/assets.yml @@ -0,0 +1,3 @@ +js: + 'cron': + - '@OroCronBundle/Resources/public/js/job.js' diff --git a/src/Oro/Bundle/CronBundle/Resources/config/navigation.yml b/src/Oro/Bundle/CronBundle/Resources/config/navigation.yml new file mode 100644 index 00000000000..357cd5cdc70 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Resources/config/navigation.yml @@ -0,0 +1,25 @@ +oro_menu_config: + items: + oro_cron_job: + label: Job Queue + route: oro_cron_job_index + + + oro_cron_job_shortcut: + label: Jobs List + route: oro_cron_job_index + + tree: + application_menu: + children: + system_tab: + children: + oro_cron_job: ~ + + shortcuts: + children: + oro_cron_job_shortcut: ~ + +oro_titles: + oro_cron_job_index: Job Queue + oro_cron_job_view: 'Job "%%command%%" (ID: %%id%%)' diff --git a/src/Oro/Bundle/CronBundle/Resources/config/routing.yml b/src/Oro/Bundle/CronBundle/Resources/config/routing.yml new file mode 100644 index 00000000000..ba4cb741d5b --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Resources/config/routing.yml @@ -0,0 +1,3 @@ +oro_cron_bundle: + resource: "@OroCronBundle/Controller" + type: annotation diff --git a/src/Oro/Bundle/CronBundle/Resources/config/services.yml b/src/Oro/Bundle/CronBundle/Resources/config/services.yml new file mode 100644 index 00000000000..576bb7bc383 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Resources/config/services.yml @@ -0,0 +1,12 @@ +parameters: + oro_cron.job_daemon.class: Oro\Bundle\CronBundle\Job\Daemon + oro_cron.job_manager.class: Oro\Bundle\CronBundle\Entity\Manager\JobManager + +services: + oro_cron.job_daemon: + class: %oro_cron.job_daemon.class% + arguments: [%kernel.root_dir%, %oro_cron.max_jobs%] + + oro_cron.job_manager: + class: %oro_cron.job_manager.class% + arguments: ["@doctrine.orm.entity_manager"] diff --git a/src/Oro/Bundle/CronBundle/Resources/doc/index.rst b/src/Oro/Bundle/CronBundle/Resources/doc/index.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Oro/Bundle/CronBundle/Resources/public/images/loading.gif b/src/Oro/Bundle/CronBundle/Resources/public/images/loading.gif new file mode 100644 index 00000000000..5b33f7e54f4 Binary files /dev/null and b/src/Oro/Bundle/CronBundle/Resources/public/images/loading.gif differ diff --git a/src/Oro/Bundle/CronBundle/Resources/public/js/job.js b/src/Oro/Bundle/CronBundle/Resources/public/js/job.js new file mode 100644 index 00000000000..b961e0372d2 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Resources/public/js/job.js @@ -0,0 +1,70 @@ +$(function() { + $(document).on('click', '#run-daemon, #stop-daemon', function (e) { + var el = $(this), + img = $('#status-daemon').closest('div').find('img'); + + img.show(); + + $.getJSON(el.attr('href'), function (data) { + if (data.error) { + alert(data.message); + } else { + el + .closest('div').find('span:first').toggleClass('label-success label-important').text($.isNumeric(data.message) ? _.__('Running') : _.__('Not running')).end() + .closest('div').find('span:last').text(data.message).end(); + + switchButtons(!$.isNumeric(data.message)); + } + + img.hide(); + }); + + return false; + }); + + $(document).on('click', '.stack-trace a', function (e) { + var el = $(this), + traceCon = el.closest('.stack-trace').find('.traces'), + traceConVis = traceCon.is(':visible'); + + if (el.next('.trace').length) { + el.next('.trace').toggle(); + } else { + $('.traces').hide(); + traceCon.toggle(!traceConVis); + } + + el.find('img').toggleClass('hide'); + + return false; + }); + + setInterval(function () { + var statusBtn = $('#status-daemon'), + img = statusBtn.closest('div').find('img'); + + img.show(); + + $.get(statusBtn.attr('href'), function (data) { + data = parseInt(data); + + statusBtn + .closest('div').find('span:first').removeClass(data > 0 ? 'label-important' : 'label-success').addClass(data > 0 ? 'label-success' : 'label-important').text(data > 0 ? _.__('Running') : _.__('Not running')).end() + .closest('div').find('span:last').text(data > 0 ? data : _.__('N/A')).end(); + + switchButtons(!data); + + img.hide(); + }); + }, 30000); + + function switchButtons(run) { + if (run) { + $('#run-daemon').show(); + $('#stop-daemon').hide(); + } else { + $('#run-daemon').hide(); + $('#stop-daemon').show(); + } + } +}) \ No newline at end of file diff --git a/src/Oro/Bundle/CronBundle/Resources/translations/jsmessages.en.yml b/src/Oro/Bundle/CronBundle/Resources/translations/jsmessages.en.yml new file mode 100644 index 00000000000..f60e8458bd3 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Resources/translations/jsmessages.en.yml @@ -0,0 +1,3 @@ +"Running": "Running" +"Not running": "Not running" +"N/A": "N/A" diff --git a/src/Oro/Bundle/CronBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/CronBundle/Resources/translations/messages.en.yml new file mode 100644 index 00000000000..dce0885120f --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Resources/translations/messages.en.yml @@ -0,0 +1,38 @@ +oro: + cron: + header: + jobs: Jobs + id: ID + command: Command + state: State + created: Created at + runtime: Runtime, s + memory: Memory usage, MB + retry: Retry jobs + trace: Stack trace + output: Output + error: Error output + statistics: Statistics + no_jobs: There are no jobs + na: N/A + sidebar: + status: Daemon status + pid: Daemon PID + running: Running + not_running: Not running + run: Run daemon + stop: Stop daemon + refresh: Refresh list + check: Check status + view: + original: Original Job + related: Related Entities + deps: Dendencies + in_deps: Incoming Dependencies + message: + start: + fail: Failed to start daemon + success: Daemon started + stop: + fail: Failed to stop daemon + success: Daemon stopped diff --git a/src/Oro/Bundle/CronBundle/Resources/views/Job/index.html.twig b/src/Oro/Bundle/CronBundle/Resources/views/Job/index.html.twig new file mode 100644 index 00000000000..5dbbb78748b --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Resources/views/Job/index.html.twig @@ -0,0 +1,54 @@ +{% extends bap.layout %} +{% import 'JMSJobQueueBundle:Job:macros.html.twig' as macros %} +{% use 'OroCronBundle:Job:sidebar.html.twig' %} + +{% block page_container %} +
+

{{ 'oro.cron.header.jobs'|trans }}

+
+
+ {{ block('sidebar') }} +
+
+ {{ block('content') }} +
+
+
+{% endblock page_container %} + +{% block content %} + +{{ knp_pagination_render(pager, 'OroUIBundle::pager.html.twig') }} + + + + + + + + + + + + + + {% for job in pager %} + + + + + + + + + {% else %} + + + + {% endfor %} + +
{{ 'oro.cron.header.id'|trans }}{{ 'oro.cron.header.command'|trans }}{{ 'oro.cron.header.state'|trans }}{{ 'oro.cron.header.created'|trans }}{{ 'oro.cron.header.runtime'|trans }}{{ 'oro.cron.header.memory'|trans }}
{{ job.id }}{{ macros.command(job) }}{{ macros.state(job) }}{{ macros.ago(job.createdAt) }}{{ job.runtime|default('oro.cron.na'|trans) }}{{ (job.memoryUsageReal / 1048576)|number_format(2) }}
{{ 'oro.cron.no_jobs'|trans }}
+ +{{ knp_pagination_render(pager, 'OroUIBundle::pager.html.twig') }} + +{% endblock %} diff --git a/src/Oro/Bundle/CronBundle/Resources/views/Job/sidebar.html.twig b/src/Oro/Bundle/CronBundle/Resources/views/Job/sidebar.html.twig new file mode 100644 index 00000000000..b39a28b8217 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Resources/views/Job/sidebar.html.twig @@ -0,0 +1,16 @@ +{%- block sidebar %} +

+ {{ 'oro.cron.sidebar.status'|trans }}: + {{ pid ? 'oro.cron.sidebar.running'|trans : 'oro.cron.sidebar.not_running'|trans }} + +

+

{{ 'oro.cron.sidebar.pid'|trans }}: {{ pid|default('oro.cron.na'|trans) }}

+ +

+ + +

+

{{ 'oro.cron.sidebar.refresh'|trans }}

+ + +{% endblock -%} diff --git a/src/Oro/Bundle/CronBundle/Resources/views/Job/view.html.twig b/src/Oro/Bundle/CronBundle/Resources/views/Job/view.html.twig new file mode 100644 index 00000000000..92de07adc22 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Resources/views/Job/view.html.twig @@ -0,0 +1,176 @@ +{% extends 'OroUIBundle:actions:view.html.twig' %} +{% import 'JMSJobQueueBundle:Job:macros.html.twig' as macros %} +{% use 'OroCronBundle:Job:sidebar.html.twig' %} + +{% oro_title_set({ params: { '%command%': job.command, '%id%': job.id } }) %} + +{% block pageHeader %} + {% set breadcrumbs = { + entity: job, + indexPath: path('oro_cron_job_index'), + indexLabel: 'Jobs', + entityTitle: 'Job "' ~ job.command ~ '"' + } + %} + {{ parent() }} +{% endblock pageHeader %} + +{% block stats %}{% endblock stats %} + +{% block content_data %} +
+
+
+ {{ block('sidebar') }} +
+
+ + + + + + + + + + + + + + + {% if job.isRetryJob() %} + + + + + {% endif %} + + {% if relatedEntities|length > 0 %} + + + + + {% endif %} + + {% if job.dependencies|length > 0 %} + + + + + {% endif %} + + {% if dependencies|length > 0 %} + + + + + {% endif %} +
{{ 'oro.cron.header.command'|trans }}{{ macros.command(job) }}
{{ 'oro.cron.header.state'|trans }}{{ macros.state(job) }}
{{ 'oro.cron.header.created'|trans }}{{ macros.ago(job.createdAt) }}
{{ 'oro.cron.view.original'|trans }}#{{ job.originalJob.id }} {{ macros.state(job.originalJob) }}
{{ 'oro.cron.view.related'|trans }} + {%- for entity in relatedEntities %} + {%- if entity.raw is jms_job_queue_linkable -%} + {{ entity.raw|jms_job_queue_linkname }} + {%- else -%} + {{ entity.class }} ({{ entity.id }}) + {%- endif -%} + + {% if not loop.last %}, {% endif -%} + {% endfor -%} +
{{ 'oro.cron.view.deps'|trans }} + {%- for dep in job.dependencies -%} + {{ dep.command }} {{ macros.state(dep) }} + {%- if not loop.last %}, {% endif -%} + {%- endfor -%} +
{{ 'oro.cron.view.in_deps'|trans }} + {%- for dep in dependencies -%} + {{ dep.command }} {{ macros.state(dep) }} + {%- endfor -%} +
+ + {% if job.retryJobs|length > 0 %} +

{{ 'oro.cron.header.retry'|trans }}

+ + + + + + + + + + + + {% for retryJob in job.retryJobs %} + + + + + + {% endfor %} + +
{{ 'oro.cron.header.id'|trans }}{{ 'oro.cron.header.created'|trans }}{{ 'oro.cron.header.state'|trans }}
{{ retryJob.id }}{{ macros.ago(retryJob.createdAt) }}{{ macros.state(retryJob) }}
+ {% endif %} + + {% if job.output is not empty %} +

{{ 'oro.cron.header.output'|trans }}

+
{{ job.output }}
+ {% endif %} + + {% if job.errorOutput is not empty %} +

{{ 'oro.cron.header.error'|trans }}

+
{{ job.errorOutput }}
+ {% endif %} + + {% if job.stackTrace is not empty %} +

{{ 'oro.cron.header.trace'|trans }}

+ {% for position, ex in job.stackTrace.toarray %} +
+

+ {{ ex.class|abbr_class }}: {{ ex.message|nl2br|format_file_from_text }}  + {% spaceless %} + + - + + + + {% endspaceless %} +

+
    + {%- for i, trace in ex.trace %} +
  1. + {% if trace.function %} + + {{ trace.short_class }} + {{ trace.type ~ trace.function }} + + ({{ trace.args|format_args }}) + {% endif %} + + {% if trace.file is defined and trace.file and trace.line is defined and trace.line %} + {{ trace.function ? '
    ' : '' }} + {{ trace.file|format_file(trace.line) }}  + + - + + + +
    + {{ trace.file|file_excerpt(trace.line) }} +
    + {% endif %} +
  2. + {% endfor -%} +
+
+ {% endfor %} + {% endif %} + + {% if statistics is not empty %} +

{{ 'oro.cron.header.statistics'|trans }}

+
+ + {% endif %} +
+
+
+{% endblock %} diff --git a/src/Oro/Bundle/CronBundle/Tests/Unit/DependencyInjection/OroCronExtensionTest.php b/src/Oro/Bundle/CronBundle/Tests/Unit/DependencyInjection/OroCronExtensionTest.php new file mode 100644 index 00000000000..4737ec9a255 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Tests/Unit/DependencyInjection/OroCronExtensionTest.php @@ -0,0 +1,87 @@ +createEmptyConfiguration(); + + $this->assertParameter(5, 'oro_cron.max_jobs'); + } + + public function testLoad() + { + $this->createFullConfiguration(); + + $this->assertParameter(10, 'oro_cron.max_jobs'); + } + + protected function createEmptyConfiguration() + { + $this->configuration = new ContainerBuilder(); + + $loader = new OroCronExtension(); + $config = $this->getEmptyConfig(); + + $loader->load(array($config), $this->configuration); + + $this->assertTrue($this->configuration instanceof ContainerBuilder); + } + + protected function createFullConfiguration() + { + $this->configuration = new ContainerBuilder(); + + $loader = new OroCronExtension(); + $config = $this->getFullConfig(); + + $loader->load(array($config), $this->configuration); + + $this->assertTrue($this->configuration instanceof ContainerBuilder); + } + + /** + * @return array + */ + protected function getEmptyConfig() + { + $yaml = ''; + $parser = new Parser(); + + return $parser->parse($yaml); + } + + protected function getFullConfig() + { + $yaml = <<parse($yaml); + } + + /** + * @param mixed $value + * @param string $key + */ + protected function assertParameter($value, $key) + { + $this->assertEquals($value, $this->configuration->getParameter($key), sprintf('%s parameter is correct', $key)); + } + + protected function tearDown() + { + unset($this->configuration); + } +} diff --git a/src/Oro/Bundle/CronBundle/Tests/Unit/Entity/Manager/JobManagerTest.php b/src/Oro/Bundle/CronBundle/Tests/Unit/Entity/Manager/JobManagerTest.php new file mode 100644 index 00000000000..cee5bbb114d --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Tests/Unit/Entity/Manager/JobManagerTest.php @@ -0,0 +1,98 @@ +getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $conn = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + + $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + ->setConstructorArgs(array($em)) + ->enableArgumentCloning() + ->getMock(); + + $expr = $this->getMock('Doctrine\ORM\Query\Expr'); + $class = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata'); + + $qb->expects($this->any())->method('select')->will($this->returnValue($qb)); + $qb->expects($this->any())->method('from')->will($this->returnValue($qb)); + $qb->expects($this->any())->method('where')->will($this->returnValue($qb)); + $qb->expects($this->any())->method('orderBy')->will($this->returnValue($qb)); + $qb->expects($this->any())->method('expr')->will($this->returnValue($expr)); + + $em->expects($this->any()) + ->method('createQueryBuilder') + ->will($this->returnValue($qb)); + + $em->expects($this->any()) + ->method('getConnection') + ->will($this->returnValue($conn)); + + $em->expects($this->any()) + ->method('getClassMetadata') + ->with($this->equalTo('Oro\Bundle\CronBundle\Entity\Schedule')) + ->will($this->returnValue($class)); + + $conn->expects($this->any()) + ->method('query') + ->will($this->returnValue(array( + array( + 'characteristic' => 'memory', + 'createdAt' => '2013-08-16 14:51:08', + 'charValue' => '818759584' + ) + ))); + + $this->object = new Manager\JobManager($em); + $this->job = new Job('oro:test'); + } + + public function testGetListQuery() + { + $this->assertInstanceOf('Doctrine\ORM\QueryBuilder', $this->object->getListQuery()); + } + + public function testGetRelatedEntities() + { + $relEntity = new Schedule(); + + $this->assertInternalType('array', $this->object->getRelatedEntities($this->job)); + $this->assertEmpty($this->object->getRelatedEntities($this->job)); + + $this->job->addRelatedEntity($relEntity); + + $this->assertNotEmpty($this->object->getRelatedEntities($this->job)); + } + + public function testGetJobStatistics() + { + $stat = $this->object->getJobStatistics($this->job); + + $this->assertInternalType('array', $stat); + $this->assertNotEmpty($stat); + $this->assertEquals('Time', $stat[0][0]); + $this->assertEquals('memory', $stat[0][1]); + $this->assertInternalType('float', $stat[1][1]); + $this->assertEquals(780, (int) $stat[1][1]); + } +} diff --git a/src/Oro/Bundle/CronBundle/Tests/Unit/Entity/ScheduleTest.php b/src/Oro/Bundle/CronBundle/Tests/Unit/Entity/ScheduleTest.php new file mode 100644 index 00000000000..ab6e34bccfb --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Tests/Unit/Entity/ScheduleTest.php @@ -0,0 +1,45 @@ +object = new Schedule(); + } + + public function testGetId() + { + $this->assertNull($this->object->getId()); + } + + public function testCommand() + { + $object = $this->object; + $command = 'oro:test'; + + $this->assertEmpty($object->getCommand()); + + $object->setCommand($command); + + $this->assertEquals($command, $object->getCommand()); + } + + public function testDefinition() + { + $object = $this->object; + $def = '*/5 * * * *'; + + $this->assertEmpty($object->getDefinition()); + + $object->setDefinition($def); + + $this->assertEquals($def, $object->getDefinition()); + } +} diff --git a/src/Oro/Bundle/CronBundle/Tests/Unit/Job/DaemonTest.php b/src/Oro/Bundle/CronBundle/Tests/Unit/Job/DaemonTest.php new file mode 100644 index 00000000000..544855322e7 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Tests/Unit/Job/DaemonTest.php @@ -0,0 +1,165 @@ +markTestSkipped('Unable to run on Windows'); + } + + $this->object = $this->getMockBuilder('Oro\Bundle\CronBundle\Job\Daemon') + ->setConstructorArgs(array('app', 10)) + ->setMethods(array('getPidProcess', 'getQueueRunProcess', 'getQueueStopProcess')) + ->getMock(); + + $this->process = $this->getMockBuilder('Symfony\Component\Process\Process') + ->setConstructorArgs(array('echo 1')) + ->getMock(); + + $this->process + ->expects($this->any()) + ->method('run') + ->will($this->returnValue(true)); + + $this->process + ->expects($this->any()) + ->method('start') + ->will($this->returnValue(true)); + + $this->object + ->expects($this->any()) + ->method('getPidProcess') + ->will($this->returnValue($this->process)); + + $this->object + ->expects($this->any()) + ->method('getQueueRunProcess') + ->will($this->returnValue($this->process)); + + $this->object + ->expects($this->any()) + ->method('getQueueStopProcess') + ->with($this->anything()) + ->will($this->returnValue($this->process)); + } + + public function testGetPid() + { + if (defined('PHP_WINDOWS_VERSION_BUILD')) { + $this->markTestSkipped('Unable to run on Windows'); + } + + $this->process + ->expects($this->once()) + ->method('getOutput') + ->will($this->returnValue($this->getPsOutput())); + + $this->assertEquals(111, $this->object->getPid()); + } + + /** + * @expectedException \RuntimeException + */ + public function testAlreadyRun() + { + $this->process + ->expects($this->once()) + ->method('getOutput') + ->will($this->returnValue($this->getPsOutput())); + + $this->object->run(); + } + + public function testFailedRun() + { + $this->process + ->expects($this->once()) + ->method('getOutput') + ->will($this->returnValue('')); + + $this->process + ->expects($this->once()) + ->method('getPid') + ->will($this->returnValue(null)); + + $this->assertNull($this->object->run()); + } + + public function testRun() + { + $this->process + ->expects($this->once()) + ->method('getOutput') + ->will($this->returnValue('')); + + $this->process + ->expects($this->once()) + ->method('getPid') + ->will($this->returnValue(111)); + + $this->assertNotEmpty($this->object->run()); + } + + /** + * @expectedException \RuntimeException + */ + public function testAlreadyStop() + { + $this->process + ->expects($this->once()) + ->method('getOutput') + ->will($this->returnValue('')); + + $this->object->stop(); + } + + public function testFailedStop() + { + $this->process + ->expects($this->once()) + ->method('getOutput') + ->will($this->returnValue($this->getPsOutput())); + + $this->process + ->expects($this->once()) + ->method('isSuccessful') + ->will($this->returnValue(false)); + + $this->assertFalse($this->object->stop()); + } + + public function testStop() + { + $this->process + ->expects($this->once()) + ->method('getOutput') + ->will($this->returnValue($this->getPsOutput())); + + $this->process + ->expects($this->once()) + ->method('isSuccessful') + ->will($this->returnValue(true)); + + $this->assertTrue($this->object->stop()); + } + + protected function getPsOutput() + { + return '111 ? S 0:01 php app/console jms-job-queue:run --max-runtime=999999999 --max-concurrent-jobs=5'; + } +} diff --git a/src/Oro/Bundle/CronBundle/Tests/bootstrap.php b/src/Oro/Bundle/CronBundle/Tests/bootstrap.php new file mode 100644 index 00000000000..82a2b5bd000 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/Tests/bootstrap.php @@ -0,0 +1,9 @@ +=5.3.3", + "symfony/symfony": "2.3.*", + "jms/job-queue-bundle": "dev-master", + "knplabs/knp-paginator-bundle": "dev-master", + "mtdowling/cron-expression": "1.0.*", + "oro/ui-bundle": "dev-master", + "oro/user-bundle": "dev-master", + "oro/navigation-bundle": "dev-master", + "oro/translation-bundle": "dev-master" + }, + "autoload": { + "psr-0": { "Oro\\Bundle\\CronBundle": "" } + }, + "target-dir": "Oro/Bundle/CronBundle", + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + } +} \ No newline at end of file diff --git a/src/Oro/Bundle/CronBundle/phpunit.xml.dist b/src/Oro/Bundle/CronBundle/phpunit.xml.dist new file mode 100644 index 00000000000..1a41559fd81 --- /dev/null +++ b/src/Oro/Bundle/CronBundle/phpunit.xml.dist @@ -0,0 +1,31 @@ + + + + + + + ./Tests + + + + + + . + + ./vendor + ./Resources + ./Tests + + + + \ No newline at end of file diff --git a/src/Oro/Bundle/DataAuditBundle/Controller/AuditController.php b/src/Oro/Bundle/DataAuditBundle/Controller/AuditController.php index 35f1cf906e8..9c029a4da33 100644 --- a/src/Oro/Bundle/DataAuditBundle/Controller/AuditController.php +++ b/src/Oro/Bundle/DataAuditBundle/Controller/AuditController.php @@ -2,10 +2,12 @@ namespace Oro\Bundle\DataAuditBundle\Controller; +use Oro\Bundle\DataAuditBundle\Datagrid\AuditHistoryDatagridManager; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Oro\Bundle\UserBundle\Annotation\Acl; use Oro\Bundle\DataAuditBundle\Entity\Audit; @@ -50,6 +52,7 @@ public function indexAction(Request $request) * requirements={"entity"="[a-zA-Z_]+", "id"="\d+"}, * defaults={"entity"="entity", "id"=0, "_format" = "html"} * ) + * @Template * @Acl( * id="oro_dataaudit_history", * name="View entity history", @@ -72,12 +75,13 @@ public function historyAction($entity, $id) ) ); - $datagrid = $datagridManager->getDatagrid(); + $datagridView = $datagridManager->getDatagrid()->createView(); + if ('json' == $this->getRequest()->getRequestFormat()) { + return $this->get('oro_grid.renderer')->renderResultsJsonResponse($datagridView); + } - $view = 'json' == $this->getRequest()->getRequestFormat() - ? 'OroGridBundle:Datagrid:list.json.php' - : 'OroDataAuditBundle:Audit:history.html.twig'; - - return $this->render($view, array('datagrid' => $datagrid->createView())); + return array( + 'datagrid' => $datagridView, + ); } } diff --git a/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php b/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php index f35e36df63d..6d90c623ff0 100644 --- a/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php +++ b/src/Oro/Bundle/DataAuditBundle/EventListener/EntitySubscriber.php @@ -27,7 +27,6 @@ class EntitySubscriber implements EventSubscriber /** * @param LoggableManager $loggableManager * @param ExtendMetadataFactory $metadataFactory - * @param ConfigProvider $auditConfigProvider */ public function __construct(LoggableManager $loggableManager, ExtendMetadataFactory $metadataFactory) { @@ -60,7 +59,9 @@ public function onFlush(OnFlushEventArgs $event) */ public function loadClassMetadata(LoadClassMetadataEventArgs $event) { - if ($metadata = $this->metadataFactory->extendLoadMetadataForClass($event->getClassMetadata())) { + if ($event->getClassMetadata()->getReflectionClass() + && $metadata = $this->metadataFactory->extendLoadMetadataForClass($event->getClassMetadata()) + ) { $this->loggableManager->addConfig($metadata); } } diff --git a/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php b/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php index bc0569f341a..0ced48717f0 100644 --- a/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php +++ b/src/Oro/Bundle/DataAuditBundle/Loggable/LoggableManager.php @@ -431,7 +431,9 @@ function ($item) { ); } elseif ($newData instanceof \DateTime) { - $oldData = $oldData->format(\DateTime::ISO8601); + if ($oldData instanceof \DateTime) { + $oldData = $oldData->format(\DateTime::ISO8601); + } $newData = $newData->format(\DateTime::ISO8601); } elseif (is_object($newData)) { @@ -509,15 +511,15 @@ protected function checkAuditable($entityClassName) return; } - if ($this->auditConfigProvider->hasConfig($entityClassName) + if ($this->auditConfigProvider->isConfigurable($entityClassName) && $this->auditConfigProvider->getConfig($entityClassName)->is('auditable') ) { $reflection = new \ReflectionClass($entityClassName); $classMetadata = new ClassMetadata($reflection->getName()); foreach ($reflection->getProperties() as $reflectionProperty) { - if ($this->auditConfigProvider->hasFieldConfig($entityClassName, $reflectionProperty->getName()) - && ($fieldConfig = $this->auditConfigProvider->getFieldConfig($entityClassName, $reflectionProperty->getName())) + if ($this->auditConfigProvider->hasConfig($entityClassName, $reflectionProperty->getName()) + && ($fieldConfig = $this->auditConfigProvider->getConfig($entityClassName, $reflectionProperty->getName())) && $fieldConfig->is('auditable') ) { $propertyMetadata = new PropertyMetadata($entityClassName, $reflectionProperty->getName()); diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/config/entity_config.yml b/src/Oro/Bundle/DataAuditBundle/Resources/config/entity_config.yml index ea4e62daa51..89567026d62 100644 --- a/src/Oro/Bundle/DataAuditBundle/Resources/config/entity_config.yml +++ b/src/Oro/Bundle/DataAuditBundle/Resources/config/entity_config.yml @@ -5,7 +5,7 @@ oro_entity_config: auditable: options: priority: 60 - is_bool: true + default_value: true grid: type: boolean label: 'Auditable' @@ -26,7 +26,7 @@ oro_entity_config: auditable: options: priority: 60 - is_bool: true + default_value: true grid: type: boolean label: 'Auditable' diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/config/services.yml b/src/Oro/Bundle/DataAuditBundle/Resources/config/services.yml index 2402392a66b..eeab3f2534e 100644 --- a/src/Oro/Bundle/DataAuditBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/DataAuditBundle/Resources/config/services.yml @@ -14,7 +14,7 @@ services: class: %oro_dataaudit.loggable.loggable_manager.class% arguments: - %oro_dataaudit.loggable.entity.class% - - @oro_dataaudit.config.config_provider + - @oro_entity_config.provider.dataaudit oro_dataaudit.metadata.metadata_factory: class: %oro_dataaudit.metadata.metadata_factory.class% @@ -39,7 +39,3 @@ services: arguments: [@oro_dataaudit.loggable.loggable_manager, @security.context] tags: - { name: kernel.event_listener, event: kernel.request , method: onKernelRequest} - - oro_dataaudit.config.config_provider: - tags: - - { name: oro_entity_config.provider, scope: dataaudit } diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/views/Audit/history.html.twig b/src/Oro/Bundle/DataAuditBundle/Resources/views/Audit/history.html.twig deleted file mode 100644 index 2791b4ab9fd..00000000000 --- a/src/Oro/Bundle/DataAuditBundle/Resources/views/Audit/history.html.twig +++ /dev/null @@ -1,13 +0,0 @@ -{% set gridId = "entity-grid-" ~ random() %} -{% block head_script %} - {% include 'OroGridBundle:Include:javascript.html.twig' with {'datagridView': datagrid, 'selector': '#' ~ gridId} %} - {% include 'OroGridBundle:Include:stylesheet.html.twig' %} -{% endblock %} - -{% block page_container %} -
- {% block content %} -
- {% endblock %} -
-{% endblock %} diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/views/Audit/widget/history.html.twig b/src/Oro/Bundle/DataAuditBundle/Resources/views/Audit/widget/history.html.twig new file mode 100644 index 00000000000..bf0bc88ce3f --- /dev/null +++ b/src/Oro/Bundle/DataAuditBundle/Resources/views/Audit/widget/history.html.twig @@ -0,0 +1,12 @@ +
+ {% set gridId = "entity-grid-" ~ random() %} + + {% include 'OroGridBundle:Include:javascript.html.twig' with {'datagridView': datagrid, 'selector': '#' ~ gridId} %} + {% include 'OroGridBundle:Include:stylesheet.html.twig' %} + +
+ {% block content %} +
+ {% endblock %} +
+
diff --git a/src/Oro/Bundle/DataAuditBundle/Resources/views/change_history_link.html.twig b/src/Oro/Bundle/DataAuditBundle/Resources/views/change_history_link.html.twig index 5f27f7b5b34..2b2344bfffb 100644 --- a/src/Oro/Bundle/DataAuditBundle/Resources/views/change_history_link.html.twig +++ b/src/Oro/Bundle/DataAuditBundle/Resources/views/change_history_link.html.twig @@ -1,6 +1,3 @@ - +
  • {{ 'Change History'|trans }}
  • + diff --git a/src/Oro/Bundle/DataAuditBundle/Tests/Functional/RestDataAuditApiTest.php b/src/Oro/Bundle/DataAuditBundle/Tests/Functional/API/RestDataAuditApiTest.php similarity index 87% rename from src/Oro/Bundle/DataAuditBundle/Tests/Functional/RestDataAuditApiTest.php rename to src/Oro/Bundle/DataAuditBundle/Tests/Functional/API/RestDataAuditApiTest.php index 9b906a80b4c..40a785540d7 100644 --- a/src/Oro/Bundle/DataAuditBundle/Tests/Functional/RestDataAuditApiTest.php +++ b/src/Oro/Bundle/DataAuditBundle/Tests/Functional/API/RestDataAuditApiTest.php @@ -1,6 +1,6 @@ client)) { - $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); - } else { - $this->client->restart(); - } + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); } /** @@ -36,7 +32,10 @@ public function testPreconditions() ToolsAPI::assertJsonResponse($result, 200); $result = ToolsAPI::jsonToArray($result->getContent()); foreach ($result as $audit) { - $this->client->request('DELETE', $this->client->generate('oro_api_delete_audit', array('id' => $audit['id']))); + $this->client->request( + 'DELETE', + $this->client->generate('oro_api_delete_audit', array('id' => $audit['id'])) + ); $result = $this->client->getResponse(); ToolsAPI::assertJsonResponse($result, 204); } @@ -50,7 +49,8 @@ public function testPreconditions() "plainPassword" => '1231231q', "firstName" => "firstName", "lastName" => "lastName", - "rolesCollection" => array("1") + "rolesCollection" => array("1"), + "owner" => "1", ) ); @@ -109,7 +109,10 @@ public function testGetAudit($response) public function testDeleteAudit($response) { foreach ($response as $audit) { - $this->client->request('DELETE', $this->client->generate('oro_api_delete_audit', array('id' => $audit['id']))); + $this->client->request( + 'DELETE', + $this->client->generate('oro_api_delete_audit', array('id' => $audit['id'])) + ); $result = $this->client->getResponse(); ToolsAPI::assertJsonResponse($result, 204); } diff --git a/src/Oro/Bundle/DataAuditBundle/Tests/Functional/SoapDataAuditApiTest.php b/src/Oro/Bundle/DataAuditBundle/Tests/Functional/API/SoapDataAuditApiTest.php similarity index 71% rename from src/Oro/Bundle/DataAuditBundle/Tests/Functional/SoapDataAuditApiTest.php rename to src/Oro/Bundle/DataAuditBundle/Tests/Functional/API/SoapDataAuditApiTest.php index 9f22183b670..45fc5189175 100644 --- a/src/Oro/Bundle/DataAuditBundle/Tests/Functional/SoapDataAuditApiTest.php +++ b/src/Oro/Bundle/DataAuditBundle/Tests/Functional/API/SoapDataAuditApiTest.php @@ -1,6 +1,6 @@ client)) { - $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); - $this->client->soap( - "http://localhost/api/soap", - array( - 'location' => 'http://localhost/api/soap', - 'soap_version' => SOAP_1_2 - ) - ); - - } else { - $this->client->restart(); - } + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); + $this->client->soap( + "http://localhost/api/soap", + array( + 'location' => 'http://localhost/api/soap', + 'soap_version' => SOAP_1_2 + ) + ); } /** @@ -39,7 +34,7 @@ public function setUp() public function testPreconditions() { //clear Audits - $result = $this->client->soapClient->getAudits(); + $result = $this->client->getSoap()->getAudits(); $result = ToolsAPI::classToArray($result); if (!empty($result)) { if (!is_array(reset($result['item']))) { @@ -49,7 +44,7 @@ public function testPreconditions() $result = $result['item']; } foreach ($result as $audit) { - $this->client->soapClient->deleteAudit($audit['id']); + $this->client->getSoap()->deleteAudit($audit['id']); } } @@ -61,10 +56,13 @@ public function testPreconditions() "plainPassword" => '1231231q', "firstName" => "firstName", "lastName" => "lastName", - "rolesCollection" => array("1") + "rolesCollection" => array("1"), + "owner" => "1" ); - $result = $this->client->soapClient->createUser($request); - $this->assertTrue($result, $this->client->soapClient->__getLastResponse()); + + $id = $this->client->getSoap()->createUser($request); + $this->assertInternalType('int', $id, $this->client->getSoap()->__getLastResponse()); + $this->assertGreaterThan(0, $id); return $request; } @@ -76,7 +74,7 @@ public function testPreconditions() */ public function testGetAudits($response) { - $result = $this->client->soapClient->getAudits(); + $result = $this->client->getSoap()->getAudits(); $result = ToolsAPI::classToArray($result); if (!is_array(reset($result['item']))) { @@ -104,7 +102,7 @@ public function testGetAudits($response) public function testGetAudit($response) { foreach ($response as $audit) { - $result = $this->client->soapClient->getAudit($audit['id']); + $result = $this->client->getSoap()->getAudit($audit['id']); $result = ToolsAPI::classToArray($result); unset($result['loggedAt']); unset($audit['loggedAt']); @@ -119,9 +117,9 @@ public function testGetAudit($response) public function testDeleteAudit($response) { foreach ($response as $audit) { - $this->client->soapClient->deleteAudit($audit['id']); + $this->client->getSoap()->deleteAudit($audit['id']); } - $result = $this->client->soapClient->getAudits(); + $result = $this->client->getSoap()->getAudits(); $result = ToolsAPI::classToArray($result); $this->assertEmpty($result); } diff --git a/src/Oro/Bundle/DataAuditBundle/Tests/Functional/ControllersTest.php b/src/Oro/Bundle/DataAuditBundle/Tests/Functional/ControllersTest.php new file mode 100644 index 00000000000..0c88a22df25 --- /dev/null +++ b/src/Oro/Bundle/DataAuditBundle/Tests/Functional/ControllersTest.php @@ -0,0 +1,132 @@ + 'testAdmin', + 'email' => 'test@test.com', + 'firstName' => 'FirstNameAudit', + 'lastName' => 'LastNameAudit', + 'birthday' => '07/01/2013', + 'enabled' => 1, + 'roles' => 'Superadmin', + 'groups' => 'Sales', + 'company' => 'company', + 'gender' => 'Male' + ); + /** + * @var Client + */ + protected $client; + + public function setUp() + { + $this->client = static::createClient( + array(), + array_merge(ToolsAPI::generateBasicHeader(), array('HTTP_X-CSRF-Header' => 1)) + ); + } + + public function prepareFixture() + { + $crawler = $this->client->request('GET', $this->client->generate('oro_user_create')); + $form = $crawler->selectButton('Save and Close')->form(); + $form['oro_user_user_form[enabled]'] = $this->userData['enabled']; + $form['oro_user_user_form[username]'] = $this->userData['username']; + $form['oro_user_user_form[plainPassword][first]'] = 'password'; + $form['oro_user_user_form[plainPassword][second]'] = 'password'; + $form['oro_user_user_form[firstName]'] = $this->userData['firstName']; + $form['oro_user_user_form[lastName]'] = $this->userData['lastName']; + $form['oro_user_user_form[birthday]'] = $this->userData['birthday']; + $form['oro_user_user_form[email]'] = $this->userData['email']; + $form['oro_user_user_form[groups][1]'] = 2; + $form['oro_user_user_form[rolesCollection][2]'] = 4; + $form['oro_user_user_form[values][company][varchar]'] = $this->userData['company']; + $form['oro_user_user_form[owner]'] = 1; + + $this->client->followRedirects(true); + $crawler = $this->client->submit($form); + + $result = $this->client->getResponse(); + ToolsAPI::assertJsonResponse($result, 200, 'text/html; charset=UTF-8'); + $this->assertContains("User successfully saved", $crawler->html()); + } + + public function testIndex() + { + $this->client->request('GET', $this->client->generate('oro_dataaudit_index')); + $result = $this->client->getResponse(); + ToolsAPI::assertJsonResponse($result, 200, 'text/html; charset=UTF-8'); + } + + public function testHistory() + { + $this->prepareFixture(); + $this->client->request( + 'GET', + $this->client->generate('oro_dataaudit_index', array('_format' =>'json')), + array( + 'audit[_filter][objectName][type]' => null, + 'audit[_filter][objectName][value]' => $this->userData['username'], + 'audit[_pager][_page]' => 1, + 'audit[_pager][_per_page]' => 10, + 'audit[_sort_by][action]' => 'ASC') + ); + + $result = $this->client->getResponse(); + ToolsAPI::assertJsonResponse($result, 200); + $result = ToolsAPI::jsonToArray($result->getContent()); + $result = reset($result['data']); + $this->client->request( + 'GET', + $this->client->generate( + 'oro_dataaudit_history', + array( + 'entity' => str_replace('\\', '_', $result['objectClass']), + 'id' => $result['objectId'], + '_format' =>'json' + ) + ) + ); + $result = $this->client->getResponse(); + ToolsAPI::assertJsonResponse($result, 200); + $result = ToolsAPI::jsonToArray($result->getContent()); + $result = reset($result['data']); + + $result['old'] = strip_tags(preg_replace('/()|(\h)/Uis', '', $result['old'])); + $count = 0; + do { + $result['old'] = strip_tags(preg_replace('/\n{2,}/Uis', "\n", $result['old'], -1, $count)); + } while ($count > 0); + $result['new'] = strip_tags(preg_replace('/()|(\h)/Uis', '', $result['new'])); + $count = 0; + do { + $result['new'] = strip_tags(preg_replace('/\n{2,}/Uis', "\n", $result['new'], -1, $count)); + } while ($count > 0); + $result['old'] = explode("\n", trim($result['old'], "\n")); + $result['new'] = explode("\n", trim($result['new'], "\n")); + foreach ($result['old'] as $auditRecord) { + $auditValue = explode(':', $auditRecord); + $this->assertEquals('', $auditValue[1]); + } + + foreach ($result['new'] as $auditRecord) { + $auditValue = explode(':', $auditRecord); + $this->assertEquals($this->userData[$auditValue[0]], $auditValue[1]); + } + + $this->assertEquals('John Doe - admin@example.com', $result['author']); + + } +} diff --git a/src/Oro/Bundle/DataAuditBundle/Tests/Unit/Loggable/LoggableManagerTest.php b/src/Oro/Bundle/DataAuditBundle/Tests/Unit/Loggable/LoggableManagerTest.php index 9ea42160961..e408ebf3447 100644 --- a/src/Oro/Bundle/DataAuditBundle/Tests/Unit/Loggable/LoggableManagerTest.php +++ b/src/Oro/Bundle/DataAuditBundle/Tests/Unit/Loggable/LoggableManagerTest.php @@ -46,7 +46,7 @@ public function setUp() $provider ->expects($this->any()) - ->method('hasConfig') + ->method('isConfigurable') ->will($this->returnValue(false)); $this->loggableManager = new LoggableManager('Oro\Bundle\DataAuditBundle\Entity\Audit', $provider); diff --git a/src/Oro/Bundle/DataFlowBundle/Resources/views/layout.html.twig b/src/Oro/Bundle/DataFlowBundle/Resources/views/layout.html.twig index 0900f44c128..760c048e6dc 100644 --- a/src/Oro/Bundle/DataFlowBundle/Resources/views/layout.html.twig +++ b/src/Oro/Bundle/DataFlowBundle/Resources/views/layout.html.twig @@ -17,7 +17,7 @@ {% endstylesheets %} {% javascripts - '@OroUIBundle/Resources/public/lib/jquery.min.js' + '@OroUIBundle/Resources/public/lib/jquery-1.10.2.js' '@OroUIBundle/Resources/public/lib/less-1.3.1.min.js' '@OroUIBundle/Resources/public/lib/bootstrap.min.js' '@OroUIBundle/Resources/public/js/layout.js' diff --git a/src/Oro/Bundle/EmailBundle/.gitignore b/src/Oro/Bundle/EmailBundle/.gitignore new file mode 100644 index 00000000000..00ae1784e18 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/.gitignore @@ -0,0 +1,2 @@ +/PoC/* +*~ diff --git a/src/Oro/Bundle/EmailBundle/Builder/EmailEntityBatchInterface.php b/src/Oro/Bundle/EmailBundle/Builder/EmailEntityBatchInterface.php new file mode 100644 index 00000000000..e349e376739 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Builder/EmailEntityBatchInterface.php @@ -0,0 +1,15 @@ +emailAddressManager = $emailAddressManager; + $this->emailOwnerProvider = $emailOwnerProvider; + } + + /** + * Register Email object + * + * @param Email $obj + */ + public function addEmail(Email $obj) + { + $this->emails[] = $obj; + } + + /** + * Register EmailAddress object + * + * @param EmailAddress $obj + * @throws \LogicException + */ + public function addAddress(EmailAddress $obj) + { + $key = strtolower($obj->getEmail()); + if (isset($this->addresses[$key])) { + throw new \LogicException(sprintf('The email address "%s" already exists in the batch.', $obj->getEmail())); + } + $this->addresses[$key] = $obj; + } + + /** + * Get EmailAddress if it exists in the batch + * + * @param string $email The email address + * @return EmailAddress|null + */ + public function getAddress($email) + { + $key = strtolower($email); + + return isset($this->addresses[$key]) + ? $this->addresses[$key] + : null; + } + + /** + * Register EmailFolder object + * + * @param EmailFolder $obj + * @throws \LogicException + */ + public function addFolder(EmailFolder $obj) + { + $key = strtolower(sprintf('%s_%s', $obj->getType(), $obj->getName())); + if (isset($this->folders[$key])) { + throw new \LogicException( + sprintf('The folder "%s" (type: %s) already exists in the batch.', $obj->getName(), $obj->getType()) + ); + } + $this->folders[$key] = $obj; + } + + /** + * Get EmailFolder if it exists in the batch + * + * @param string $type The folder type + * @param string $name The folder name + * @return EmailFolder|null + */ + public function getFolder($type, $name) + { + $key = strtolower(sprintf('%s_%s', $type, $name)); + + return isset($this->folders[$key]) + ? $this->folders[$key] + : null; + } + + /** + * Register EmailOrigin object + * + * @param EmailOrigin $obj + * @throws \LogicException + */ + public function addOrigin(EmailOrigin $obj) + { + $key = strtolower($obj->getName()); + if (isset($this->origins[$key])) { + throw new \LogicException(sprintf('The origin "%s" already exists in the batch.', $obj->getName())); + } + $this->origins[$key] = $obj; + } + + /** + * Get EmailOrigin if it exists in the batch + * + * @param string $name The origin name + * @return EmailOrigin|null + */ + public function getOrigin($name) + { + $key = strtolower($name); + + return isset($this->origins[$key]) + ? $this->origins[$key] + : null; + } + + /** + * Tell the given EntityManager to manage this batch + * + * @param EntityManager $em + */ + public function persist(EntityManager $em) + { + $this->persistOrigins($em); + $this->persistFolders($em); + $this->persistAddresses($em); + $this->persistEmails($em); + } + + /** + * Tell the given EntityManager to manage Email objects and all its children in this batch + * + * @param EntityManager $em + */ + protected function persistEmails(EntityManager $em) + { + foreach ($this->emails as $email) { + $em->persist($email); + } + } + + /** + * Tell the given EntityManager to manage EmailAddress objects in this batch + * + * @param EntityManager $em + */ + protected function persistAddresses(EntityManager $em) + { + $repository = $this->emailAddressManager->getEmailAddressRepository($em); + foreach ($this->addresses as $key => $obj) { + /** @var EmailAddress $dbObj */ + $dbObj = $repository->findOneBy(array('email' => $obj->getEmail())); + if ($dbObj === null) { + $obj->setOwner($this->emailOwnerProvider->findEmailOwner($em, $obj->getEmail())); + $em->persist($obj); + } else { + $this->updateAddressReferences($obj, $dbObj); + $this->origins[$key] = $dbObj; + } + } + } + + /** + * Tell the given EntityManager to manage EmailFolder objects in this batch + * + * @param EntityManager $em + */ + protected function persistFolders(EntityManager $em) + { + $repository = $em->getRepository('OroEmailBundle:EmailFolder'); + foreach ($this->folders as $key => $obj) { + /** @var EmailFolder $dbObj */ + $dbObj = $repository->findOneBy(array('name' => $obj->getName(), 'type' => $obj->getType())); + if ($dbObj === null) { + $em->persist($obj); + } else { + $this->updateFolderReferences($obj, $dbObj); + $this->origins[$key] = $dbObj; + } + } + } + + /** + * Tell the given EntityManager to manage EmailOrigin objects in this batch + * + * @param EntityManager $em + */ + protected function persistOrigins(EntityManager $em) + { + $repository = $em->getRepository('OroEmailBundle:EmailOrigin'); + foreach ($this->origins as $key => $obj) { + /** @var EmailOrigin $dbObj */ + $dbObj = $repository->findOneBy(array('name' => $obj->getName())); + if ($dbObj === null) { + $em->persist($obj); + } else { + $this->updateOriginReferences($obj, $dbObj); + $this->origins[$key] = $dbObj; + } + } + } + + /** + * Make sure that all objects in this batch have correct EmailAddress references + * + * @param EmailAddress $old + * @param EmailAddress $new + */ + protected function updateAddressReferences(EmailAddress $old, EmailAddress $new) + { + foreach ($this->emails as $email) { + if ($email->getFromEmailAddress() === $old) { + $email->setFromEmailAddress($new); + } + foreach ($email->getRecipients() as $recipient) { + if ($recipient->getEmailAddress() === $old) { + $recipient->setEmailAddress($new); + } + } + } + } + + /** + * Make sure that all objects in this batch have correct EmailFolder references + * + * @param EmailFolder $old + * @param EmailFolder $new + */ + protected function updateFolderReferences(EmailFolder $old, EmailFolder $new) + { + foreach ($this->emails as $obj) { + if ($obj->getFolder() === $old) { + $obj->setFolder($new); + } + } + } + + /** + * Make sure that all objects in this batch have correct EmailOrigin references + * + * @param EmailOrigin $old + * @param EmailOrigin $new + */ + protected function updateOriginReferences(EmailOrigin $old, EmailOrigin $new) + { + foreach ($this->folders as $obj) { + if ($obj->getOrigin() === $old) { + $obj->setOrigin($new); + } + } + } +} diff --git a/src/Oro/Bundle/EmailBundle/Builder/EmailEntityBuilder.php b/src/Oro/Bundle/EmailBundle/Builder/EmailEntityBuilder.php new file mode 100644 index 00000000000..7bed605a9a1 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Builder/EmailEntityBuilder.php @@ -0,0 +1,319 @@ +batch = $batch; + $this->emailAddressManager = $emailAddressManager; + } + + /** + * Create Email entity object + * + * @param string $subject The email subject + * @param string $from The FROM email address, for example: john@example.com or "John Smith" + * @param string|string[]|null $to The TO email address(es). Example of email address see in description of $from parameter + * @param \DateTime $sentAt The date/time when email sent + * @param \DateTime $receivedAt The date/time when email received + * @param \DateTime $internalDate The date/time an email server returned in INTERNALDATE field + * @param integer $importance The email importance flag. Can be one of *_IMPORTANCE constants of Email class + * @param string|string[]|null $cc The CC email address(es). Example of email address see in description of $from parameter + * @param string|string[]|null $bcc The BCC email address(es). Example of email address see in description of $from parameter + * @return Email + */ + public function email($subject, $from, $to, $sentAt, $receivedAt, $internalDate, $importance = Email::NORMAL_IMPORTANCE, $cc = null, $bcc = null) + { + $result = new Email(); + $result + ->setSubject($subject) + ->setFromName($from) + ->setFromEmailAddress($this->address($from)) + ->setSentAt($sentAt) + ->setReceivedAt($receivedAt) + ->setInternalDate($internalDate) + ->setImportance($importance); + + $this->addRecipients($result, EmailRecipient::TO, $to); + $this->addRecipients($result, EmailRecipient::CC, $cc); + $this->addRecipients($result, EmailRecipient::BCC, $bcc); + + return $result; + } + + /** + * Add recipients to the specified Email object + * + * @param Email $obj The Email object recipients is added to + * @param string $type The recipient type. Can be to, cc or bcc + * @param string $email The email address, for example: john@example.com or "John Smith" + */ + protected function addRecipients(Email $obj, $type, $email) + { + if (!empty($email)) { + if (is_string($email)) { + $obj->addRecipient($this->recipient($type, $email)); + } elseif (is_array($email) || $email instanceof \Traversable) { + foreach ($email as $e) { + $obj->addRecipient($this->recipient($type, $e)); + } + } + } + } + + /** + * Create EmailAddress entity object + * + * @param string $email The email address, for example: john@example.com or "John Smith" + * @return EmailAddress + */ + public function address($email) + { + $pureEmail = EmailUtil::extractPureEmailAddress($email); + $result = $this->batch->getAddress($pureEmail); + if ($result === null) { + $result = $this->emailAddressManager->newEmailAddress() + ->setEmail($pureEmail); + $this->batch->addAddress($result); + } + + return $result; + } + + /** + * Create EmailAttachment entity object + * + * @param string $fileName The attachment file name + * @param string $contentType The attachment content type. It may be any MIME type + * @return EmailAttachment + */ + public function attachment($fileName, $contentType) + { + $result = new EmailAttachment(); + $result + ->setFileName($fileName) + ->setContentType($contentType); + + return $result; + } + + /** + * Create EmailAttachmentContent entity object + * + * @param string $content The attachment content encoded as it is specified in $contentTransferEncoding parameter + * @param string $contentTransferEncoding The attachment content encoding type + * @return EmailAttachmentContent + */ + public function attachmentContent($content, $contentTransferEncoding) + { + $result = new EmailAttachmentContent(); + $result + ->setValue($content) + ->setContentTransferEncoding($contentTransferEncoding); + + return $result; + } + + /** + * Create EmailBody entity object + * + * @param string $content The body content + * @param bool $isHtml Indicate whether the body content is HTML or TEXT + * @param bool $persistent Indicate whether this email body can be removed by the email cache manager or not + * Set false for external email, and false for system email, for example sent by BAP + * @return EmailBody + */ + public function body($content, $isHtml, $persistent = false) + { + $result = new EmailBody(); + $result + ->setContent($content) + ->setBodyIsText(!$isHtml) + ->setPersistent($persistent); + + return $result; + } + + /** + * Create EmailFolder entity object for INBOX folder + * + * @param string $name The name of INBOX folder if known + * @return EmailFolder + */ + public function folderInbox($name = null) + { + return $this->folder(EmailFolder::INBOX, $name !== null ? $name : 'Inbox'); + } + + /** + * Create EmailFolder entity object for SENT folder + * + * @param string $name The name of SENT folder if known + * @return EmailFolder + */ + public function folderSent($name = null) + { + return $this->folder(EmailFolder::SENT, $name !== null ? $name : 'Sent'); + } + + /** + * Create EmailFolder entity object for TRASH folder + * + * @param string $name The name of TRASH folder if known + * @return EmailFolder + */ + public function folderTrash($name = null) + { + return $this->folder(EmailFolder::TRASH, $name !== null ? $name : 'Trash'); + } + + /** + * Create EmailFolder entity object for DRAFTS folder + * + * @param string $name The name of DRAFTS folder if known + * @return EmailFolder + */ + public function folderDrafts($name = null) + { + return $this->folder(EmailFolder::DRAFTS, $name !== null ? $name : 'Drafts'); + } + + /** + * Create EmailFolder entity object for custom folder + * + * @param string $name The name of the folder + * @return EmailFolder + */ + public function folderOther($name) + { + return $this->folder(EmailFolder::OTHER, $name); + } + + /** + * Create EmailFolder entity object + * + * @param string $type The folder type. Can be inbox, sent, trash, drafts or other + * @param string $name The folder name + * @return EmailFolder + */ + protected function folder($type, $name) + { + $result = $this->batch->getFolder($type, $name); + if ($result === null) { + $result = new EmailFolder(); + $result + ->setType($type) + ->setName($name); + $this->batch->addFolder($result); + } + + return $result; + } + + /** + * Create EmailOrigin entity object + * + * @param string $name The email origin name + * @return EmailOrigin + */ + public function origin($name) + { + $result = $this->batch->getOrigin($name); + if ($result === null) { + $result = new EmailOrigin(); + $result->setName($name); + $this->batch->addOrigin($result); + } + + return $result; + } + + /** + * Create EmailRecipient entity object to store TO field + * + * @param string $email The email address, for example: john@example.com or "John Smith" + * @return EmailRecipient + */ + public function recipientTo($email) + { + return $this->recipient(EmailRecipient::TO, $email); + } + + /** + * Create EmailRecipient entity object to store CC field + * + * @param string $email The email address, for example: john@example.com or "John Smith" + * @return EmailRecipient + */ + public function recipientCc($email) + { + return $this->recipient(EmailRecipient::CC, $email); + } + + /** + * Create EmailRecipient entity object to store BCC field + * + * @param string $email The email address, for example: john@example.com or "John Smith" + * @return EmailRecipient + */ + public function recipientBcc($email) + { + return $this->recipient(EmailRecipient::BCC, $email); + } + + /** + * Create EmailRecipient entity object + * + * @param string $type The recipient type. Can be to, cc or bcc + * @param string $email The email address, for example: john@example.com or "John Smith" + * @return EmailRecipient + */ + protected function recipient($type, $email) + { + $result = new EmailRecipient(); + + return $result + ->setType($type) + ->setName($email) + ->setEmailAddress($this->address($email)); + } + + /** + * Get built batch contains all entities managed by this builder + * + * @return EmailEntityBatchInterface + */ + public function getBatch() + { + return $this->batch; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Cache/EmailCacheManager.php b/src/Oro/Bundle/EmailBundle/Cache/EmailCacheManager.php new file mode 100644 index 00000000000..dc1acf17940 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Cache/EmailCacheManager.php @@ -0,0 +1,98 @@ +em = $em; + } + + /** + * Check that email body is cached. If not load it through an email service connector and add it to a cache + * + * @param Email $email + */ + public function ensureEmailBodyCached(Email $email) + { + if ($email->getEmailBody() !== null) { + // The email body is already cached + return; + } + + // TODO: implement getting email details through correct connector here + + //$emailOriginName = $email->getFolder()->getOrigin()->getName(); + //$connector = $this->get(sprintf('oro_%s.connector', $emailOriginName)); + + $emailBody = new EmailBody(); + $emailBody + ->setHeader($email) + ->setContent("\n

    Sample Email Body

    \n some text \n some text \n some text \n some text \n some text"); + + $emailBody->addAttachment( + $this->createEmailAttachment( + 'sample attachment file.txt', + 'text/plain', + 'binary', + 'some text' + ) + ); + + $emailBody->addAttachment( + $this->createEmailAttachment( + 'sample attachment file (base64).txt', + 'text/plain', + 'base64', + 'some text' + ) + ); + + $email->setEmailBody($emailBody); + + $this->em->persist($email); + $this->em->flush(); + } + + /** + * Create CreateEmailAttachment object + * + * @param string $fileName + * @param string $contentType + * @param string $contentTransferEncoding + * @param string $content + * @return EmailAttachment + */ + protected function createEmailAttachment($fileName, $contentType, $contentTransferEncoding, $content) + { + $emailAttachmentContent = new EmailAttachmentContent(); + $emailAttachmentContent + ->setContentTransferEncoding($contentTransferEncoding) + ->setValue($content); + + $emailAttachment = new EmailAttachment(); + $emailAttachment + ->setFileName($fileName) + ->setContentType($contentType) + ->setContent($emailAttachmentContent); + + return $emailAttachment; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Cache/EntityCacheClearer.php b/src/Oro/Bundle/EmailBundle/Cache/EntityCacheClearer.php new file mode 100644 index 00000000000..0738c809716 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Cache/EntityCacheClearer.php @@ -0,0 +1,73 @@ +entityCacheDir = $entityCacheDir; + $this->entityCacheNamespace = $entityCacheNamespace; + $this->entityProxyNameTemplate = $entityProxyNameTemplate; + } + + /** + * {inheritdoc} + */ + public function clear($cacheDir) + { + $fs = $this->createFilesystem(); + + $entityCacheDir = sprintf('%s/%s', $this->entityCacheDir, str_replace('\\', '/', $this->entityCacheNamespace)); + + $this->clearEmailAddressCache($entityCacheDir, $fs); + } + + /** + * Create Filesystem object + * + * @return Filesystem + */ + protected function createFilesystem() + { + return new Filesystem(); + } + + /** + * Clear a proxy class for EmailAddress entity and save it in cache + * + * @param string $entityCacheDir + * @param Filesystem $fs + */ + protected function clearEmailAddressCache($entityCacheDir, Filesystem $fs) + { + $className = sprintf($this->entityProxyNameTemplate, 'EmailAddress'); + $fs->remove(sprintf('%s/%s.php', $entityCacheDir, $className)); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Cache/EntityCacheWarmer.php b/src/Oro/Bundle/EmailBundle/Cache/EntityCacheWarmer.php new file mode 100644 index 00000000000..460f1baba73 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Cache/EntityCacheWarmer.php @@ -0,0 +1,137 @@ +getProviders() as $provider) { + $this->emailOwnerClasses[count($this->emailOwnerClasses) + 1] = $provider->getEmailOwnerClass(); + } + + $this->entityCacheDir = $entityCacheDir; + $this->entityCacheNamespace = $entityCacheNamespace; + $this->entityProxyNameTemplate = $entityProxyNameTemplate; + } + + /** + * {inheritdoc} + */ + public function warmUp($cacheDir) + { + $fs = $this->createFilesystem(); + $twig = $this->createTwigEnvironment(); + + $entityCacheDir = sprintf('%s/%s', $this->entityCacheDir, str_replace('\\', '/', $this->entityCacheNamespace)); + + // Ensure the cache directory exists + if (!$fs->exists($entityCacheDir)) { + $fs->mkdir($entityCacheDir, 0777); + } + + $this->processEmailAddressTemplate($entityCacheDir, $twig); + } + + /** + * {inheritdoc} + */ + public function isOptional() + { + return false; + } + + /** + * Create Filesystem object + * + * @return Filesystem + */ + protected function createFilesystem() + { + return new Filesystem(); + } + + /** + * Create Twig_Environment object + * + * @return \Twig_Environment + */ + protected function createTwigEnvironment() + { + $entityTemplateDir = __DIR__ . '/../Resources/cache/Entity'; + return new \Twig_Environment(new \Twig_Loader_Filesystem($entityTemplateDir)); + } + + /** + * Create a proxy class for EmailAddress entity and save it in cache + * + * @param string $entityCacheDir + * @param \Twig_Environment $twig + */ + protected function processEmailAddressTemplate($entityCacheDir, \Twig_Environment $twig) + { + $args = array(); + foreach ($this->emailOwnerClasses as $key => $emailOwnerClass) { + $prefix = strtolower(substr($emailOwnerClass, 0, strpos($emailOwnerClass, '\\'))); + if ($prefix === 'oro' || $prefix === 'orocrm') { + // do not use prefix if email's owner is a part of BAP and CRM + $prefix = ''; + } else { + $prefix .= '_'; + } + $suffix = strtolower(substr($emailOwnerClass, strrpos($emailOwnerClass, '\\') + 1)); + + $args[] = array( + 'targetEntity' => $emailOwnerClass, + 'columnName' => sprintf('owner_%s%s_id', $prefix, $suffix), + 'fieldName' => sprintf('owner%d', $key) + ); + } + + $className = sprintf($this->entityProxyNameTemplate, 'EmailAddress'); + $content = $twig->render( + 'EmailAddress.php.twig', + array( + 'namespace' => $this->entityCacheNamespace, + 'className' => $className, + 'owners' => $args + ) + ); + + $this->writeCacheFile(sprintf('%s/%s.php', $entityCacheDir, $className), $content); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailController.php b/src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailController.php new file mode 100644 index 00000000000..13e0d1affb4 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailController.php @@ -0,0 +1,71 @@ +getRequest()->get('page', 1); + $limit = (int)$this->getRequest()->get('limit', self::ITEMS_PER_PAGE); + + return $this->handleGetListRequest($page, $limit); + } + + /** + * REST GET item + * + * @param string $id + * + * @ApiDoc( + * description="Get email", + * resource=true + * ) + * @AclAncestor("oro_email_view") + * @return Response + */ + public function getAction($id) + { + return $this->handleGetRequest($id); + } + + /** + * Get entity manager + * + * @return EmailApiEntityManager + */ + public function getManager() + { + return $this->container->get('oro_email.manager.email.api'); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailTemplateController.php b/src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailTemplateController.php index d86c546bc4b..b53aaa4958b 100644 --- a/src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailTemplateController.php +++ b/src/Oro/Bundle/EmailBundle/Controller/Api/Rest/EmailTemplateController.php @@ -4,6 +4,7 @@ use FOS\Rest\Util\Codes; use Nelmio\ApiDocBundle\Annotation\ApiDoc; +use FOS\RestBundle\Controller\Annotations\Get as GetRoute; use FOS\RestBundle\Controller\Annotations\NamePrefix; use FOS\RestBundle\Controller\Annotations\RouteResource; @@ -13,6 +14,7 @@ use Oro\Bundle\UserBundle\Annotation\Acl; use Oro\Bundle\UserBundle\Annotation\AclAncestor; use Oro\Bundle\SoapBundle\Form\Handler\ApiFormHandler; +use Oro\Bundle\EmailBundle\Provider\VariablesProvider; use Oro\Bundle\SoapBundle\Entity\Manager\ApiEntityManager; use Oro\Bundle\SoapBundle\Controller\Api\Rest\RestController; use Oro\Bundle\EmailBundle\Entity\Repository\EmailTemplateRepository; @@ -93,6 +95,32 @@ public function getAction($entityName = null) ); } + /** + * REST GET available variables by entity name + * + * @param string $entityName + * + * @ApiDoc( + * description="Get available variables by entity name", + * resource=true + * ) + * @AclAncestor("oro_email_emailtemplate_update") + * @GetRoute(requirements={"entityName"="(.*)"}) + * @return Response + */ + public function getAvailableVariablesAction($entityName = null) + { + $entityName = str_replace('_', '\\', $entityName); + + /** @var VariablesProvider $provider */ + $provider = $this->get('oro_email.provider.variable_provider'); + $allowedData = $provider->getTemplateVariables($entityName); + + return $this->handleView( + $this->view($allowedData, Codes::HTTP_OK) + ); + } + /** * Get entity Manager * diff --git a/src/Oro/Bundle/EmailBundle/Controller/Api/Soap/EmailController.php b/src/Oro/Bundle/EmailBundle/Controller/Api/Soap/EmailController.php new file mode 100644 index 00000000000..995d0ab979e --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Controller/Api/Soap/EmailController.php @@ -0,0 +1,100 @@ +handleGetListRequest($page, $limit); + } + + /** + * @Soap\Method("getEmail") + * @Soap\Param("id", phpType = "int") + * @Soap\Result(phpType = "Oro\Bundle\EmailBundle\Entity\Email") + * @AclAncestor("oro_email_view") + */ + public function getAction($id) + { + return $this->handleGetRequest($id); + } + + /** + * @Soap\Method("getEmailBody") + * @Soap\Param("id", phpType="int") + * @Soap\Result(phpType="Oro\Bundle\EmailBundle\Entity\EmailBody") + * @AclAncestor("oro_email_view") + */ + public function getEmailBodyAction($id) + { + $entity = $this->getEntity($id); + $this->getEmailCacheManager()->ensureEmailBodyCached($entity); + + return $entity->getEmailBody(); + } + + /** + * @Soap\Method("getEmailAttachment") + * @Soap\Param("id", phpType="int") + * @Soap\Result(phpType="Oro\Bundle\EmailBundle\Entity\EmailAttachmentContent") + * @AclAncestor("oro_email_view") + */ + public function getEmailAttachment($id) + { + return $this->getEmailAttachmentContentEntity($id); + } + + /** + * Get email attachment by identifier. + * + * @param integer $attachmentId + * @return EmailAttachmentContent + * @throws \SoapFault + */ + protected function getEmailAttachmentContentEntity($attachmentId) + { + $attachment = $this->getManager()->findEmailAttachment($attachmentId); + + if (!$attachment) { + throw new \SoapFault('NOT_FOUND', sprintf('Record #%u can not be found', $attachmentId)); + } + + return $attachment->getContent(); + } + + /** + * Get entity manager + * + * @return EmailApiEntityManager + */ + public function getManager() + { + return $this->container->get('oro_email.manager.email.api'); + } + + /** + * Get email cache manager + * + * @return EmailCacheManager + */ + protected function getEmailCacheManager() + { + return $this->container->get('oro_email.email.cache.manager'); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Controller/EmailController.php b/src/Oro/Bundle/EmailBundle/Controller/EmailController.php new file mode 100644 index 00000000000..356bd59f3cc --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Controller/EmailController.php @@ -0,0 +1,147 @@ +getEmailCacheManager()->ensureEmailBodyCached($entity); + + return array( + 'entity' => $entity + ); + } + + /** + * Get email list + * TODO: This is a temporary action created for demo purposes. It will be removed when 'display activities' + * functionality is implemented + * + * @AclAncestor("oro_email_view") + * @Template + */ + public function activitiesAction($emails) + { + /** @var $emailRepository EmailRepository */ + $emailRepository = $this->getDoctrine()->getRepository('OroEmailBundle:Email'); + + $emails = $this->extractEmailAddresses($emails); + $rows = empty($emails) + ? array() + : $emailRepository->getEmailListQueryBuilder($emails)->getQuery()->execute(); + + return array( + 'entities' => $rows + ); + } + + /** + * Get the given email body content + * + * @Route("/body/{id}", name="oro_email_body", requirements={"id"="\d+"}) + * @AclAncestor("oro_email_view") + */ + public function bodyAction(EmailBody $entity) + { + return new Response($entity->getContent()); + } + + /** + * Get a response for download the given email attachment + * + * @Route("/attachment/{id}", name="oro_email_attachment", requirements={"id"="\d+"}) + * @AclAncestor("oro_email_view") + */ + public function attachmentAction(EmailAttachment $entity) + { + $response = new Response(); + $response->headers->set('Content-Type', $entity->getContentType()); + $response->headers->set('Content-Disposition', sprintf('attachment; filename="%s"', $entity->getFileName())); + $response->headers->set('Content-Transfer-Encoding', $entity->getContent()->getContentTransferEncoding()); + $response->headers->set('Pragma', 'no-cache'); + $response->headers->set('Expires', '0'); + $response->setContent($entity->getContent()->getValue()); + + return $response; + } + + /** + * Get email cache manager + * + * @return EmailCacheManager + */ + protected function getEmailCacheManager() + { + return $this->container->get('oro_email.email.cache.manager'); + } + + /** + * Extract email addresses from the given argument. + * Always return an array, even if no any email is given. + * + * @param $emails + * @return string[] + * @throws \InvalidArgumentException + */ + protected function extractEmailAddresses($emails) + { + if (is_string($emails)) { + return empty($emails) + ? array() + : array($emails); + } + if (!is_array($emails) && !($emails instanceof \Traversable)) { + throw new \InvalidArgumentException('The emails argument must be a string, array or collection.'); + } + + $result = array(); + foreach ($emails as $email) { + if (is_string($email)) { + $result[] = $email; + } elseif ($email instanceof EmailInterface) { + $result[] = $email->getEmail(); + } else { + throw new \InvalidArgumentException( + 'Each item of the emails collection must be a string or an object of EmailInterface.' + ); + } + } + + return $result; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Controller/EmailTemplateController.php b/src/Oro/Bundle/EmailBundle/Controller/EmailTemplateController.php index b9414cf2c07..53234d5fa94 100644 --- a/src/Oro/Bundle/EmailBundle/Controller/EmailTemplateController.php +++ b/src/Oro/Bundle/EmailBundle/Controller/EmailTemplateController.php @@ -2,7 +2,9 @@ namespace Oro\Bundle\EmailBundle\Controller; +use Doctrine\ORM\EntityManager; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\Form\FormInterface; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; @@ -113,4 +115,44 @@ public function cloneAction(EmailTemplate $entity) { return $this->updateAction(clone $entity, true); } + + /** + * @Route("/preview/{id}", requirements={"id"="\d+"}, defaults={"id"=0})) + * @Acl( + * id="oro_email_emailtemplate_preview", + * name="Preview email template", + * description="Preview email template", + * parent="oro_email_emailtemplate" + * ) + * @Template("OroEmailBundle:EmailTemplate:preview.html.twig") + * @param bool|int $emailTemplateId + * @return array + */ + public function previewAction($emailTemplateId = false) + { + if (!$emailTemplateId) { + $emailTemplate = new EmailTemplate(); + } else { + /** @var EntityManager $em */ + $em = $this->get('doctrine.orm.entity_manager'); + $em->getRepository('Oro\Bundle\EmailBundle\Entity\EmailTemplate')->find($emailTemplateId); + } + + /** @var FormInterface $form */ + $form = $this->get('oro_email.form.emailtemplate'); + $form->setData($emailTemplate); + $request = $this->get('request'); + + if (in_array($request->getMethod(), array('POST', 'PUT'))) { + $form->submit($request); + } + + list ($subjectRendered, $templateRendered) = $this->get('oro_email.email_renderer') + ->compileMessage($emailTemplate); + + return array( + 'subject' => $subjectRendered, + 'content' => $templateRendered, + ); + } } diff --git a/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailOriginData.php b/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailOriginData.php new file mode 100644 index 00000000000..fc4071207ef --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/DataFixtures/ORM/LoadEmailOriginData.php @@ -0,0 +1,24 @@ +setName('BAP'); + + $manager->persist($emailOrigin); + $manager->flush(); + } +} diff --git a/src/Oro/Bundle/EmailBundle/DataFixtures/data/emails/user/update_user.html.twig b/src/Oro/Bundle/EmailBundle/DataFixtures/data/emails/user/update_user.html.twig index 610c5c18c74..08f74a2ffcf 100644 --- a/src/Oro/Bundle/EmailBundle/DataFixtures/data/emails/user/update_user.html.twig +++ b/src/Oro/Bundle/EmailBundle/DataFixtures/data/emails/user/update_user.html.twig @@ -1,5 +1,5 @@ @entityName = Oro\Bundle\UserBundle\Entity\User -@subject = Subject {{ entity.username }} +@subject = Subject @isSystem = 1 -

    Some dude updated user '{{ entity.username }}'

    \ No newline at end of file +

    Some dude updated user

    diff --git a/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php b/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php index 87d2a23dfab..eacf9f95f69 100644 --- a/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php +++ b/src/Oro/Bundle/EmailBundle/Datagrid/EmailTemplateDatagridManager.php @@ -4,6 +4,9 @@ use Doctrine\ORM\QueryBuilder; +use Oro\Bundle\GridBundle\Action\MassAction\DeleteMassAction; +use Oro\Bundle\GridBundle\Datagrid\ResultRecordInterface; +use Oro\Bundle\GridBundle\Property\ActionConfigurationProperty; use Oro\Bundle\GridBundle\Property\UrlProperty; use Oro\Bundle\GridBundle\Action\ActionInterface; use Oro\Bundle\GridBundle\Field\FieldDescription; @@ -39,6 +42,14 @@ protected function getProperties() new UrlProperty('update_link', $this->router, 'oro_email_emailtemplate_update', array('id')), new UrlProperty('clone_link', $this->router, 'oro_email_emailtemplate_clone', array('id')), new UrlProperty('delete_link', $this->router, 'oro_api_delete_emailtemplate', array('id')), + new ActionConfigurationProperty( + function (ResultRecordInterface $record) { + if ($record->getValue('isSystem')) { + return array('delete' => false); + } + return null; + } + ) ); } @@ -47,25 +58,11 @@ protected function getProperties() */ protected function configureFields(FieldDescriptionCollection $fieldsCollection) { - $fieldId = new FieldDescription(); - $fieldId->setName('id'); - $fieldId->setOptions( - array( - 'type' => FieldDescriptionInterface::TYPE_INTEGER, - 'label' => $this->translate('ID'), - 'field_name' => 'id', - 'filter_type' => FilterInterface::TYPE_NUMBER, - 'show_column' => false - ) - ); - $fieldsCollection->add($fieldId); - /*----------------------------------------------------------------*/ - $fieldEntityName = new FieldDescription(); $fieldEntityName->setName('entityName'); $fieldEntityName->setOptions( array( - 'type' => FieldDescriptionInterface::TYPE_TEXT, + 'type' => FieldDescriptionInterface::TYPE_HTML, 'label' => $this->translate('oro.email.datagrid.emailtemplate.column.entity_name'), 'field_name' => 'entityName', 'filter_type' => FilterInterface::TYPE_CHOICE, diff --git a/src/Oro/Bundle/EmailBundle/DependencyInjection/Compiler/EmailOwnerConfigurationPass.php b/src/Oro/Bundle/EmailBundle/DependencyInjection/Compiler/EmailOwnerConfigurationPass.php new file mode 100644 index 00000000000..6b56b39865d --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/DependencyInjection/Compiler/EmailOwnerConfigurationPass.php @@ -0,0 +1,76 @@ +hasDefinition(self::SERVICE_KEY)) { + return; + } + $storageDefinition = $container->getDefinition(self::SERVICE_KEY); + + $providers = $this->loadProviders($container); + foreach ($providers as $providerServiceId) { + $storageDefinition->addMethodCall('addProvider', array(new Reference($providerServiceId))); + } + + $this->setEmailAddressEntityResolver($container); + } + + /** + * Load services implements an email owner providers + * + * @param ContainerBuilder $container + * @return array + */ + protected function loadProviders(ContainerBuilder $container) + { + $taggedServices = $container->findTaggedServiceIds(self::TAG); + $providers = array(); + foreach ($taggedServices as $id => $tagAttributes) { + $order = PHP_INT_MAX; + foreach ($tagAttributes as $attributes) { + if (!empty($attributes['order'])) { + $order = (int)$attributes['order']; + break; + } + } + $providers[$order] = $id; + } + ksort($providers); + + return $providers; + } + + /** + * Register a proxy of EmailAddress entity in doctrine ORM + * + * @param ContainerBuilder $container + */ + protected function setEmailAddressEntityResolver(ContainerBuilder $container) + { + if ($container->hasDefinition('doctrine.orm.listeners.resolve_target_entity')) { + $targetEntityResolver = $container->getDefinition('doctrine.orm.listeners.resolve_target_entity'); + $targetEntityResolver->addMethodCall( + 'addResolveTargetEntity', + array( + 'Oro\Bundle\EmailBundle\Entity\EmailAddress', + sprintf('%s\EmailAddressProxy', $container->getParameter('oro_email.entity.cache_namespace')), + array() + ) + ); + } + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Email.php b/src/Oro/Bundle/EmailBundle/Entity/Email.php new file mode 100644 index 00000000000..11d475ff630 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Email.php @@ -0,0 +1,508 @@ +importance = self::NORMAL_IMPORTANCE; + $this->recipients = new ArrayCollection(); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Get entity created date/time + * + * @return \DateTime + */ + public function getCreatedAt() + { + return $this->created; + } + + /** + * Get email subject + * + * @return string + */ + public function getSubject() + { + return $this->subject; + } + + /** + * Set email subject + * + * @param string $subject + * @return $this + */ + public function setSubject($subject) + { + $this->subject = $subject; + + return $this; + } + + /** + * Get FROM email name + * + * @return string + */ + public function getFromName() + { + return $this->fromName; + } + + /** + * Set FROM email name + * + * @param string $fromName + * @return $this + */ + public function setFromName($fromName) + { + $this->fromName = $fromName; + + return $this; + } + + /** + * Get FROM email address + * + * @return EmailAddress + */ + public function getFromEmailAddress() + { + return $this->fromEmailAddress; + } + + /** + * Set FROM email address + * + * @param EmailAddress $fromEmailAddress + * @return $this + */ + public function setFromEmailAddress(EmailAddress $fromEmailAddress) + { + $this->fromEmailAddress = $fromEmailAddress; + + return $this; + } + + /** + * Get email recipients + * + * @param null|string $recipientType null to get all recipients, + * or 'to', 'cc' or 'bcc' if you need specific type of recipients + * @return EmailRecipient[] + */ + public function getRecipients($recipientType = null) + { + if ($recipientType === null) { + return $this->recipients; + } + + return $this->recipients->filter( + function ($recipient) use ($recipientType) { + /** @var EmailRecipient $recipient */ + return $recipient->getType() === $recipientType; + } + ); + } + + /** + * Add folder + * + * @param EmailRecipient $recipient + * @return $this + */ + public function addRecipient(EmailRecipient $recipient) + { + $this->recipients[] = $recipient; + + $recipient->setEmail($this); + + return $this; + } + + /** + * Get date/time when email received + * + * @return \DateTime + */ + public function getReceivedAt() + { + return $this->receivedAt; + } + + /** + * Set date/time when email received + * + * @param \DateTime $receivedAt + * @return $this + */ + public function setReceivedAt($receivedAt) + { + $this->receivedAt = $receivedAt; + + return $this; + } + + /** + * Get date/time when email sent + * + * @return \DateTime + */ + public function getSentAt() + { + return $this->sentAt; + } + + /** + * Set date/time when email sent + * + * @param \DateTime $sentAt + * @return $this + */ + public function setSentAt($sentAt) + { + $this->sentAt = $sentAt; + + return $this; + } + + /** + * Get email importance + * + * @return integer Can be one of *_IMPORTANCE constants + */ + public function getImportance() + { + return $this->importance; + } + + /** + * Set email importance + * + * @param integer $importance Can be one of *_IMPORTANCE constants + * @return $this + */ + public function setImportance($importance) + { + $this->importance = $importance; + + return $this; + } + + /** + * Get email internal date receives from an email server + * + * @return \DateTime + */ + public function getInternalDate() + { + return $this->internalDate; + } + + /** + * Set email internal date receives from an email server + * + * @param \DateTime $internalDate + * @return $this + */ + public function setInternalDate($internalDate) + { + $this->internalDate = $internalDate; + + return $this; + } + + /** + * Get value of email Message-ID header + * + * @return string + */ + public function getMessageId() + { + return $this->messageId; + } + + /** + * Set value of email Message-ID header + * + * @param string $messageId + * @return $this + */ + public function setMessageId($messageId) + { + $this->messageId = $messageId; + + return $this; + } + + /** + * Get email message id uses for group related messages + * + * @return string + */ + public function getXMessageId() + { + return $this->xMessageId; + } + + /** + * Set email message id uses for group related messages + * + * @param string $xMessageId + * @return $this + */ + public function setXMessageId($xMessageId) + { + $this->xMessageId = $xMessageId; + + return $this; + } + + /** + * Get email thread id uses for group related messages + * + * @return string + */ + public function getXThreadId() + { + return $this->xThreadId; + } + + /** + * Set email thread id uses for group related messages + * + * @param string $xThreadId + * @return $this + */ + public function setXThreadId($xThreadId) + { + $this->xThreadId = $xThreadId; + + return $this; + } + + /** + * Get email folder + * + * @return EmailFolder + */ + public function getFolder() + { + return $this->folder; + } + + /** + * Set email folder + * + * @param EmailFolder $folder + * @return $this + */ + public function setFolder(EmailFolder $folder) + { + $this->folder = $folder; + + return $this; + } + + /** + * Get cached email body + * + * @return EmailBody + */ + public function getEmailBody() + { + return $this->emailBody; + } + + /** + * Set email body + * + * @param EmailBody $emailBody + * @return $this + */ + public function setEmailBody(EmailBody $emailBody) + { + $this->emailBody = $emailBody; + + return $this; + } + + /** + * Pre persist event listener + * + * @ORM\PrePersist + */ + public function beforeSave() + { + $this->created = EmailUtil::currentUTCDateTime(); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php b/src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php new file mode 100644 index 00000000000..58136a1018b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailAddress.php @@ -0,0 +1,131 @@ +id; + } + + /** + * Get entity created date/time + * + * @return \DateTime + */ + public function getCreatedAt() + { + return $this->created; + } + + /** + * Get entity updated date/time + * + * @return \DateTime + */ + public function getUpdatedAt() + { + return $this->updated; + } + + /** + * Get email address. + * + * @return string + */ + public function getEmail() + { + return $this->email; + } + + /** + * Set email address. + * + * @param string $email + * @return $this + */ + public function setEmail($email) + { + $this->email = $email; + + return $this; + } + + /** + * Get email owner + * + * @return EmailOwnerInterface + */ + public function getOwner() + { + return null; + } + + /** + * Set email owner + * + * @param EmailOwnerInterface|null $owner + * @return $this + */ + public function setOwner(EmailOwnerInterface $owner = null) + { + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php b/src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php new file mode 100644 index 00000000000..11db7b89793 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailAttachment.php @@ -0,0 +1,168 @@ +id; + } + + /** + * Get attachment file name + * + * @return string + */ + public function getFileName() + { + return $this->fileName; + } + + /** + * Set attachment file name + * + * @param string $fileName + * @return $this + */ + public function setFileName($fileName) + { + $this->fileName = $fileName; + + return $this; + } + + /** + * Get content type. It may be any MIME type + * + * @return string + */ + public function getContentType() + { + return $this->contentType; + } + + /** + * Set content type + * + * @param string $contentType any MIME type + * @return $this + */ + public function setContentType($contentType) + { + $this->contentType = $contentType; + + return $this; + } + + /** + * Get content of email attachment + * + * @return EmailAttachmentContent + */ + public function getContent() + { + return $this->attachmentContent; + } + + /** + * Set content of email attachment + * + * @param EmailAttachmentContent $attachmentContent + * @return $this + */ + public function setContent(EmailAttachmentContent $attachmentContent) + { + $this->attachmentContent = $attachmentContent; + + $attachmentContent->setEmailAttachment($this); + + return $this; + } + + /** + * Get email body + * + * @return EmailBody + */ + public function getEmailBody() + { + return $this->emailBody; + } + + /** + * Set email body + * + * @param EmailBody $emailBody + * @return $this + */ + public function setEmailBody(EmailBody $emailBody) + { + $this->emailBody = $emailBody; + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailAttachmentContent.php b/src/Oro/Bundle/EmailBundle/Entity/EmailAttachmentContent.php new file mode 100644 index 00000000000..a3ab42eb4cd --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailAttachmentContent.php @@ -0,0 +1,133 @@ +id; + } + + /** + * Get email attachment owner + * + * @return EmailAttachment + */ + public function getEmailAttachment() + { + return $this->emailAttachment; + } + + /** + * Set email attachment owner + * + * @param EmailAttachment $emailAttachment + * @return $this + */ + public function setEmailAttachment(EmailAttachment $emailAttachment) + { + $this->emailAttachment = $emailAttachment; + + return $this; + } + + /** + * Get attachment content + * + * @return string + */ + public function getValue() + { + return $this->content; + } + + /** + * Set attachment content + * + * @param string $content + * @return $this + */ + public function setValue($content) + { + $this->content = $content; + + return $this; + } + + /** + * Get encoding type of attachment content + * + * @return string + */ + public function getContentTransferEncoding() + { + return $this->contentTransferEncoding; + } + + /** + * Set encoding type of attachment content + * + * @param string $contentTransferEncoding + * @return $this + */ + public function setContentTransferEncoding($contentTransferEncoding) + { + $this->contentTransferEncoding = $contentTransferEncoding; + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php b/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php new file mode 100644 index 00000000000..898ab703f7a --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailBody.php @@ -0,0 +1,271 @@ +bodyIsText = false; + $this->hasAttachments = false; + $this->persistent = false; + $this->attachments = new ArrayCollection(); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Get entity created date/time + * + * @return \DateTime + */ + public function getCreatedAt() + { + return $this->created; + } + + /** + * Get body content. + * + * @return string + */ + public function getContent() + { + return $this->bodyContent; + } + + /** + * Set body content. + * + * @param string $bodyContent + * @return $this + */ + public function setContent($bodyContent) + { + $this->bodyContent = $bodyContent; + + return $this; + } + + /** + * Indicate whether email body is a text or html. + * + * @return bool true if body is text/plain; otherwise, the body content is text/html + */ + public function getBodyIsText() + { + return $this->bodyIsText; + } + + /** + * Set body content type. + * + * @param bool $bodyIsText true for text/plain, false for text/html + * @return $this + */ + public function setBodyIsText($bodyIsText) + { + $this->bodyIsText = $bodyIsText; + + return $this; + } + + /** + * Indicate whether email has attachments or not. + * + * @return bool true if body is text/plain; otherwise, the body content is text/html + */ + public function getHasAttachments() + { + return $this->hasAttachments; + } + + /** + * Set flag indicates whether there are attachments or not. + * + * @param bool $hasAttachments + * @return $this + */ + public function setHasAttachments($hasAttachments) + { + $this->hasAttachments = $hasAttachments; + + return $this; + } + + /** + * Indicate whether email is persistent or not. + * + * @return bool true if this email newer removed from the cache; otherwise, false + */ + public function getPersistent() + { + return $this->persistent; + } + + /** + * Set flag indicates whether email can be removed from the cache or not. + * + * @param bool $persistent true if this email newer removed from the cache; otherwise, false + * @return $this + */ + public function setPersistent($persistent) + { + $this->persistent = $persistent; + + return $this; + } + + /** + * Get email header + * + * @return Email + */ + public function getHeader() + { + return $this->header; + } + + /** + * Set email header + * + * @param Email $header + * @return $this + */ + public function setHeader(Email $header) + { + $this->header = $header; + + return $this; + } + + /** + * Get email attachments + * + * @return EmailAttachment[] + */ + public function getAttachments() + { + return $this->attachments; + } + + /** + * Add email attachment + * + * @param EmailAttachment $attachment + * @return $this + */ + public function addAttachment(EmailAttachment $attachment) + { + $this->setHasAttachments(true); + + $this->attachments[] = $attachment; + + $attachment->setEmailBody($this); + + return $this; + } + + /** + * Pre persist event listener + * + * @ORM\PrePersist + */ + public function beforeSave() + { + $this->created = EmailUtil::currentUTCDateTime(); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php b/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php new file mode 100644 index 00000000000..a2f1956726d --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailFolder.php @@ -0,0 +1,178 @@ +emails = new ArrayCollection(); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Get folder name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set folder name + * + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get folder type. + * + * @return string Can be 'inbox', 'sent', 'trash', 'drafts' or 'other' + */ + public function getType() + { + return $this->type; + } + + /** + * Set folder type + * + * @param string $type Can be 'inbox', 'sent', 'trash', 'drafts' or 'other' + * @return $this + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Get email folder origin + * + * @return EmailOrigin + */ + public function getOrigin() + { + return $this->origin; + } + + /** + * Set email folder origin + * + * @param EmailOrigin $origin + * @return $this + */ + public function setOrigin(EmailOrigin $origin) + { + $this->origin = $origin; + + return $this; + } + + /** + * Get emails + * + * @return Email[] + */ + public function getEmails() + { + return $this->emails; + } + + /** + * Add email + * + * @param Email $email + * @return $this + */ + public function addEmail(Email $email) + { + $this->emails[] = $email; + + $email->setFolder($this); + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailInterface.php b/src/Oro/Bundle/EmailBundle/Entity/EmailInterface.php new file mode 100644 index 00000000000..7eab1bf5518 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailInterface.php @@ -0,0 +1,34 @@ +folders = new ArrayCollection(); + } + + /** + * Get id + * + * @return integer + */ + public function getId() + { + return $this->id; + } + + /** + * Get email origin name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set email origin name + * + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get email folders + * + * @return EmailFolder[] + */ + public function getFolders() + { + return $this->folders; + } + + /** + * Add folder + * + * @param EmailFolder $folder + * @return $this + */ + public function addFolder(EmailFolder $folder) + { + $this->folders[] = $folder; + + $folder->setOrigin($this); + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailOwnerInterface.php b/src/Oro/Bundle/EmailBundle/Entity/EmailOwnerInterface.php new file mode 100644 index 00000000000..96ced7e2357 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailOwnerInterface.php @@ -0,0 +1,54 @@ +id; + } + + /** + * Get full email name + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Set full email name + * + * @param string $name + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Get recipient type. + * + * @return string Can be 'to', 'cc' or 'bcc' + */ + public function getType() + { + return $this->type; + } + + /** + * Set recipient type + * + * @param string $type Can be 'to', 'cc' or 'bcc' + * @return $this + */ + public function setType($type) + { + $this->type = $type; + + return $this; + } + + /** + * Get email address + * + * @return EmailAddress + */ + public function getEmailAddress() + { + return $this->emailAddress; + } + + /** + * Set email address + * + * @param EmailAddress $emailAddress + * @return $this + */ + public function setEmailAddress(EmailAddress $emailAddress) + { + $this->emailAddress = $emailAddress; + + return $this; + } + + /** + * Get email + * + * @return Email + */ + public function getEmail() + { + return $this->email; + } + + /** + * Set email + * + * @param Email $email + * @return $this + */ + public function setEmail(Email $email) + { + $this->email = $email; + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/EmailTemplate.php b/src/Oro/Bundle/EmailBundle/Entity/EmailTemplate.php index 7433befd921..3757d8cb09e 100644 --- a/src/Oro/Bundle/EmailBundle/Entity/EmailTemplate.php +++ b/src/Oro/Bundle/EmailBundle/Entity/EmailTemplate.php @@ -10,6 +10,8 @@ use Symfony\Component\Validator\Constraints as Assert; +use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Configurable; + /** * EmailTemplate * @@ -19,7 +21,7 @@ * @ORM\Index(name="email_is_system_idx", columns={"isSystem"}), * @ORM\Index(name="email_entity_name_idx", columns={"entityName"})}) * @ORM\Entity(repositoryClass="Oro\Bundle\EmailBundle\Entity\Repository\EmailTemplateRepository") - * @Gedmo\TranslationEntity(class="Oro\Bundle\EmailBundle\Entity\EmailTemplateTranslation")* + * @Gedmo\TranslationEntity(class="Oro\Bundle\EmailBundle\Entity\EmailTemplateTranslation") */ class EmailTemplate implements Translatable { @@ -356,6 +358,14 @@ public function __clone() $this->parent = $this->id; $this->id = null; $this->isSystem = false; + + if ($this->getTranslations() instanceof ArrayCollection) { + $clonedTranslations = new ArrayCollection(); + foreach ($this->getTranslations() as $translation) { + $clonedTranslations->add(clone $translation); + } + $this->setTranslations($clonedTranslations); + } } /** diff --git a/src/Oro/Bundle/EmailBundle/Entity/Manager/EmailAddressManager.php b/src/Oro/Bundle/EmailBundle/Entity/Manager/EmailAddressManager.php new file mode 100644 index 00000000000..82162bb047a --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Manager/EmailAddressManager.php @@ -0,0 +1,66 @@ +entityCacheNamespace = $entityCacheNamespace; + $this->entityProxyNameTemplate = $entityProxyNameTemplate; + } + + /** + * Create EmailAddress entity object. Actually a proxy class is created + * + * @return EmailAddress + */ + public function newEmailAddress() + { + $emailAddressClass = $this->getEmailAddressProxyClass(); + + return new $emailAddressClass(); + } + + /** + * Get a repository for EmailAddress entity + * + * @param EntityManager $em + * @return EntityRepository + */ + public function getEmailAddressRepository(EntityManager $em) + { + return $em->getRepository($this->getEmailAddressProxyClass()); + } + + /** + * Get full class name of a proxy of EmailAddress entity + * + * @return string + */ + protected function getEmailAddressProxyClass() + { + return sprintf('%s\%s', $this->entityCacheNamespace, sprintf($this->entityProxyNameTemplate, 'EmailAddress')); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Manager/EmailApiEntityManager.php b/src/Oro/Bundle/EmailBundle/Entity/Manager/EmailApiEntityManager.php new file mode 100644 index 00000000000..d40320fb753 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Manager/EmailApiEntityManager.php @@ -0,0 +1,32 @@ +getEmailAttachmentRepository()->find($id); + } + + /** + * Get email attachment repository + * + * @return ObjectRepository + */ + public function getEmailAttachmentRepository() + { + return $this->getObjectManager()->getRepository('Oro\Bundle\EmailBundle\Entity\EmailAttachment'); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Manager/EmailOwnerManager.php b/src/Oro/Bundle/EmailBundle/Entity/Manager/EmailOwnerManager.php new file mode 100644 index 00000000000..660e5529ae5 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Manager/EmailOwnerManager.php @@ -0,0 +1,223 @@ +getProviders() as $provider) { + $fieldName = sprintf('owner%d', count($this->emailOwnerClasses) + 1); + $this->emailOwnerClasses[$fieldName] = $provider->getEmailOwnerClass(); + } + $this->emailAddressManager = $emailAddressManager; + } + + /** + * Handle onFlush event + * + * @param OnFlushEventArgs $event + */ + public function handleOnFlush(OnFlushEventArgs $event) + { + $em = $event->getEntityManager(); + $uow = $em->getUnitOfWork(); + $needChangeSetsComputing = false; + + $needChangeSetsComputing |= $this->handleInsertionsOrUpdates($uow->getScheduledEntityInsertions(), $em, $uow); + $needChangeSetsComputing |= $this->handleInsertionsOrUpdates($uow->getScheduledEntityUpdates(), $em, $uow); + $needChangeSetsComputing |= $this->handleDeletions($uow->getScheduledEntityDeletions(), $em); + + if ($needChangeSetsComputing) { + $uow->computeChangeSets(); + } + } + + /** + * @param array $entities + * @param EntityManager $em + * @param UnitOfWork $uow + * @return bool true if UnitOfWork change set need to be recomputed + */ + protected function handleInsertionsOrUpdates(array $entities, EntityManager $em, UnitOfWork $uow) + { + $needChangeSetsComputing = false; + foreach ($entities as $entity) { + if ($entity instanceof EmailOwnerInterface) { + $needChangeSetsComputing |= $this->processInsertionOrUpdateEntity( + $entity->getPrimaryEmailField(), + $entity, + $entity, + $em, + $uow + ); + } elseif ($entity instanceof EmailInterface) { + $needChangeSetsComputing |= $this->processInsertionOrUpdateEntity( + $entity->getEmailField(), + $entity, + $entity->getEmailOwner(), + $em, + $uow + ); + } + } + + return $needChangeSetsComputing; + } + + /** + * @param $emailField + * @param mixed $entity + * @param EmailOwnerInterface $owner + * @param EntityManager $em + * @param UnitOfWork $uow + * @return bool true if UnitOfWork change set need to be recomputed + */ + protected function processInsertionOrUpdateEntity( + $emailField, + $entity, + EmailOwnerInterface $owner, + EntityManager $em, + UnitOfWork $uow + ) { + $needChangeSetsComputing = false; + if (!empty($emailField)) { + foreach ($uow->getEntityChangeSet($entity) as $field => $vals) { + if ($field === $emailField) { + list($oldValue, $newValue) = $vals; + if ($newValue !== $oldValue) { + $needChangeSetsComputing |= $this->bindEmailAddress($em, $owner, $newValue, $oldValue); + } + } + } + } + + return $needChangeSetsComputing; + } + + /** + * @param array $entities + * @param EntityManager $em + * @return bool true if UnitOfWork change set need to be recomputed + */ + protected function handleDeletions(array $entities, EntityManager $em) + { + $needChangeSetsComputing = false; + foreach ($entities as $entity) { + if ($entity instanceof EmailOwnerInterface) { + $needChangeSetsComputing |= $this->unbindEmailAddress($em, $entity); + } elseif ($entity instanceof EmailInterface) { + $needChangeSetsComputing |= $this->unbindEmailAddress($em, $entity->getEmailOwner(), $entity); + } + } + + return $needChangeSetsComputing; + } + + /** + * Bind EmailAddress entity to the given owner + * + * @param EntityManager $em + * @param EmailOwnerInterface $owner + * @param string $newEmail + * @param string $oldEmail + * @return bool true if UnitOfWork change set need to be recomputed + */ + protected function bindEmailAddress(EntityManager $em, EmailOwnerInterface $owner, $newEmail, $oldEmail) + { + $result = false; + $repository = $this->emailAddressManager->getEmailAddressRepository($em); + if (!empty($newEmail)) { + $emailAddress = $repository->findOneBy(array('email' => $newEmail)); + if ($emailAddress === null) { + $em->persist($this->createEmailAddress($newEmail, $owner)); + $result = true; + } elseif ($emailAddress->getOwner() != $owner) { + $emailAddress->setOwner($owner); + $result = true; + } + } + if (!empty($oldEmail)) { + $emailAddress = $repository->findOneBy(array('email' => $oldEmail)); + if ($emailAddress !== null) { + $emailAddress->setOwner(null); + $result = true; + } + } + + return $result; + } + + /** + * Unbind EmailAddress entity from the given owner + * + * @param EntityManager $em + * @param EmailOwnerInterface $owner + * @param EmailInterface $email + * @return bool true if UnitOfWork change set need to be recomputed + */ + protected function unbindEmailAddress(EntityManager $em, EmailOwnerInterface $owner, EmailInterface $email = null) + { + $result = false; + $repository = $this->emailAddressManager->getEmailAddressRepository($em); + foreach ($this->emailOwnerClasses as $fieldName => $emailOwnerClass) { + $condition = array($fieldName => $owner); + if ($email !== null) { + $condition['email'] = $email->getEmail(); + } + /** @var EmailAddress $emailAddress */ + foreach ($repository->findBy($condition) as $emailAddress) { + $emailAddress->setOwner(null); + $result = true; + } + } + + return $result; + } + + /** + * Create EmailAddress entity object + * + * @param string $email + * @param EmailOwnerInterface $owner + * @return EmailAddress + */ + protected function createEmailAddress($email, EmailOwnerInterface $owner) + { + return $this->emailAddressManager->newEmailAddress() + ->setEmail($email) + ->setOwner($owner); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Provider/EmailOwnerProvider.php b/src/Oro/Bundle/EmailBundle/Entity/Provider/EmailOwnerProvider.php new file mode 100644 index 00000000000..aeb807f5d0e --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Provider/EmailOwnerProvider.php @@ -0,0 +1,47 @@ +emailOwnerProviderStorage = $emailOwnerProviderStorage; + } + + /** + * Find an entity object which is an owner of the given email address + * + * @param \Doctrine\ORM\EntityManager $em + * @param string $email + * @return EmailOwnerInterface + */ + public function findEmailOwner(EntityManager $em, $email) + { + $emailOwner = null; + foreach ($this->emailOwnerProviderStorage->getProviders() as $provider) { + $emailOwner = $provider->findEmailOwner($em, $email); + if ($emailOwner !== null) { + break; + } + } + + return $emailOwner; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Provider/EmailOwnerProviderInterface.php b/src/Oro/Bundle/EmailBundle/Entity/Provider/EmailOwnerProviderInterface.php new file mode 100644 index 00000000000..8fda66a66fb --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Provider/EmailOwnerProviderInterface.php @@ -0,0 +1,28 @@ +emailOwnerProviders[] = $provider; + } + + /** + * Get all email owner providers + * + * @return EmailOwnerProviderInterface[] + */ + public function getProviders() + { + return $this->emailOwnerProviders; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php b/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php new file mode 100644 index 00000000000..c18fd7c02d3 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Repository/EmailRepository.php @@ -0,0 +1,51 @@ +getEntityManager()->createQueryBuilder() + ->select('re.id') + ->from('OroEmailBundle:Email', 're') + ->innerJoin('re.recipients', 'r') + ->innerJoin('r.emailAddress', 'ra'); + $qbRecipients->where($qbRecipients->expr()->in('ra.email', $emails)); + + $qb = $this->createQueryBuilder('e') + ->select('partial e.{id, fromName, subject, sentAt}, a') + ->innerJoin('e.fromEmailAddress', 'a') + ->orderBy('e.sentAt', 'DESC'); + $qb->where( + $qb->expr()->orX( + $qb->expr()->in('e.id', $qbRecipients->getDQL()), + $qb->expr()->in('a.email', $emails) + ) + ); + + if ($firstResult !== null) { + $qb->setFirstResult($firstResult); + } + if ($maxResults !== null) { + $qb->setMaxResults($maxResults); + } + + return $qb; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Entity/Util/EmailUtil.php b/src/Oro/Bundle/EmailBundle/Entity/Util/EmailUtil.php new file mode 100644 index 00000000000..4f004d4be29 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Entity/Util/EmailUtil.php @@ -0,0 +1,48 @@ +; 'pure' email address john@example.com + * email address: John Smith ; 'pure' email address john@example.com + * email address: ; 'pure' email address john@example.com + * email address: john@example.com; 'pure' email address john@example.com + * + * @param string $fullEmailAddress + * @return string + */ + public static function extractPureEmailAddress($fullEmailAddress) + { + $atPos = strpos($fullEmailAddress, '@'); + if ($atPos === false) { + return $fullEmailAddress; + } + + $startPos = strrpos($fullEmailAddress, '<', -$atPos); + if ($startPos === false) { + return $fullEmailAddress; + } + + $endPos = strpos($fullEmailAddress, '>', $atPos); + if ($endPos === false) { + return $fullEmailAddress; + } + + return substr($fullEmailAddress, $startPos + 1, $endPos - $startPos - 1); + } + + /** + * Return current UTC date/time + * + * @return \DateTime + */ + public static function currentUTCDateTime() + { + return new \DateTime('now', new \DateTimeZone('UTC')); + } +} diff --git a/src/Oro/Bundle/EmailBundle/EventListener/ConfigSubscriber.php b/src/Oro/Bundle/EmailBundle/EventListener/ConfigSubscriber.php new file mode 100644 index 00000000000..51cd33ddf23 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/EventListener/ConfigSubscriber.php @@ -0,0 +1,71 @@ +cache = $cache; + $this->cacheKey = $cacheKey; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + Events::NEW_ENTITY_CONFIG_MODEL => 'newEntityConfig', + Events::PRE_PERSIST_CONFIG => 'persistConfig', + ); + } + + /** + * @param NewEntityConfigModelEvent $event + */ + public function newEntityConfig(NewEntityConfigModelEvent $event) + { + // clear cache when new entity added to configurator + // in case if default value for some fields will equal true + $cp = $event->getConfigManager()->getProvider('email'); + $fieldConfigs = $cp->filter( + function (ConfigInterface $config) { + return $config->is('available_in_template'); + }, + $event->getClassName() + ); + + if (count($fieldConfigs)) { + $this->cache->delete($this->cacheKey); + } + } + + /** + * @param PersistConfigEvent $event + */ + public function persistConfig(PersistConfigEvent $event) + { + $event->getConfigManager()->calculateConfigChangeSet($event->getConfig()); + $change = $event->getConfigManager()->getConfigChangeSet($event->getConfig()); + + if ($event->getConfig()->getId()->getScope() == 'email' && isset($change['available_in_template'])) { + $this->cache->delete($this->cacheKey); + } + } +} diff --git a/src/Oro/Bundle/EmailBundle/EventListener/EntitySubscriber.php b/src/Oro/Bundle/EmailBundle/EventListener/EntitySubscriber.php new file mode 100644 index 00000000000..1f23a337fbf --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/EventListener/EntitySubscriber.php @@ -0,0 +1,42 @@ +emailOwnerManager = $emailOwnerManager; + } + + /** + * @return array + */ + public function getSubscribedEvents() + { + return array( + //@codingStandardsIgnoreStart + Events::onFlush + //@codingStandardsIgnoreEnd + ); + } + + /** + * @param OnFlushEventArgs $event + */ + public function onFlush(OnFlushEventArgs $event) + { + $this->emailOwnerManager->handleOnFlush($event); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Form/Handler/EmailTemplateHandler.php b/src/Oro/Bundle/EmailBundle/Form/Handler/EmailTemplateHandler.php index c2011144844..6fbd9436b48 100644 --- a/src/Oro/Bundle/EmailBundle/Form/Handler/EmailTemplateHandler.php +++ b/src/Oro/Bundle/EmailBundle/Form/Handler/EmailTemplateHandler.php @@ -61,7 +61,7 @@ public function process(EmailTemplate $entity) // deny to modify system templates if ($entity->getIsSystem()) { $message = $this->translator->trans( - 'oro.mail.validators.emailtemplate.attempt_save_system_template', + 'oro.email.handler.attempt_save_system_template', array(), 'validators' ); diff --git a/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateType.php b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateType.php index d2b45def822..003a57219e9 100644 --- a/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateType.php +++ b/src/Oro/Bundle/EmailBundle/Form/Type/EmailTemplateType.php @@ -62,8 +62,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'html' => 'oro.email.datagrid.emailtemplate.filter.type.html', 'txt' => 'oro.email.datagrid.emailtemplate.filter.type.txt' ), - 'required' => true, - 'translation_domain' => 'datagrid' + 'required' => true ) ); @@ -76,8 +75,11 @@ public function buildForm(FormBuilderInterface $builder, array $options) ); $builder->add( - 'parent', - 'hidden' + 'parentTemplate', + 'hidden', + array( + 'property_path' => 'parent' + ) ); } diff --git a/src/Oro/Bundle/EmailBundle/LICENSE b/src/Oro/Bundle/EmailBundle/LICENSE new file mode 100644 index 00000000000..938870a7a30 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2013 Oro, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/src/Oro/Bundle/EmailBundle/OroEmailBundle.php b/src/Oro/Bundle/EmailBundle/OroEmailBundle.php index 2e5e81d059b..1f047ad0a3b 100644 --- a/src/Oro/Bundle/EmailBundle/OroEmailBundle.php +++ b/src/Oro/Bundle/EmailBundle/OroEmailBundle.php @@ -2,8 +2,72 @@ namespace Oro\Bundle\EmailBundle; +use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\DoctrineOrmMappingsPass; +use Oro\Bundle\EmailBundle\DependencyInjection\Compiler\EmailOwnerConfigurationPass; +use Symfony\Component\Filesystem\Filesystem; class OroEmailBundle extends Bundle { + /** + * {@inheritdoc} + */ + public function build(ContainerBuilder $container) + { + parent::build($container); + + $container->addCompilerPass(new EmailOwnerConfigurationPass()); + $this->addDoctrineOrmMappingsPass($container); + } + + /** + * Add a compiler pass handles annotations of extended entities + * + * @param ContainerBuilder $container + */ + protected function addDoctrineOrmMappingsPass(ContainerBuilder $container) + { + $cacheDir = sprintf('%s/entities', $container->getParameter('kernel.root_dir')); + $entityCacheNamespace = 'Extend\Cache\OroEmailBundle\Entity'; + + $container->setParameter('oro_email.entity.cache_dir', $cacheDir); + $container->setParameter('oro_email.entity.cache_namespace', $entityCacheNamespace); + $container->setParameter('oro_email.entity.proxy_name_template', '%sProxy'); + + $entityCacheDir = sprintf('%s/%s', $cacheDir, str_replace('\\', '/', $entityCacheNamespace)); + // Ensure the cache directory exists + $fs = new Filesystem(); + if (!is_dir($entityCacheDir)) { + $fs->mkdir($entityCacheDir, 0777); + } + + $container->addCompilerPass( + $this->createAnnotationMappingDriver( + array($entityCacheNamespace), + array($entityCacheDir) + ) + ); + } + + /** + * Create DoctrineOrmMappingsPass object + * + * @param array $namespaces List of namespaces that are handled with annotation mapping + * @param array $directories List of directories to look for annotated classes + * @param string[] $managerParameters List of parameters that could which object manager name your bundle uses. + * This compiler pass will automatically append the parameter name for the default entity manager to this list. + * @param bool|string $enabledParameter Service container parameter that must be present to enable the mapping + * Set to false to not do any check, optional. + * @return DoctrineOrmMappingsPass + */ + protected function createAnnotationMappingDriver(array $namespaces, array $directories, array $managerParameters = array(), $enabledParameter = false) + { + $reader = new Reference('annotation_reader'); + $driver = new Definition('Doctrine\ORM\Mapping\Driver\AnnotationDriver', array($reader, $directories)); + + return new DoctrineOrmMappingsPass($driver, $namespaces, $managerParameters, $enabledParameter); + } } diff --git a/src/Oro/Bundle/EmailBundle/Provider/EmailRenderer.php b/src/Oro/Bundle/EmailBundle/Provider/EmailRenderer.php new file mode 100644 index 00000000000..dc6c9227cbd --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Provider/EmailRenderer.php @@ -0,0 +1,125 @@ +configProvider = $configProvider; + $this->sandBoxConfigCache = $cache; + $this->cacheKey = $cacheKey; + $this->user = $securityContext->getToken() && !is_string($securityContext->getToken()->getUser()) + ? $securityContext->getToken()->getUser() : false; + + $this->addExtension($sandbox); + $this->configureSandbox(); + } + + /** + * Configure sandbox form config data + * + */ + protected function configureSandbox() + { + $allowedData = $this->sandBoxConfigCache->fetch($this->cacheKey); + + if (false === $allowedData) { + $allowedData = $this->prepareConfiguration(); + $this->sandBoxConfigCache->save($this->cacheKey, serialize($allowedData)); + } else { + $allowedData = unserialize($allowedData); + } + + /** @var \Twig_Extension_Sandbox $sandbox */ + $sandbox = $this->getExtension('sandbox'); + /** @var \Twig_Sandbox_SecurityPolicy $security */ + $security = $sandbox->getSecurityPolicy(); + $security->setAllowedMethods($allowedData); + } + + /** + * Prepare configuration from entity config + * + * @return array + */ + private function prepareConfiguration() + { + $configuration = array(); + + foreach ($this->configProvider->getIds() as $entityConfigId) { + $className = $entityConfigId->getClassName(); + $fields = $this->configProvider->filter( + function (ConfigInterface $fieldConfig) { + return $fieldConfig->is('available_in_template'); + }, + $className + ); + + if (count($fields)) { + $configuration[$className] = array(); + foreach ($fields as $fieldConfig) { + $configuration[$className][] = 'get' . strtolower($fieldConfig->getId()->getFieldName()); + } + } + } + + return $configuration; + } + + /** + * Compile email message + * + * @param EmailTemplate $entity + * @param array $templateParams + * + * @return array first element is email subject, second - message + */ + public function compileMessage(EmailTemplate $entity, array $templateParams = array()) + { + // ensure we have no html tags in txt template + $content = $entity->getContent(); + $content = $entity->getType() == 'txt' ? strip_tags($content) : $content; + + $templateParams['user'] = $this->user; + + $templateRendered = $this->render('{% verbatim %}' . $content . '{% endverbatim %}', $templateParams); + $subjectRendered = $this->render( + '{% verbatim %}' . $entity->getSubject() . '{% endverbatim %}', + $templateParams + ); + + return array($subjectRendered, $templateRendered); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Provider/VariablesProvider.php b/src/Oro/Bundle/EmailBundle/Provider/VariablesProvider.php new file mode 100644 index 00000000000..9a687bda7b6 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Provider/VariablesProvider.php @@ -0,0 +1,88 @@ +securityContext = $securityContext; + $this->configProvider = $provider; + } + + /** + * Return available in template variables + * + * @param string $entityName + * @return array + */ + public function getTemplateVariables($entityName) + { + $userClassName = $this->getUser() ? get_class($this->getUser()) : false; + $allowedData = array( + 'entity' => array(), + 'user' => array() + ); + + $ids = $this->configProvider->getIds(); + foreach ($ids as $entityConfigId) { + // export variables of asked entity and current user entity class + $className = $entityConfigId->getClassName(); + if ($className == $entityName || $className == $userClassName) { + $fields = $this->configProvider->filter( + function (ConfigInterface $config) { + return $config->is('available_in_template'); + }, + $className + ); + + $fields = array_values( + array_map( + function (ConfigInterface $field) { + return $field->getId()->getFieldName(); + }, + $fields + ) + ); + + switch ($className) { + case $entityName: + $allowedData['entity'] = $fields; + break; + case $userClassName: + $allowedData['user'] = $fields; + break; + } + + if ($entityName == $userClassName) { + $allowedData['user'] = $allowedData['entity']; + } + } + } + + return $allowedData; + } + + /** + * Return current user + * + * @return UserInterface|bool + */ + private function getUser() + { + return $this->securityContext->getToken() && !is_string($this->securityContext->getToken()->getUser()) + ? $this->securityContext->getToken()->getUser() : false; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Resources/cache/Entity/EmailAddress.php.twig b/src/Oro/Bundle/EmailBundle/Resources/cache/Entity/EmailAddress.php.twig new file mode 100644 index 00000000000..a89038d5a14 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/cache/Entity/EmailAddress.php.twig @@ -0,0 +1,81 @@ +{{ owner.fieldName }} !== null) { + return $this->{{ owner.fieldName }}; + } +{% endfor %} + + return null; + } + + /** + * {@inheritdoc} + */ + public function setOwner(EmailOwnerInterface $owner = null) + { +{% for owner in owners %} + if (is_a($owner, '{{ owner.targetEntity }}')) { + $this->{{ owner.fieldName }} = $owner; + } else { + $this->{{ owner.fieldName }} = null; + } +{% endfor %} + + return $this; + } + + /** + * Pre persist event listener + * + * @ORM\PrePersist + */ + public function beforeSave() + { + $this->created = EmailUtil::currentUTCDateTime(); + $this->updated = EmailUtil::currentUTCDateTime(); + } + + /** + * Pre update event listener + * + * @ORM\PreUpdate + */ + public function beforeUpdate() + { + $this->updated = EmailUtil::currentUTCDateTime(); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml b/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml index 412124533f8..438a58fa6f2 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/assets.yml @@ -3,6 +3,12 @@ js: - '@OroEmailBundle/Resources/public/js/views/templates.updater.js' - '@OroEmailBundle/Resources/public/js/models/templates.updater.js' - '@OroEmailBundle/Resources/public/js/collections/templates.updater.js' + - '@OroEmailBundle/Resources/public/js/email.js' + + 'email_variables': + - '@OroEmailBundle/Resources/public/js/models/variable.js' + - '@OroEmailBundle/Resources/public/js/views/variables.updater.js' + css: 'email': - '@OroEmailBundle/Resources/public/css/style.css' diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml index 847133ebabe..6e6d67f430c 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/datagrid.yml @@ -9,5 +9,6 @@ services: - name: oro_grid.datagrid.manager datagrid_name: emailtemplate entity_name: %oro_email.emailtemplate.entity.class% - entity_hint: email templates + entity_hint: email template route_name: oro_email_emailtemplate_index + identifier_field: "id" diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/entity_config.yml b/src/Oro/Bundle/EmailBundle/Resources/config/entity_config.yml new file mode 100644 index 00000000000..0bb7802db9b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/config/entity_config.yml @@ -0,0 +1,15 @@ +oro_entity_config: + email: + field: + items: + available_in_template: + options: + default_value: false + is_bool: true + form: + type: choice + options: + choices: ['No', 'Yes'] + empty_value: false + block: other + label: 'Available in email templates' diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/entity_output.yml b/src/Oro/Bundle/EmailBundle/Resources/config/entity_output.yml new file mode 100644 index 00000000000..7fefbdae6d5 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/config/entity_output.yml @@ -0,0 +1,4 @@ +Oro\Bundle\EmailBundle\Entity\Email: + icon_class: icon-envelope + name: entity.email.name + description: entity.email.description diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/navigation.yml b/src/Oro/Bundle/EmailBundle/Resources/config/navigation.yml index 797fbd3807f..b5be9220f75 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/navigation.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/navigation.yml @@ -1,7 +1,7 @@ oro_menu_config: items: oro_email_emailtemplate_list: - label: 'Email templates' + label: 'Email Templates' route: 'oro_email_emailtemplate_index' extras: routes: ['oro_email_emailtemplate_*'] @@ -13,6 +13,7 @@ oro_menu_config: oro_email_emailtemplate_list: ~ oro_titles: + oro_email_view: "%%subject%% - Email" oro_email_emailtemplate_index: ~ oro_email_emailtemplate_update: "Edit Template %%name%%" oro_email_emailtemplate_create: "Create Email Template" diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/routing.yml b/src/Oro/Bundle/EmailBundle/Resources/config/routing.yml index 71ddbfd6b79..74707b7aa7b 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/routing.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/routing.yml @@ -3,12 +3,11 @@ oro_email: type: annotation prefix: /email - -oro_email_api: - resource: Oro\Bundle\EmailBundle\Controller\Api\Rest\EmailTemplateController +oro_email_bundle_api: + resource: "@OroEmailBundle/Resources/config/routing_api.yml" type: rest - prefix: api/rest/{version}/ + prefix: api/rest/{version} requirements: - version: latest|v1 + version: latest|v1 defaults: - version: latest \ No newline at end of file + version: latest diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/routing_api.yml b/src/Oro/Bundle/EmailBundle/Resources/config/routing_api.yml new file mode 100644 index 00000000000..b6bb463e90b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/config/routing_api.yml @@ -0,0 +1,7 @@ +oro_email_api: + resource: Oro\Bundle\EmailBundle\Controller\Api\Rest\EmailController + type: rest + +oro_email_emailtemplate_api: + resource: Oro\Bundle\EmailBundle\Controller\Api\Rest\EmailTemplateController + type: rest diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/search.yml b/src/Oro/Bundle/EmailBundle/Resources/config/search.yml new file mode 100644 index 00000000000..ec8e7eac497 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/config/search.yml @@ -0,0 +1,14 @@ +Oro\Bundle\EmailBundle\Entity\Email: + alias: oro_email + label: Emails + search_template: OroEmailBundle:Email:searchResult.html.twig + route: + name: oro_email_view + parameters: + id: id + title_fields: [subject] + fields: + - + name: subject + target_type: text + target_fields: [subject] diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml index b4dc39d581d..470f30d80f0 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/services.yml @@ -1,4 +1,17 @@ parameters: + oro_email.email.entity.class: Oro\Bundle\EmailBundle\Entity\Email + oro_email.email.cache.manager.class: Oro\Bundle\EmailBundle\Cache\EmailCacheManager + oro_email.email.address.manager.class: Oro\Bundle\EmailBundle\Entity\Manager\EmailAddressManager + oro_email.email.owner.provider.class: Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProvider + oro_email.email.owner.provider.storage.class: Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderStorage + oro_email.email.owner.manager.class: Oro\Bundle\EmailBundle\Entity\Manager\EmailOwnerManager + oro_email.email.entity.builder.class: Oro\Bundle\EmailBundle\Builder\EmailEntityBuilder + oro_email.email.entity.batch_processor.class: Oro\Bundle\EmailBundle\Builder\EmailEntityBatchProcessor + oro_email.listener.entity_subscriber.class: Oro\Bundle\EmailBundle\EventListener\EntitySubscriber + oro_email.manager.email.api.class: Oro\Bundle\EmailBundle\Entity\Manager\EmailApiEntityManager + oro_email.entity.cache.warmer.class: Oro\Bundle\EmailBundle\Cache\EntityCacheWarmer + oro_email.entity.cache.clearer.class: Oro\Bundle\EmailBundle\Cache\EntityCacheClearer + oro_email.emailtemplate.entity.class: Oro\Bundle\EmailBundle\Entity\EmailTemplate # Email template field @@ -14,7 +27,99 @@ parameters: oro_email.manager.emailtemplate.api.class: Oro\Bundle\SoapBundle\Entity\Manager\ApiEntityManager oro_email.form.type.emailtemplate.api.class: Oro\Bundle\EmailBundle\Form\Type\EmailTemplateApiType + # Entity config event listener + oro_email.listener.config_subscriber.class: Oro\Bundle\EmailBundle\EventListener\ConfigSubscriber + + # Providers + oro_email.provider.variable_provider.class: Oro\Bundle\EmailBundle\Provider\VariablesProvider + + # Cache keys + oro_email.cache.available_in_template_key: 'oro_email.available_in_template_fields' + + # Email renderer, twig instance + oro_email.email_renderer.class: Oro\Bundle\EmailBundle\Provider\EmailRenderer + oro_email.twig.email_security_policy.class: Twig_Sandbox_SecurityPolicy + services: + oro_email.entity.cache.warmer: + public: false + class: %oro_email.entity.cache.warmer.class% + arguments: + - @oro_email.email.owner.provider.storage + - %oro_email.entity.cache_dir% + - %oro_email.entity.cache_namespace% + - %oro_email.entity.proxy_name_template% + tags: + - { name: kernel.cache_warmer, priority: 30 } + + oro_email.entity.cache.clearer: + public: false + class: %oro_email.entity.cache.clearer.class% + arguments: + - %oro_email.entity.cache_dir% + - %oro_email.entity.cache_namespace% + - %oro_email.entity.proxy_name_template% + tags: + - { name: kernel.cache_clearer } + + oro_email.email.cache.manager: + class: %oro_email.email.cache.manager.class% + arguments: + - @doctrine.orm.entity_manager + + oro_email.email.address.manager: + public: false + class: %oro_email.email.address.manager.class% + arguments: + - %oro_email.entity.cache_namespace% + - %oro_email.entity.proxy_name_template% + + oro_email.email.owner.provider.storage: + public: false + class: %oro_email.email.owner.provider.storage.class% + + oro_email.email.owner.provider: + public: false + class: %oro_email.email.owner.provider.class% + arguments: + - @oro_email.email.owner.provider.storage + + oro_email.email.owner.manager: + public: false + class: %oro_email.email.owner.manager.class% + arguments: + - @oro_email.email.owner.provider.storage + - @oro_email.email.address.manager + + oro_email.email.entity.builder: + class: %oro_email.email.entity.builder.class% + scope: prototype + arguments: + - @oro_email.email.entity.batch_processor + - @oro_email.email.address.manager + + oro_email.email.entity.batch_processor: + class: %oro_email.email.entity.batch_processor.class% + public: false + scope: prototype + arguments: + - @oro_email.email.address.manager + - @oro_email.email.owner.provider + + oro_email.listener.entity_subscriber: + public: false + class: %oro_email.listener.entity_subscriber.class% + arguments: + - @oro_email.email.owner.manager + tags: + - { name: doctrine.event_subscriber, connection: default } + + oro_email.manager.email.api: + class: %oro_email.manager.email.api.class% + arguments: + - %oro_email.email.entity.class% + - @doctrine.orm.entity_manager + # Email template field oro_email.form.subscriber.emailtemplate: class: %oro_email.form.subscriber.emailtemplate.class% @@ -81,3 +186,65 @@ services: - @request - @doctrine.orm.entity_manager - @translator + + oro_email.cache: + parent: oro.cache.abstract + calls: + - [setNamespace, ['oro_email.cache']] + + # Available variables services + oro_email.listener.config_subscriber: + class: %oro_email.listener.config_subscriber.class% + arguments: [@oro_email.cache, %oro_email.cache.available_in_template_key%] + tags: + - { name: kernel.event_subscriber} + + # email template twig instance + oro_email.twig.string_loader: + class: Twig_Loader_String + + oro_email.email_renderer: + class: %oro_email.email_renderer.class% + arguments: + - @oro_email.twig.string_loader + - # twig environment options + strict_variables: true + - @oro_entity_config.provider.email + - @oro_email.cache + - %oro_email.cache.available_in_template_key% + - @security.context + - @oro_email.twig.email_sandbox + + oro_email.twig.email_security_policy: + class: %oro_email.twig.email_security_policy.class% + arguments: + # tags + - [ 'if', 'app' ] + # filters + - [ 'upper', 'escape' ] + # methods + - [] + # properties + - [] + # functions + - [] + + oro_email.twig.email_sandbox: + class: Twig_Extension_Sandbox + arguments: + - @oro_email.twig.email_security_policy + - true # use sandbox globally in instance + + oro_email.provider.variable_provider: + class: %oro_email.provider.variable_provider.class% + arguments: + - @security.context + - @oro_entity_config.provider.email + + oro_email.validator.variables_validator: + class: Oro\Bundle\EmailBundle\Validator\VariablesValidator + arguments: + - @oro_email.email_renderer + - @security.context + tags: + - { name: validator.constraint_validator, alias: oro_email.variables_validator } diff --git a/src/Oro/Bundle/EmailBundle/Resources/config/validation.yml b/src/Oro/Bundle/EmailBundle/Resources/config/validation.yml index 34cc488ad8d..234d2c6a8ea 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/config/validation.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/config/validation.yml @@ -1,7 +1,7 @@ Oro\Bundle\EmailBundle\Entity\EmailTemplate: constraints: - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: [ name, entityName ] - + - Oro\Bundle\EmailBundle\Validator\Constraints\VariablesConstraint: ~ properties: name: - NotBlank: ~ diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/css/style.css b/src/Oro/Bundle/EmailBundle/Resources/public/css/style.css index bc09aa6110c..c9653d36242 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/public/css/style.css +++ b/src/Oro/Bundle/EmailBundle/Resources/public/css/style.css @@ -10,3 +10,7 @@ .a2lix_translationsFields.tab-content .tab-pane textarea { width: 700px; } +.modal-body .loading-content { + background: #fff url('/bundles/orogrid/img/loader.gif') no-repeat center left; + padding-left: 30px; +} diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/js/email.js b/src/Oro/Bundle/EmailBundle/Resources/public/js/email.js new file mode 100644 index 00000000000..b45c7e94018 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/public/js/email.js @@ -0,0 +1,16 @@ +$(function () { + $(document).on('click', '.view-email-entity-btn', function (e) { + new Oro.widget.DialogView({ + url: $(this).attr('href'), + dialogOptions: { + allowMaximize: true, + allowMinimize: true, + dblclick: 'maximize', + maximizedHeightDecreaseBy: 'minimize-bar', + width: 1000, + title: $(this).attr('title') + } + }).render(); + e.preventDefault(); + }); +}); diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/js/models/variable.js b/src/Oro/Bundle/EmailBundle/Resources/public/js/models/variable.js new file mode 100644 index 00000000000..7166e5c7b32 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/public/js/models/variable.js @@ -0,0 +1,26 @@ +Oro = Oro || {}; +Oro.Email = Oro.Email || {}; +Oro.Email.VariablesUpdater = Oro.Email.VariablesUpdater || {}; + +Oro.Email.VariablesUpdater.Variable = Backbone.Model.extend({ + defaults: { + user: [], + entity: [], + entityName: null + }, + + route: 'oro_api_get_emailtemplate_available_variables', + url: null, + + initialize: function() { + this.updateUrl(); + this.bind('change:entityName', this.updateUrl, this); + }, + + /** + * onChange entityName attribute + */ + updateUrl: function() { + this.url = Routing.generate(this.route, {entityName: this.get('entityName')}); + } +}); diff --git a/src/Oro/Bundle/EmailBundle/Resources/public/js/views/variables.updater.js b/src/Oro/Bundle/EmailBundle/Resources/public/js/views/variables.updater.js new file mode 100644 index 00000000000..2c2a9eae8bf --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/public/js/views/variables.updater.js @@ -0,0 +1,81 @@ +Oro = Oro || {}; +Oro.Email = Oro.Email || {}; +Oro.Email.VariablesUpdater = Oro.Email.VariablesUpdater || {}; + +Oro.Email.VariablesUpdater.View = Backbone.View.extend({ + events: { + 'click ul li a': 'addVariable' + }, + target: null, + + lastElement: null, + + /** + * Constructor + * + * @param options {Object} + */ + initialize: function (options) { + this.target = options.target; + + this.listenTo(this.model, 'sync', this.render); + this.target.on('change', _.bind(this.selectionChanged, this)); + + $('input[name*="subject"], textarea[name*="content"]').on('blur', _.bind(this._updateElementsMetaData, this)); + this.render(); + }, + + /** + * onChange event listener + * + * @param e {Object} + */ + selectionChanged: function (e) { + var entityName = $(e.currentTarget).val(); + this.model.set('entityName', entityName.split('\\').join('_')); + this.model.fetch(); + }, + + /** + * Renders target element + * + * @returns {*} + */ + render: function() { + var html = _.template(this.options.template.html(), { + userVars: this.model.get('user'), + entityVars: this.model.get('entity') + }); + + $(this.el).html(html); + + return this; + }, + + /** + * Add variable to last element + * + * @param e + * @returns {*} + */ + addVariable: function(e) { + if (!_.isNull(this.lastElement) && this.lastElement.is(':visible')) { + this.lastElement.val(this.lastElement.val() + $(e.currentTarget).html()); + } + + return this; + }, + + /** + * Update elements metadata + * + * @param e + * @private + * @returns {*} + */ + _updateElementsMetaData: function(e) { + this.lastElement = $(e.currentTarget); + + return this; + } +}); diff --git a/src/Oro/Bundle/EmailBundle/Resources/translations/config.en.yml b/src/Oro/Bundle/EmailBundle/Resources/translations/config.en.yml new file mode 100644 index 00000000000..23359375e9b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/translations/config.en.yml @@ -0,0 +1,4 @@ +entity: + email: + name: Email + description: Email message diff --git a/src/Oro/Bundle/EmailBundle/Resources/translations/datagrid.en.yml b/src/Oro/Bundle/EmailBundle/Resources/translations/datagrid.en.yml deleted file mode 100644 index 219f57115bc..00000000000 --- a/src/Oro/Bundle/EmailBundle/Resources/translations/datagrid.en.yml +++ /dev/null @@ -1,21 +0,0 @@ -oro: - email: - datagrid: - emailtemplate: - action: - update: "Update" - clone: "Clone" - delete: "Delete" - column: - entity_name: "Entity name" - name: "Template name" - isSystem: "Is system" - type: "Type" - filter: - type: - html: "HTML" - txt: "Plain text" - isSystem: - yes: "Yes" - no: "No" - diff --git a/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml index da42cb102d4..26e799b7919 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml +++ b/src/Oro/Bundle/EmailBundle/Resources/translations/messages.en.yml @@ -6,3 +6,35 @@ oro: message: "Template sucessfully saved" form: choose_template: "Choose a template..." + datagrid: + emailtemplate: + action: + update: "Update" + clone: "Clone" + delete: "Delete" + column: + entity_name: "Entity name" + name: "Template name" + isSystem: "Is system" + type: "Type" + filter: + type: + html: "HTML" + txt: "Plain text" + isSystem: + yes: "Yes" + no: "No" + page_size: + all: "All" + handler: + attempt_save_system_template: "Overriding of system's templates is prohibited, clone it instead." +"%subject% - Email": "%subject% - Email" +"Emails": "Emails" +"Email": "Email" +"Subject": "Subject" +"Sent": "Sent" +"From": "From" +"To": "To" +"Cc": "Cc" +"Bcc": "Bcc" +"Attachments": "Attachments" diff --git a/src/Oro/Bundle/EmailBundle/Resources/translations/validators.en.yml b/src/Oro/Bundle/EmailBundle/Resources/translations/validators.en.yml deleted file mode 100644 index 23b2934d501..00000000000 --- a/src/Oro/Bundle/EmailBundle/Resources/translations/validators.en.yml +++ /dev/null @@ -1,5 +0,0 @@ -oro: - mail: - validators: - emailtemplate: - attempt_save_system_template: "Overriding of system's templates is prohibited, clone it instead." diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/activities.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/activities.html.twig new file mode 100644 index 00000000000..408bbf3e2ea --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/activities.html.twig @@ -0,0 +1,33 @@ +{# + Available variables: + * entities - array of activity entities. Which items of this array is an associative array contains subset of fields of an activity entity +#} +{# TODO: This is a temporary template created for demo purposes. It will be removed when 'display activities' functionality is implemented #} +{% import 'OroUIBundle::macros.html.twig' as UI %} +{% import 'OroEmailBundle::macros.html.twig' as EA %} +{% set format = oro_config_value('oro_user.name_format') %} +
    +
    +
    +
    +
    + + + + + + + + + + + {% for entity in entities %} + {% include 'OroEmailBundle:Email:activity.html.twig' with {'entity': entity} %} + {% endfor %} + +
    IDFrom Subject Sent
    +
    +
    +
    +
    +
    diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/activity.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/activity.html.twig new file mode 100644 index 00000000000..bb16624a711 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/activity.html.twig @@ -0,0 +1,11 @@ +{# + Available variables: + * entity - email entity Oro\Bundle\EmailBundle\Entity\Email +#} +{# TODO: This is a demo template. It need to be replaced with a real one #} + + {{ entity.id }} + {{ EA.email_address(entity.fromEmailAddress, entity.fromName, format) }} + {{ entity.subject }} + {{ UI.time(entity.sentAt) }} + diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/dialog.view.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/dialog.view.html.twig new file mode 100644 index 00000000000..0bf5d7763f1 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/dialog.view.html.twig @@ -0,0 +1,29 @@ +{# + Available variables: + * entity - email entity Oro\Bundle\EmailBundle\Entity\Email +#} +{% import 'OroUIBundle::macros.html.twig' as UI %} +{% import 'OroEmailBundle::macros.html.twig' as EA %} + +{% set format = oro_config_value('oro_user.name_format') %} + +{% block page_container %} + +{% endblock %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/searchResult.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/searchResult.html.twig new file mode 100644 index 00000000000..f390f679f5d --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/searchResult.html.twig @@ -0,0 +1,22 @@ +{# + Available variables: + * entity - email entity Oro\Bundle\EmailBundle\Entity\Email + * indexer_item - indexer item Oro\Bundle\SearchBundle\Query\Result\Item +#} +{% extends 'OroSearchBundle:Search:searchResultItem.html.twig' %} +{% import 'OroUserBundle::macros.html.twig' as UI %} +{% import 'OroEmailBundle::macros.html.twig' as EA %} + +{% set format = oro_config_value('oro_user.name_format') %} +{% set iconType = 'envelope' %} + +{% set recordUrl = indexer_item.recordUrl %} +{% set title = entity ? entity.subject : indexer_item.recordTitle %} + +{% set entityType = 'Email'|trans %} + +{% set entityInfo = [ +{'title': 'Sent', 'value': UI.time(entity.sentAt)}, +{'title': 'From', 'value': EA.email_address(entity.fromEmailAddress, entity.fromName, format)}, +{'title': 'To', 'value': EA.recipient_email_addresses(entity.recipients('to'), format)}, +] %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/Email/view.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/Email/view.html.twig new file mode 100644 index 00000000000..a026899a324 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/Email/view.html.twig @@ -0,0 +1,55 @@ +{# + Available variables: + * entity - email entity Oro\Bundle\EmailBundle\Entity\Email +#} +{% extends 'OroUIBundle:actions:view.html.twig' %} +{% import 'OroUIBundle::macros.html.twig' as UI %} +{% import 'OroEmailBundle::macros.html.twig' as EA %} +{% set format = oro_config_value('oro_user.name_format') %} +{% oro_title_set({params : {"%subject%": entity.subject} }) %} + +{% block pageHeader %} + {% set breadcrumbs = {'entity': entity, 'indexLabel': 'Emails', 'entityTitle': entity.subject } %} + {{ parent() }} +{% endblock pageHeader %} + +{% block stats %} +
  • {{ 'Created'|trans }}: {{ breadcrumbs.entity.createdAt ? UI.time(breadcrumbs.entity.createdAt) : 'N/A' }}
  • +{% endblock stats %} + +{% block content_data %} + {% set id = 'email-profile' %} + + {% set attributes = [ + UI.attibuteRow('Sent', UI.time(entity.sentAt)), + UI.attibuteRow('From', EA.email_address(entity.fromEmailAddress, entity.fromName, format)), + UI.attibuteRow('To', EA.recipient_email_addresses(entity.recipients('to'), format)), + UI.attibuteRow('Cc', EA.recipient_email_addresses(entity.recipients('cc'), format)), + UI.attibuteRow('Bcc', EA.recipient_email_addresses(entity.recipients('bcc'), format)), + UI.attibuteRow('Subject', entity.subject) + ] + %} + {% if entity.emailBody.hasAttachments %} + {% set attributes = attributes | merge([UI.attibuteRow('Attachments', EA.attachments(entity.emailBody.attachments))]) %} + {% endif %} + {% set attributes = attributes | merge([EA.body(entity.emailBody)]) %} + + {% set data = { + 'dataBlocks': + [ + { + 'title': 'General', + 'class': 'active', + 'subblocks': [ + { + 'title': null, + 'useSpan': false, + 'data': attributes + } + ] + } + ] + } + %} + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/preview.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/preview.html.twig new file mode 100644 index 00000000000..33bc6e646c9 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/preview.html.twig @@ -0,0 +1,2 @@ +{#

    Subject: {{ subject }}

    #} +{{ content|raw }} \ No newline at end of file diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/update.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/update.html.twig index ca3e9897db8..079e0558a42 100644 --- a/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/update.html.twig +++ b/src/Oro/Bundle/EmailBundle/Resources/views/EmailTemplate/update.html.twig @@ -2,9 +2,10 @@ {% form_theme form with [ 'OroUIBundle:Form:fields.html.twig', ]%} +{% import 'OroEmailBundle::macros.html.twig' as _emailMacros %} {% set title = form.vars.value.id ? 'Edit email template "%name%"'|trans({'%name%': form.vars.value.name}) - : isClone ? 'Clone Email Template'|trans : 'Add Email Template'|trans + : isClone ? 'Clone Email Template'|trans : 'New Email Template'|trans %} {% if form.vars.value.id %} {% oro_title_set({params : {"%name%": form.vars.value.name} }) %} @@ -16,6 +17,19 @@ %} {% block navButtons %} + {% if resource_granted('oro_email_emailtemplate_preview') %} + {{ UI.button({ + 'path' : path('oro_email_emailtemplate_preview', {'id': form.vars.value.id }), + 'title' : 'Preview', + 'label' : 'Preview', + aClass: 'btn-success dialog-form-renderer no-hash', + iClass: 'icon-share' + }) + }} + + {{ _emailMacros.renderPreviewDialog(form.vars.id, form.vars.value.subject) }} + {% endif %} + {% if form.vars.value.id and resource_granted('oro_email_emailtemplate_clone') %} {{ UI.button({ 'path' : path('oro_email_emailtemplate_clone', {'id': form.vars.value.id}), @@ -27,6 +41,7 @@ }} {{ UI.buttonSeparator() }} {% endif %} + {% set cancelButton = UI.button({'path' : path('oro_email_emailtemplate_index'), 'title' : 'Cancel', 'label' : 'Cancel'}) %} {% if form.vars.value.isSystem == false %} {% if form.vars.value.id and resource_granted('oro_email_emailtemplate_remove') %} @@ -89,13 +104,17 @@ 'data': [ form_widget(form.translations) ] + },{ + 'title': 'Available variables'|trans, + 'data': [ + _emailMacros.renderAvailableVariablesWidget(form.vars.value.entityName, form.entityName.vars.id) + ] }] }] %} {% set data = { 'formErrors': form_errors(form)? form_errors(form) : null, 'dataBlocks': dataBlocks, - 'hiddenData': form_rest(form) } %} diff --git a/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig new file mode 100644 index 00000000000..f6442fbff0b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Resources/views/macros.html.twig @@ -0,0 +1,187 @@ +{% macro renderAvailableVariablesWidget(entityName, dependentFieldId) %} + + + + +
    +{% endmacro %} + +{% macro renderPreviewDialog(formName, title) %} + +{% endmacro %} + +{# + Render email address name, owner name or a link to owner view page can be rendered depends on given parameters + Parameters: + emailAddress - email address entity Oro\Bundle\EmailBundle\Entity\EmailAddress + emailAddressName - a string contains an email address. It is used if the email address has no owner + nameFormat - a format string used to render the owner full name + noLink - determines whether the rendering of a link to the owner view page is forbidden or not. Default value is false +#} +{% macro email_address(emailAddress, emailAddressName, nameFormat, noLink) -%} + {% macro email_address_text(emailAddress, nameFormat) -%} + {{ emailAddress.owner.fullname(nameFormat)|default('N/A')|raw }} + {%- endmacro -%} + {% macro email_address_link(emailAddress, nameFormat) -%} + {#- TODO: we need EntityConfig to get view url for an entity -#} + {{ _self.email_address_text(emailAddress, nameFormat) }} + {%- endmacro -%} + {% if emailAddress.owner is null -%} + {{ emailAddressName|raw }} + {%- else -%} + {% if noLink|default(false) -%} + {{ _self.email_address_text(emailAddress, nameFormat)|raw }} + {%- else -%} + {{ _self.email_address_link(emailAddress, nameFormat)|raw }} + {%- endif %} + {%- endif %} +{%- endmacro %} + +{# + Render the given email recipients + Parameters: + recipients - an array of Oro\Bundle\EmailBundle\Entity\EmailRecipients + nameFormat - a format string used to render the owner full name + noLink - determines whether the rendering of a link to the owner view page is forbidden or not. Default value is false +#} +{% macro recipient_email_addresses(recipients, nameFormat, noLink) -%} + {% set addreses = {} -%} + {% for recipient in recipients -%} + {{ _self.email_address(recipient.emailAddress, recipient.name, nameFormat, noLink) }} + {%- if not loop.last %}; {% endif %} + {%- endfor %} +{%- endmacro %} + +{# + Render the given email attachments + Parameters: + attachments - an array of Oro\Bundle\EmailBundle\Entity\EmailAttachment +#} +{% macro attachments(attachments) -%} + {% macro attachment_row_data(values) -%} + {%- for val in values -%} + + {{ val.fileName }} + + {%- endfor -%} + {%- endmacro -%} + {{ _self.attachment_row_data(attachments)|raw }} +{%- endmacro %} + +{# + Render email body + Parameters: + emailBody - email body entity Oro\Bundle\EmailBundle\Entity\EmailBody + cssClass - used to specify an additional CSS class for email body container HTML element +#} +{% macro body(emailBody, cssClass) -%} + {% if emailBody.bodyIsText -%} + + {%- else -%} + + {%- endif %} +{%- endmacro %} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Functional/ControllersTest.php b/src/Oro/Bundle/EmailBundle/Tests/Functional/ControllersTest.php new file mode 100644 index 00000000000..717505678cd --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Functional/ControllersTest.php @@ -0,0 +1,56 @@ +client = static::createClient(array(), ToolsAPI::generateBasicHeader()); + } + + public function testIndex() + { + $this->client->request('GET', $this->client->generate('oro_email_emailtemplate_index')); + $result = $this->client->getResponse(); + ToolsAPI::assertJsonResponse($result, 200, 'text/html; charset=UTF-8'); + } + + public function testCreate() + { + $this->markTestIncomplete('Skipped due to issue with dynamic form loading'); + $crawler = $this->client->request('GET', $this->client->generate('oro_email_emailtemplate_create')); + $dom = new \DOMDocument('1.0', 'UTF-8'); + $dom->loadHTML($crawler->html()); + $dom->getElementById('oro_email_emailtemplate'); + $form = $crawler->filterXPath("//form[@name='oro_email_emailtemplate']"); + + $form = $crawler->selectButton('Save and Close')->form(); + $fields = $form->all(); + $form['oro_email_emailtemplate[entityName]'] = 'Oro\Bundle\UserBundle\Entity\User'; + $form['oro_email_emailtemplate[name]'] = 'User Template'; + $form['oro_email_emailtemplate[translations][defaultLocale][en][content]'] = 'Content template'; + $form['oro_email_emailtemplate[translations][defaultLocale][en][subject]'] = 'Subject'; + $form['oro_email_emailtemplate[type]'] = 'html'; + + $this->client->followRedirects(true); + $crawler = $this->client->submit($form); + + $result = $this->client->getResponse(); + ToolsAPI::assertJsonResponse($result, 200, ''); + $this->assertContains("Template sucessfully saved", $crawler->html()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailEntityBatchProcessorTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailEntityBatchProcessorTest.php new file mode 100644 index 00000000000..19820c81e52 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailEntityBatchProcessorTest.php @@ -0,0 +1,251 @@ +ownerProvider = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProvider') + ->disableOriginalConstructor() + ->getMock(); + $this->addrManager = new EmailAddressManager( + 'Oro\Bundle\EmailBundle\Tests\Unit\Entity\TestFixtures', + 'Test%sProxy' + ); + $this->batch = new EmailEntityBatchProcessor($this->addrManager, $this->ownerProvider); + } + + public function testAddEmail() + { + $this->batch->addEmail(new Email()); + $this->assertCount(1, ReflectionUtil::getProtectedProperty($this->batch, 'emails')); + } + + public function testAddAddress() + { + $this->batch->addAddress($this->addrManager->newEmailAddress()->setEmail('Test@example.com')); + $this->assertCount(1, ReflectionUtil::getProtectedProperty($this->batch, 'addresses')); + + $this->assertEquals('Test@example.com', $this->batch->getAddress('TeST@example.com')->getEmail()); + $this->assertNull($this->batch->getAddress('Another@example.com')); + + $this->setExpectedException('LogicException'); + $this->batch->addAddress($this->addrManager->newEmailAddress()->setEmail('TEST@example.com')); + } + + public function testAddFolder() + { + $folder = new EmailFolder(); + $folder->setType('sent'); + $folder->setName('Test'); + $this->batch->addFolder($folder); + $this->assertCount(1, ReflectionUtil::getProtectedProperty($this->batch, 'folders')); + + $this->assertEquals('Test', $this->batch->getFolder('sent', 'TeST')->getName()); + $this->assertNull($this->batch->getFolder('sent', 'Another')); + + $folder1 = new EmailFolder(); + $folder1->setType('trash'); + $folder1->setName('Test'); + $this->batch->addFolder($folder1); + $this->assertCount(2, ReflectionUtil::getProtectedProperty($this->batch, 'folders')); + + $this->assertEquals('Test', $this->batch->getFolder('trash', 'TeST')->getName()); + $this->assertNull($this->batch->getFolder('trash', 'Another')); + + $this->setExpectedException('LogicException'); + $folder2 = new EmailFolder(); + $folder2->setType('sent'); + $folder2->setName('TEST'); + $this->batch->addFolder($folder2); + } + + public function testAddOrigin() + { + $origin = new EmailOrigin(); + $origin->setName('Test'); + $this->batch->addOrigin($origin); + $this->assertCount(1, ReflectionUtil::getProtectedProperty($this->batch, 'origins')); + + $this->assertEquals('Test', $this->batch->getOrigin('TeST')->getName()); + $this->assertNull($this->batch->getOrigin('Another')); + + $this->setExpectedException('LogicException'); + $origin1 = new EmailOrigin(); + $origin1->setName('TEST'); + $this->batch->addOrigin($origin1); + } + + /** + * @SuppressWarnings(PHPMD.ExcessiveMethodLength) + */ + public function testPersist() + { + $origin = new EmailOrigin(); + $origin->setName('Exist'); + $this->batch->addOrigin($origin); + $newOrigin = new EmailOrigin(); + $newOrigin->setName('New'); + $this->batch->addOrigin($newOrigin); + + $dbOrigin = new EmailOrigin(); + $dbOrigin->setName('DbExist'); + + $folder = new EmailFolder(); + $folder->setName('Exist'); + $folder->setOrigin($origin); + $this->batch->addFolder($folder); + $newFolder = new EmailFolder(); + $newFolder->setName('New'); + $newFolder->setOrigin($newOrigin); + $this->batch->addFolder($newFolder); + + $dbFolder = new EmailFolder(); + $dbFolder->setName('DbExist'); + $dbFolder->setOrigin($dbOrigin); + + $addr = $this->addrManager->newEmailAddress()->setEmail('Exist'); + $this->batch->addAddress($addr); + $newAddr = $this->addrManager->newEmailAddress()->setEmail('New'); + $this->batch->addAddress($newAddr); + + $dbAddr = $this->addrManager->newEmailAddress()->setEmail('DbExist'); + + $email1 = new Email(); + $email1->setFolder($folder); + $email1->setFromEmailAddress($addr); + $email1Recip1 = new EmailRecipient(); + $email1Recip1->setEmailAddress($addr); + $email1Recip2 = new EmailRecipient(); + $email1Recip2->setEmailAddress($newAddr); + $email1->addRecipient($email1Recip1); + $email1->addRecipient($email1Recip2); + $this->batch->addEmail($email1); + + $email2 = new Email(); + $email2->setFolder($newFolder); + $email2->setFromEmailAddress($newAddr); + $email2Recip1 = new EmailRecipient(); + $email2Recip1->setEmailAddress($addr); + $email2Recip2 = new EmailRecipient(); + $email2Recip2->setEmailAddress($newAddr); + $email2->addRecipient($email2Recip1); + $email2->addRecipient($email2Recip2); + $this->batch->addEmail($email2); + + $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $originRepo = $this->getMockBuilder('Doctrine\ORM\EntityRepository') + ->disableOriginalConstructor() + ->getMock(); + $folderRepo = $this->getMockBuilder('Doctrine\ORM\EntityRepository') + ->disableOriginalConstructor() + ->getMock(); + $addrRepo = $this->getMockBuilder('Doctrine\ORM\EntityRepository') + ->disableOriginalConstructor() + ->getMock(); + $em->expects($this->exactly(3)) + ->method('getRepository') + ->will( + $this->returnValueMap( + array( + array('OroEmailBundle:EmailOrigin', $originRepo), + array('OroEmailBundle:EmailFolder', $folderRepo), + array('Oro\Bundle\EmailBundle\Tests\Unit\Entity\TestFixtures\TestEmailAddressProxy', $addrRepo), + ) + ) + ); + + $originRepo->expects($this->exactly(2)) + ->method('findOneBy') + ->will( + $this->returnCallback( + function ($c) use (&$dbOrigin) { + return $c['name'] === 'Exist' ? $dbOrigin : null; + } + ) + ); + $folderRepo->expects($this->exactly(2)) + ->method('findOneBy') + ->will( + $this->returnCallback( + function ($c) use (&$dbFolder) { + return $c['name'] === 'Exist' ? $dbFolder : null; + } + ) + ); + $addrRepo->expects($this->exactly(2)) + ->method('findOneBy') + ->will( + $this->returnCallback( + function ($c) use (&$dbAddr) { + return $c['email'] === 'Exist' ? $dbAddr : null; + } + ) + ); + + $em->expects($this->exactly(5)) + ->method('persist') + ->with( + $this->logicalOr( + $this->identicalTo($newOrigin), + $this->identicalTo($newFolder), + $this->identicalTo($newAddr), + $this->identicalTo($email1), + $this->identicalTo($email2) + ) + ); + + $owner = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'); + + $this->ownerProvider->expects($this->any()) + ->method('findEmailOwner') + ->will($this->returnValue($owner)); + + $this->batch->persist($em); + + $this->assertTrue($dbOrigin === $email1->getFolder()->getOrigin()); + $this->assertTrue($newOrigin === $email2->getFolder()->getOrigin()); + $this->assertTrue($dbFolder === $email1->getFolder()); + $this->assertTrue($newFolder === $email2->getFolder()); + $this->assertTrue($dbAddr === $email1->getFromEmailAddress()); + $this->assertNull($email1->getFromEmailAddress()->getOwner()); + $this->assertTrue($newAddr === $email2->getFromEmailAddress()); + $this->assertTrue($owner === $email2->getFromEmailAddress()->getOwner()); + $email1Recipients = $email1->getRecipients(); + $this->assertTrue($dbAddr === $email1Recipients[0]->getEmailAddress()); + $this->assertNull($email1Recipients[0]->getEmailAddress()->getOwner()); + $this->assertTrue($newAddr === $email1Recipients[1]->getEmailAddress()); + $this->assertTrue($owner === $email1Recipients[1]->getEmailAddress()->getOwner()); + $email2Recipients = $email2->getRecipients(); + $this->assertTrue($dbAddr === $email2Recipients[0]->getEmailAddress()); + $this->assertNull($email2Recipients[0]->getEmailAddress()->getOwner()); + $this->assertTrue($newAddr === $email2Recipients[1]->getEmailAddress()); + $this->assertTrue($owner === $email2Recipients[1]->getEmailAddress()->getOwner()); + + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailEntityBuilderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailEntityBuilderTest.php new file mode 100644 index 00000000000..a48f05c12e6 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Builder/EmailEntityBuilderTest.php @@ -0,0 +1,214 @@ +batch = $this->getMockBuilder('Oro\Bundle\EmailBundle\Builder\EmailEntityBatchProcessor') + ->disableOriginalConstructor() + ->getMock(); + $addrManager = new EmailAddressManager('Oro\Bundle\EmailBundle\Tests\Unit\Entity\TestFixtures', 'Test%sProxy'); + $this->builder = new EmailEntityBuilder($this->batch, $addrManager); + } + + private function initEmailStorage() + { + $storage = array(); + $this->batch->expects($this->any()) + ->method('getAddress') + ->will( + $this->returnCallback( + function ($email) use (&$storage) { + return isset($storage[$email]) ? $storage[$email] : null; + } + ) + ); + $this->batch->expects($this->any()) + ->method('addAddress') + ->will( + $this->returnCallback( + function ($obj) use (&$storage) { + $storage[$obj->getEmail()] = $obj; + } + ) + ); + } + + public function testEmail() + { + $this->initEmailStorage(); + + $date = new \DateTime('now'); + $email = $this->builder->email( + 'testSubject', + '"Test" ', + '"Test1" ', + $date, + $date, + $date, + Email::NORMAL_IMPORTANCE, + array('"Test2" ', 'test1@example.com') + ); + + $this->assertEquals('testSubject', $email->getSubject()); + $this->assertEquals('"Test" ', $email->getFromName()); + $this->assertEquals('test@example.com', $email->getFromEmailAddress()->getEmail()); + $this->assertEquals($date, $email->getSentAt()); + $this->assertEquals($date, $email->getReceivedAt()); + $this->assertEquals($date, $email->getInternalDate()); + $this->assertEquals(Email::NORMAL_IMPORTANCE, $email->getImportance()); + $to = $email->getRecipients(EmailRecipient::TO); + $this->assertEquals('"Test1" ', $to[0]->getName()); + $this->assertEquals('test1@example.com', $to[0]->getEmailAddress()->getEmail()); + $cc = $email->getRecipients(EmailRecipient::CC); + $this->assertEquals('"Test2" ', $cc[1]->getName()); + $this->assertEquals('test2@example.com', $cc[1]->getEmailAddress()->getEmail()); + $this->assertEquals('test1@example.com', $cc[2]->getName()); + $this->assertEquals('test1@example.com', $cc[2]->getEmailAddress()->getEmail()); + $bcc = $email->getRecipients(EmailRecipient::BCC); + $this->assertCount(0, $bcc); + } + + public function testToRecipient() + { + $this->initEmailStorage(); + $result = $this->builder->recipientTo('"Test" '); + + $this->assertEquals(EmailRecipient::TO, $result->getType()); + $this->assertEquals('"Test" ', $result->getName()); + $this->assertEquals('test@example.com', $result->getEmailAddress()->getEmail()); + } + + public function testCcRecipient() + { + $this->initEmailStorage(); + $result = $this->builder->recipientCc('"Test" '); + + $this->assertEquals(EmailRecipient::CC, $result->getType()); + $this->assertEquals('"Test" ', $result->getName()); + $this->assertEquals('test@example.com', $result->getEmailAddress()->getEmail()); + } + + public function testBccRecipient() + { + $this->initEmailStorage(); + $result = $this->builder->recipientBcc('"Test" '); + + $this->assertEquals(EmailRecipient::BCC, $result->getType()); + $this->assertEquals('"Test" ', $result->getName()); + $this->assertEquals('test@example.com', $result->getEmailAddress()->getEmail()); + } + + public function testOrigin() + { + $storage = array(); + $this->batch->expects($this->exactly(2)) + ->method('getOrigin') + ->will( + $this->returnCallback( + function ($name) use (&$storage) { + return isset($storage[$name]) ? $storage[$name] : null; + } + ) + ); + $this->batch->expects($this->once()) + ->method('addOrigin') + ->will( + $this->returnCallback( + function ($obj) use (&$storage) { + $storage[$obj->getName()] = $obj; + } + ) + ); + + $result = $this->builder->origin('test'); + + $this->assertEquals('test', $result->getName()); + $this->assertTrue($result === $this->builder->origin('test')); + } + + public function testFolder() + { + $storage = array(); + $this->batch->expects($this->exactly(10)) + ->method('getFolder') + ->will( + $this->returnCallback( + function ($type, $name) use (&$storage) { + return isset($storage[$type . $name]) ? $storage[$type . $name] : null; + } + ) + ); + $this->batch->expects($this->exactly(5)) + ->method('addFolder') + ->will( + $this->returnCallback( + function ($obj) use (&$storage) { + $storage[$obj->getType() . $obj->getName()] = $obj; + } + ) + ); + + $inbox = $this->builder->folderInbox('test'); + $sent = $this->builder->folderSent('test'); + $drafts = $this->builder->folderDrafts('test'); + $trash = $this->builder->folderTrash('test'); + $other = $this->builder->folderOther('test'); + + $this->assertEquals('test', $inbox->getName()); + $this->assertEquals('test', $sent->getName()); + $this->assertEquals('test', $drafts->getName()); + $this->assertEquals('test', $trash->getName()); + $this->assertEquals('test', $other->getName()); + $this->assertTrue($inbox === $this->builder->folderInbox('test')); + $this->assertTrue($sent === $this->builder->folderSent('test')); + $this->assertTrue($drafts === $this->builder->folderDrafts('test')); + $this->assertTrue($trash === $this->builder->folderTrash('test')); + $this->assertTrue($other === $this->builder->folderOther('test')); + } + + public function testBody() + { + $body = $this->builder->body('testContent', true, true); + + $this->assertEquals('testContent', $body->getContent()); + $this->assertFalse($body->getBodyIsText()); + $this->assertTrue($body->getPersistent()); + } + + public function testAttachment() + { + $attachment = $this->builder->attachment('testFileName', 'testContentType'); + + $this->assertEquals('testFileName', $attachment->getFileName()); + $this->assertEquals('testContentType', $attachment->getContentType()); + } + + public function testAttachmentContent() + { + $attachmentContent = $this->builder->attachmentContent('testContent', 'testEncoding'); + + $this->assertEquals('testContent', $attachmentContent->getValue()); + $this->assertEquals('testEncoding', $attachmentContent->getContentTransferEncoding()); + } + + public function testGetBatch() + { + $this->assertTrue($this->batch === $this->builder->getBatch()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Cache/EntityCacheClearerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Cache/EntityCacheClearerTest.php new file mode 100644 index 00000000000..6dc3fd2dec5 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Cache/EntityCacheClearerTest.php @@ -0,0 +1,30 @@ +getMockBuilder('Oro\Bundle\EmailBundle\Cache\EntityCacheClearer') + ->setConstructorArgs(array('SomeDir', 'Test\SomeNamespace', 'Test%sProxy')) + ->setMethods(array('createFilesystem')) + ->getMock(); + + $fs = $this->getMockBuilder('Symfony\Component\Filesystem\Filesystem') + ->disableOriginalConstructor() + ->getMock(); + + $clearer->expects($this->once()) + ->method('createFilesystem') + ->will($this->returnValue($fs)); + + $fs->expects($this->once()) + ->method('remove') + ->with($this->equalTo('SomeDir/Test/SomeNamespace/TestEmailAddressProxy.php')); + + $clearer->clear(''); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Cache/EntityCacheWarmerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Cache/EntityCacheWarmerTest.php new file mode 100644 index 00000000000..6e3807229b2 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Cache/EntityCacheWarmerTest.php @@ -0,0 +1,104 @@ +getMockBuilder('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderStorage') + ->disableOriginalConstructor() + ->getMock(); + + $oroProvider = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $oroProvider->expects($this->once()) + ->method('getEmailOwnerClass') + ->will($this->returnValue('Oro\TestUser')); + + $oroCrmProvider = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $oroCrmProvider->expects($this->once()) + ->method('getEmailOwnerClass') + ->will($this->returnValue('OroCRM\TestContact')); + + $acmeProvider = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $acmeProvider->expects($this->once()) + ->method('getEmailOwnerClass') + ->will($this->returnValue('Acme\TestUser')); + + $storage->expects($this->once()) + ->method('getProviders') + ->will($this->returnValue(array($oroProvider, $oroCrmProvider, $acmeProvider))); + + $warmer = $this->getMockBuilder('Oro\Bundle\EmailBundle\Cache\EntityCacheWarmer') + ->setConstructorArgs(array($storage, 'SomeDir', 'Test\SomeNamespace', 'Test%sProxy')) + ->setMethods(array('createFilesystem', 'createTwigEnvironment', 'writeCacheFile')) + ->getMock(); + + $fs = $this->getMockBuilder('Symfony\Component\Filesystem\Filesystem') + ->disableOriginalConstructor() + ->getMock(); + + $twig = $this->getMockBuilder('\Twig_Environment') + ->disableOriginalConstructor() + ->getMock(); + + $warmer->expects($this->once()) + ->method('createFilesystem') + ->will($this->returnValue($fs)); + + $warmer->expects($this->once()) + ->method('createTwigEnvironment') + ->will($this->returnValue($twig)); + + $fs->expects($this->once()) + ->method('exists') + ->with($this->equalTo('SomeDir/Test/SomeNamespace')) + ->will($this->returnValue(false)); + + $fs->expects($this->once()) + ->method('mkdir') + ->with($this->equalTo('SomeDir/Test/SomeNamespace'), $this->equalTo(0777)); + + $twig->expects($this->once()) + ->method('render') + ->with( + $this->equalTo('EmailAddress.php.twig'), + $this->equalTo( + array( + 'namespace' => 'Test\SomeNamespace', + 'className' => 'TestEmailAddressProxy', + 'owners' => array( + array( + 'targetEntity' => 'Oro\TestUser', + 'columnName' => 'owner_testuser_id', + 'fieldName' => 'owner1' + ), + array( + 'targetEntity' => 'OroCRM\TestContact', + 'columnName' => 'owner_testcontact_id', + 'fieldName' => 'owner2' + ), + array( + 'targetEntity' => 'Acme\TestUser', + 'columnName' => 'owner_acme_testuser_id', + 'fieldName' => 'owner3' + ), + ) + ) + ) + ) + ->will($this->returnValue('testContent')); + + $warmer->expects($this->once()) + ->method('writeCacheFile') + ->with( + $this->equalTo('SomeDir/Test/SomeNamespace/TestEmailAddressProxy.php'), + $this->equalTo('testContent') + ); + + $warmer->warmup(''); + $this->assertFalse($warmer->isOptional()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/DependencyInjection/Compiler/EmailOwnerConfigurationPassTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/DependencyInjection/Compiler/EmailOwnerConfigurationPassTest.php new file mode 100644 index 00000000000..7bb8e0a084a --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/DependencyInjection/Compiler/EmailOwnerConfigurationPassTest.php @@ -0,0 +1,113 @@ +getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + + $containerBuilder->expects($this->once()) + ->method('hasDefinition') + ->with($this->equalTo(EmailOwnerConfigurationPass::SERVICE_KEY)) + ->will($this->returnValue(false)); + $containerBuilder->expects($this->never()) + ->method('getDefinition'); + $containerBuilder->expects($this->never()) + ->method('findTaggedServiceIds'); + + $pass = new EmailOwnerConfigurationPass(); + $pass->process($containerBuilder); + } + + public function testProcess() + { + $service = $this->getMockBuilder('Symfony\Component\DependencyInjection\Definition') + ->disableOriginalConstructor() + ->getMock(); + $doctrineTargetEntityResolver = $this->getMockBuilder('Symfony\Component\DependencyInjection\Definition') + ->disableOriginalConstructor() + ->getMock(); + + $containerBuilder = $this->getMock('Symfony\Component\DependencyInjection\ContainerBuilder'); + + $containerBuilder->expects($this->exactly(2)) + ->method('hasDefinition') + ->will( + $this->returnValueMap( + array( + array(EmailOwnerConfigurationPass::SERVICE_KEY, true), + array('doctrine.orm.listeners.resolve_target_entity', true), + ) + ) + ); + $containerBuilder->expects($this->exactly(2)) + ->method('getDefinition') + ->will( + $this->returnValueMap( + array( + array(EmailOwnerConfigurationPass::SERVICE_KEY, $service), + array('doctrine.orm.listeners.resolve_target_entity', $doctrineTargetEntityResolver), + ) + ) + ); + $containerBuilder->expects($this->once()) + ->method('findTaggedServiceIds') + ->will( + $this->returnValue( + array( + 'provider1' => array(array('order' => 3)), + 'provider2' => array(array('order' => 1)), + 'provider4' => array(), + 'provider3' => array(array('order' => 2)), + ) + ) + ); + + $serviceMethodCalls = array(); + $serviceProviders = array(); + $service->expects($this->exactly(4)) + ->method('addMethodCall') + ->will( + $this->returnCallback( + function ($method, array $arguments) use (&$serviceMethodCalls, &$serviceProviders) { + $serviceMethodCalls[] = $method; + $serviceProviders[] = (string)$arguments[0]; + } + ) + ); + + $containerBuilder->expects($this->once()) + ->method('getParameter') + ->with($this->equalTo('oro_email.entity.cache_namespace')) + ->will($this->returnValue('SomeNamespace')); + + $doctrineTargetEntityResolver->expects($this->once()) + ->method('addMethodCall') + ->with( + $this->equalTo('addResolveTargetEntity'), + $this->equalTo( + array( + 'Oro\Bundle\EmailBundle\Entity\EmailAddress', + 'SomeNamespace\EmailAddressProxy', + array() + ) + ) + ); + + $pass = new EmailOwnerConfigurationPass(); + $pass->process($containerBuilder); + + $this->assertEquals( + array('addProvider', 'addProvider', 'addProvider', 'addProvider'), + $serviceMethodCalls + ); + $this->assertEquals( + array('provider2', 'provider3', 'provider1', 'provider4'), + $serviceProviders + ); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAddressTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAddressTest.php new file mode 100644 index 00000000000..df40da0cfea --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAddressTest.php @@ -0,0 +1,39 @@ +assertEquals(1, $entity->getId()); + } + + public function testEmailGetterAndSetter() + { + $entity = new EmailAddress(); + $entity->setEmail('test'); + $this->assertEquals('test', $entity->getEmail()); + } + + public function testCreatedAtGetterAndSetter() + { + $date = new \DateTime('now', new \DateTimeZone('UTC')); + + $entity = new EmailAddress($date); + $this->assertEquals($date, $entity->getCreatedAt()); + } + + public function testUpdatedAtGetterAndSetter() + { + $date = new \DateTime('now', new \DateTimeZone('UTC')); + + $entity = new EmailAddress($date); + $this->assertEquals($date, $entity->getUpdatedAt()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAttachmentContentTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAttachmentContentTest.php new file mode 100644 index 00000000000..edd466d9b83 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAttachmentContentTest.php @@ -0,0 +1,40 @@ +assertEquals(1, $entity->getId()); + } + + public function testEmailAttachmentGetterAndSetter() + { + $emailAttachment = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailAttachment'); + + $entity = new EmailAttachmentContent(); + $entity->setEmailAttachment($emailAttachment); + + $this->assertTrue($emailAttachment === $entity->getEmailAttachment()); + } + + public function testValueGetterAndSetter() + { + $entity = new EmailAttachmentContent(); + $entity->setValue('test'); + $this->assertEquals('test', $entity->getValue()); + } + + public function testContentTransferEncodingGetterAndSetter() + { + $entity = new EmailAttachmentContent(); + $entity->setContentTransferEncoding('test'); + $this->assertEquals('test', $entity->getContentTransferEncoding()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAttachmentTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAttachmentTest.php new file mode 100644 index 00000000000..5bf303fedd7 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailAttachmentTest.php @@ -0,0 +1,50 @@ +assertEquals(1, $entity->getId()); + } + + public function testFileNameGetterAndSetter() + { + $entity = new EmailAttachment(); + $entity->setFileName('test'); + $this->assertEquals('test', $entity->getFileName()); + } + + public function testContentTypeGetterAndSetter() + { + $entity = new EmailAttachment(); + $entity->setContentType('test'); + $this->assertEquals('test', $entity->getContentType()); + } + + public function testContentGetterAndSetter() + { + $content = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailAttachmentContent'); + + $entity = new EmailAttachment(); + $entity->setContent($content); + + $this->assertTrue($content === $entity->getContent()); + } + + public function testEmailBodyGetterAndSetter() + { + $emailBody = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailBody'); + + $entity = new EmailAttachment(); + $entity->setEmailBody($emailBody); + + $this->assertTrue($emailBody === $entity->getEmailBody()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php new file mode 100644 index 00000000000..dfb9a3afca9 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailBodyTest.php @@ -0,0 +1,81 @@ +assertEquals(1, $entity->getId()); + } + + public function testContentGetterAndSetter() + { + $entity = new EmailBody(); + $entity->setContent('test'); + $this->assertEquals('test', $entity->getContent()); + } + + public function testBodyIsTextGetterAndSetter() + { + $entity = new EmailBody(); + $entity->setBodyIsText(true); + $this->assertEquals(true, $entity->getBodyIsText()); + } + + public function testHasAttachmentsGetterAndSetter() + { + $entity = new EmailBody(); + $entity->setHasAttachments(true); + $this->assertEquals(true, $entity->getHasAttachments()); + } + + public function testPersistentGetterAndSetter() + { + $entity = new EmailBody(); + $entity->setPersistent(true); + $this->assertEquals(true, $entity->getPersistent()); + } + + public function testHeaderGetterAndSetter() + { + $email = $this->getMock('Oro\Bundle\EmailBundle\Entity\Email'); + + $entity = new EmailBody(); + $entity->setHeader($email); + + $this->assertTrue($email === $entity->getHeader()); + } + + public function testAttachmentGetterAndSetter() + { + $attachment = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailAttachment'); + + $entity = new EmailBody(); + $entity->addAttachment($attachment); + + $attachments = $entity->getAttachments(); + + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $attachments); + $this->assertCount(1, $attachments); + $this->assertTrue($attachment === $attachments[0]); + } + + public function testBeforeSave() + { + $entity = new EmailBody(); + $entity->beforeSave(); + + $createdAt = new \DateTime('now', new \DateTimeZone('UTC')); + + $this->assertEquals(false, $entity->getBodyIsText()); + $this->assertEquals(false, $entity->getHasAttachments()); + $this->assertEquals(false, $entity->getPersistent()); + $this->assertGreaterThanOrEqual($createdAt, $entity->getCreatedAt()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailFolderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailFolderTest.php new file mode 100644 index 00000000000..7e4ba94a69f --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailFolderTest.php @@ -0,0 +1,54 @@ +assertEquals(1, $entity->getId()); + } + + public function testNameGetterAndSetter() + { + $entity = new EmailFolder(); + $entity->setName('test'); + $this->assertEquals('test', $entity->getName()); + } + + public function testTypeGetterAndSetter() + { + $entity = new EmailFolder(); + $entity->setType('test'); + $this->assertEquals('test', $entity->getType()); + } + + public function testOriginGetterAndSetter() + { + $origin = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOrigin'); + + $entity = new EmailFolder(); + $entity->setOrigin($origin); + + $this->assertTrue($origin === $entity->getOrigin()); + } + + public function testEmailGetterAndSetter() + { + $email = $this->getMock('Oro\Bundle\EmailBundle\Entity\Email'); + + $entity = new EmailFolder(); + $entity->addEmail($email); + + $emails = $entity->getEmails(); + + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $emails); + $this->assertCount(1, $emails); + $this->assertTrue($email === $emails[0]); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailOriginTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailOriginTest.php new file mode 100644 index 00000000000..31d24a42d90 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailOriginTest.php @@ -0,0 +1,37 @@ +assertEquals(1, $entity->getId()); + } + + public function testNameGetterAndSetter() + { + $entity = new EmailOrigin(); + $entity->setName('test'); + $this->assertEquals('test', $entity->getName()); + } + + public function testFolderGetterAndSetter() + { + $folder = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailFolder'); + + $entity = new EmailOrigin(); + $entity->addFolder($folder); + + $folders = $entity->getFolders(); + + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $folders); + $this->assertCount(1, $folders); + $this->assertTrue($folder === $folders[0]); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailRecipientTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailRecipientTest.php new file mode 100644 index 00000000000..0590d4e71ef --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailRecipientTest.php @@ -0,0 +1,50 @@ +assertEquals(1, $entity->getId()); + } + + public function testNameGetterAndSetter() + { + $entity = new EmailRecipient(); + $entity->setName('test'); + $this->assertEquals('test', $entity->getName()); + } + + public function testTypeGetterAndSetter() + { + $entity = new EmailRecipient(); + $entity->setType('test'); + $this->assertEquals('test', $entity->getType()); + } + + public function testEmailAddressGetterAndSetter() + { + $emailAddress = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailAddress'); + + $entity = new EmailRecipient(); + $entity->setEmailAddress($emailAddress); + + $this->assertTrue($emailAddress === $entity->getEmailAddress()); + } + + public function testEmailGetterAndSetter() + { + $email = $this->getMock('Oro\Bundle\EmailBundle\Entity\Email'); + + $entity = new EmailRecipient(); + $entity->setEmail($email); + + $this->assertTrue($email === $entity->getEmail()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTemplateTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTemplateTest.php index 9025546cd8f..9e142ed689f 100644 --- a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTemplateTest.php +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTemplateTest.php @@ -2,8 +2,9 @@ namespace Oro\Bundle\EmailBundle\Tests\Unit\Entity; +use Doctrine\Common\Collections\ArrayCollection; + use Oro\Bundle\EmailBundle\Entity\EmailTemplate; -use Oro\Bundle\EmailBundle\Tests\Unit\Form\Type\EmailTemplateTranslationTypeTest; class EmailTemplateTest extends \PHPUnit_Framework_TestCase { @@ -45,8 +46,9 @@ public function testSettersGetters() $this->assertEquals('abc', $this->emailTemplate->{'get'.ucfirst($field)}()); $translation = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailTemplateTranslation'); - $this->emailTemplate->setTranslations(array($translation)); - $this->assertEquals($this->emailTemplate->getTranslations(), array($translation)); + $this->emailTemplate->setTranslations(new ArrayCollection(array($translation))); + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $this->emailTemplate->getTranslations()); + $this->assertCount(1, $this->emailTemplate->getTranslations()); } } @@ -55,11 +57,16 @@ public function testSettersGetters() */ public function testCloneAndToString() { + $translation = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailTemplateTranslation'); + + $this->emailTemplate->getTranslations()->add($translation); + $clone = clone $this->emailTemplate; $this->assertNull($clone->getId()); $this->assertEquals($clone->getParent(), $this->emailTemplate->getId()); $this->assertEquals($this->emailTemplate->getName(), (string)$this->emailTemplate); + $this->assertFalse($clone->getTranslations()->first() === $translation); } } diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php new file mode 100644 index 00000000000..572129fd00a --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/EmailTest.php @@ -0,0 +1,169 @@ +assertEquals(1, $entity->getId()); + } + + public function testSubjectGetterAndSetter() + { + $entity = new Email(); + $entity->setSubject('test'); + $this->assertEquals('test', $entity->getSubject()); + } + + public function testFromNameGetterAndSetter() + { + $entity = new Email(); + $entity->setFromName('test'); + $this->assertEquals('test', $entity->getFromName()); + } + + public function testFromEmailAddressGetterAndSetter() + { + $emailAddress = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailAddress'); + + $entity = new Email(); + $entity->setFromEmailAddress($emailAddress); + + $this->assertTrue($emailAddress === $entity->getFromEmailAddress()); + } + + public function testRecipientGetterAndSetter() + { + $toRecipient = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailRecipient'); + $toRecipient->expects($this->any()) + ->method('getType') + ->will($this->returnValue('to')); + + $ccRecipient = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailRecipient'); + $ccRecipient->expects($this->any()) + ->method('getType') + ->will($this->returnValue('cc')); + + $bccRecipient = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailRecipient'); + $bccRecipient->expects($this->any()) + ->method('getType') + ->will($this->returnValue('bcc')); + + $entity = new Email(); + $entity->addRecipient($toRecipient); + $entity->addRecipient($ccRecipient); + $entity->addRecipient($bccRecipient); + + $recipients = $entity->getRecipients(); + + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $recipients); + $this->assertCount(3, $recipients); + $this->assertTrue($toRecipient === $recipients[0]); + $this->assertTrue($ccRecipient === $recipients[1]); + $this->assertTrue($bccRecipient === $recipients[2]); + + $recipients = $entity->getRecipients('to'); + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $recipients); + $this->assertCount(1, $recipients); + $this->assertTrue($toRecipient === $recipients->first()); + + $recipients = $entity->getRecipients('cc'); + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $recipients); + $this->assertCount(1, $recipients); + $this->assertTrue($ccRecipient === $recipients->first()); + + $recipients = $entity->getRecipients('bcc'); + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $recipients); + $this->assertCount(1, $recipients); + $this->assertTrue($bccRecipient === $recipients->first()); + } + + public function testReceivedAtGetterAndSetter() + { + $entity = new Email(); + $date = new \DateTime('now', new \DateTimeZone('UTC')); + $entity->setReceivedAt($date); + $this->assertEquals($date, $entity->getReceivedAt()); + } + + public function testSentAtGetterAndSetter() + { + $entity = new Email(); + $date = new \DateTime('now', new \DateTimeZone('UTC')); + $entity->setSentAt($date); + $this->assertEquals($date, $entity->getSentAt()); + } + + public function testImportanceGetterAndSetter() + { + $entity = new Email(); + $entity->setImportance(1); + $this->assertEquals(1, $entity->getImportance()); + } + + public function testInternalDateGetterAndSetter() + { + $entity = new Email(); + $date = new \DateTime('now', new \DateTimeZone('UTC')); + $entity->setInternalDate($date); + $this->assertEquals($date, $entity->getInternalDate()); + } + + public function testMessageIdGetterAndSetter() + { + $entity = new Email(); + $entity->setMessageId('test'); + $this->assertEquals('test', $entity->getMessageId()); + } + + public function testXMessageIdGetterAndSetter() + { + $entity = new Email(); + $entity->setXMessageId('test'); + $this->assertEquals('test', $entity->getXMessageId()); + } + + public function testXThreadIdGetterAndSetter() + { + $entity = new Email(); + $entity->setXThreadId('test'); + $this->assertEquals('test', $entity->getXThreadId()); + } + + public function testFolderGetterAndSetter() + { + $folder = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailFolder'); + + $entity = new Email(); + $entity->setFolder($folder); + + $this->assertTrue($folder === $entity->getFolder()); + } + + public function testEmailBodyGetterAndSetter() + { + $emailBody = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailBody'); + + $entity = new Email(); + $entity->setEmailBody($emailBody); + + $this->assertTrue($emailBody === $entity->getEmailBody()); + } + + public function testBeforeSave() + { + $entity = new Email(); + $entity->beforeSave(); + + $createdAt = new \DateTime('now', new \DateTimeZone('UTC')); + + $this->assertEquals(Email::NORMAL_IMPORTANCE, $entity->getImportance()); + $this->assertGreaterThanOrEqual($createdAt, $entity->getCreatedAt()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Manager/EmailAddressManagerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Manager/EmailAddressManagerTest.php new file mode 100644 index 00000000000..eb31469dd8d --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Manager/EmailAddressManagerTest.php @@ -0,0 +1,35 @@ +assertEquals(new TestEmailAddressProxy(), $manager->newEmailAddress()); + } + + public function testGetEmailAddressRepository() + { + $manager = new EmailAddressManager('Oro\Bundle\EmailBundle\Tests\Unit\Entity\TestFixtures', 'Test%sProxy'); + + $repo = $this->getMockBuilder('Doctrine\ORM\EntityRepository') + ->disableOriginalConstructor() + ->getMock(); + + $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $em->expects($this->once()) + ->method('getRepository') + ->with($this->equalTo('Oro\Bundle\EmailBundle\Tests\Unit\Entity\TestFixtures\TestEmailAddressProxy')) + ->will($this->returnValue($repo)); + + $this->assertTrue($repo === $manager->getEmailAddressRepository($em)); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Manager/EmailOwnerManagerTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Manager/EmailOwnerManagerTest.php new file mode 100644 index 00000000000..7a12f39570f --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Manager/EmailOwnerManagerTest.php @@ -0,0 +1,446 @@ +uow = $this->getMockBuilder('Doctrine\ORM\UnitOfWork') + ->disableOriginalConstructor() + ->getMock(); + + $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->flushEventArgs = $this->getMockBuilder('Doctrine\ORM\Event\OnFlushEventArgs') + ->disableOriginalConstructor() + ->getMock(); + } + + private function getEmailOwnerProviderStorageMock() + { + $provider = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $provider->expects($this->any()) + ->method('getProviders') + ->will($this->returnValue('SomeEntity')); + $storage = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderStorage'); + $storage->expects($this->any()) + ->method('getProviders') + ->will($this->returnValue(array($provider))); + + return $storage; + } + + /** + * @dataProvider handleOnFlushProvider + */ + public function testHandleOnFlush( + $handleInsertionsOrUpdatesReturnValue, + $handleDeletionsReturnValue, + $expectComputeChangeSets + ) { + $this->initOnFlush(); + + $this->em->expects($this->once()) + ->method('getUnitOfWork') + ->will($this->returnValue($this->uow)); + + $this->flushEventArgs->expects($this->once()) + ->method('getEntityManager') + ->will($this->returnValue($this->em)); + + $manager = $this->createEmailOwnerManagerMockBuilder() + ->setMethods(array('handleInsertionsOrUpdates', 'handleDeletions')) + ->getMock(); + + $this->uow->expects($this->once()) + ->method('getScheduledEntityInsertions') + ->will($this->returnValue(array('ScheduledEntityInsertions'))); + + $this->uow->expects($this->once()) + ->method('getScheduledEntityUpdates') + ->will($this->returnValue(array('ScheduledEntityUpdates'))); + + $this->uow->expects($this->once()) + ->method('getScheduledEntityDeletions') + ->will($this->returnValue(array('ScheduledEntityDeletions'))); + + $manager->expects($this->exactly(2)) + ->method('handleInsertionsOrUpdates') + ->with( + $this->logicalOr( + $this->equalTo(array('ScheduledEntityInsertions')), + $this->equalTo(array('ScheduledEntityUpdates')) + ), + $this->identicalTo($this->em), + $this->identicalTo($this->uow) + ) + ->will($this->returnValue($handleInsertionsOrUpdatesReturnValue)); + + $manager->expects($this->once()) + ->method('handleDeletions') + ->with( + $this->equalTo(array('ScheduledEntityDeletions')), + $this->identicalTo($this->em) + ) + ->will($this->returnValue($handleDeletionsReturnValue)); + + $this->uow->expects($expectComputeChangeSets ? $this->once() : $this->never()) + ->method('computeChangeSets'); + + $manager->handleOnFlush($this->flushEventArgs); + } + + public function handleOnFlushProvider() + { + return array( + 'no changes' => array(false, false, false), + 'has updates' => array(true, false, true), + 'has deletion' => array(false, true, true), + 'has updates and deletion' => array(true, true, true), + ); + } + + /** + * @dataProvider handleInsertionsOrUpdatesProvider + */ + public function testHandleInsertionsOrUpdates( + $entity, + $processInsertionOrUpdateEntityCall, + $processInsertionOrUpdateEntityReturnValue, + $returnValue + ) { + $this->initOnFlush(); + + $manager = $this->createEmailOwnerManagerMockBuilder() + ->setMethods(array('processInsertionOrUpdateEntity')) + ->getMock(); + + if ($processInsertionOrUpdateEntityCall) { + if ($entity instanceof EmailOwnerInterface) { + $args = array('SomeField', $entity, $entity, $this->em, $this->uow); + } elseif ($entity instanceof EmailInterface) { + $args = array( + 'SomeField', + $entity, + $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'), + $this->em, + $this->uow + ); + } else { + $this->fail('Unexpected entity type'); + + return; + } + + $manager->expects($this->once()) + ->method('processInsertionOrUpdateEntity') + ->with( + $this->equalTo($args[0]), + $this->equalTo($args[1]), + $this->equalTo($args[2]), + $this->equalTo($args[3]), + $this->equalTo($args[4]) + ) + ->will($this->returnValue($processInsertionOrUpdateEntityReturnValue)); + } else { + $manager->expects($this->never()) + ->method('processInsertionOrUpdateEntity'); + } + + $result = ReflectionUtil::callProtectedMethod( + $manager, + 'handleInsertionsOrUpdates', + array($entity === null ? array() : array($entity), $this->em, $this->uow) + ); + $this->assertEquals($returnValue, $result); + } + + public function handleInsertionsOrUpdatesProvider() + { + return array( + 'no items' => array(null, false, false, false), + 'not tracked item' => array(new \stdClass(), false, false, false), + 'EmailOwnerInterface nothing to change' => + array($this->handleInsertionsOrUpdatesPrepareMockForEmailOwnerInterface(), true, false, false), + 'EmailOwnerInterface' => array( + $this->handleInsertionsOrUpdatesPrepareMockForEmailOwnerInterface(), + true, + true, + true + ), + 'EmailInterface nothing to change' => + array($this->handleInsertionsOrUpdatesPrepareMockForEmailInterface(), true, false, false), + 'EmailInterface' => array($this->handleInsertionsOrUpdatesPrepareMockForEmailInterface(), true, true, true), + ); + } + + private function handleInsertionsOrUpdatesPrepareMockForEmailOwnerInterface() + { + $mock = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'); + $mock->expects($this->once()) + ->method('getPrimaryEmailField') + ->will($this->returnValue('SomeField')); + + return $mock; + } + + private function handleInsertionsOrUpdatesPrepareMockForEmailInterface() + { + $mock = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailInterface'); + $mock->expects($this->once()) + ->method('getEmailField') + ->will($this->returnValue('SomeField')); + $mock->expects($this->once()) + ->method('getEmailOwner') + ->will($this->returnValue($this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'))); + + return $mock; + } + + /** + * @dataProvider handleDeletionsProvider + */ + public function testHandleDeletions( + $entity, + $unbindEmailAddressCall, + $unbindEmailAddressReturnValue, + $returnValue + ) { + $this->initOnFlush(); + + $manager = $this->createEmailOwnerManagerMockBuilder() + ->setMethods(array('unbindEmailAddress')) + ->getMock(); + + if ($unbindEmailAddressCall) { + if ($entity instanceof EmailOwnerInterface) { + $args = array($this->em, $entity, null); + $manager->expects($this->once()) + ->method('unbindEmailAddress') + ->with($this->equalTo($args[0]), $this->equalTo($args[1])) + ->will($this->returnValue($unbindEmailAddressReturnValue)); + } elseif ($entity instanceof EmailInterface) { + $args = array($this->em, $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'), $entity); + $manager->expects($this->once()) + ->method('unbindEmailAddress') + ->with($this->equalTo($args[0]), $this->equalTo($args[1]), $this->equalTo($args[2])) + ->will($this->returnValue($unbindEmailAddressReturnValue)); + } else { + $this->fail('Unexpected entity type'); + + return; + } + } else { + $manager->expects($this->never()) + ->method('unbindEmailAddress'); + } + + $result = ReflectionUtil::callProtectedMethod( + $manager, + 'handleDeletions', + array($entity === null ? array() : array($entity), $this->em) + ); + $this->assertEquals($returnValue, $result); + } + + public function handleDeletionsProvider() + { + return array( + 'no items' => array(null, false, false, false), + 'not tracked item' => array(new \stdClass(), false, false, false), + 'EmailOwnerInterface nothing to change' => + array($this->handleDeletionsPrepareMockForEmailOwnerInterface(), true, false, false), + 'EmailOwnerInterface' => array($this->handleDeletionsPrepareMockForEmailOwnerInterface(), true, true, true), + 'EmailInterface nothing to change' => + array($this->handleDeletionsPrepareMockForEmailInterface(), true, false, false), + 'EmailInterface' => array($this->handleDeletionsPrepareMockForEmailInterface(), true, true, true), + ); + } + + private function handleDeletionsPrepareMockForEmailOwnerInterface() + { + $mock = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'); + + return $mock; + } + + private function handleDeletionsPrepareMockForEmailInterface() + { + $mock = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailInterface'); + $mock->expects($this->once()) + ->method('getEmailOwner') + ->will($this->returnValue($this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'))); + + return $mock; + } + + public function testProcessInsertionOrUpdateEntityNoEmailField() + { + $this->initOnFlush(); + + $manager = $this->createEmailOwnerManagerMockBuilder() + ->setMethods(array('bindEmailAddress')) + ->getMock(); + + $this->uow->expects($this->never()) + ->method('getEntityChangeSet'); + + $manager->expects($this->never()) + ->method('bindEmailAddress'); + + $owner = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'); + + $result = ReflectionUtil::callProtectedMethod( + $manager, + 'processInsertionOrUpdateEntity', + array(null, null, $owner, $this->em, $this->uow) + ); + + $this->assertEquals(false, $result); + } + + public function testProcessInsertionOrUpdateEntityNoEmailRelatedChanges() + { + $this->initOnFlush(); + + $manager = $this->createEmailOwnerManagerMockBuilder() + ->setMethods(array('bindEmailAddress')) + ->getMock(); + + $this->uow->expects($this->once()) + ->method('getEntityChangeSet') + ->will($this->returnValue(array('SomeField' => array('val1', 'val2')))); + + $manager->expects($this->never()) + ->method('bindEmailAddress'); + + $owner = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'); + + $result = ReflectionUtil::callProtectedMethod( + $manager, + 'processInsertionOrUpdateEntity', + array('testEmailField', null, $owner, $this->em, $this->uow) + ); + + $this->assertEquals(false, $result); + } + + public function testProcessInsertionOrUpdateEntityEmailValueNotChanged() + { + $this->initOnFlush(); + + $manager = $this->createEmailOwnerManagerMockBuilder() + ->setMethods(array('bindEmailAddress')) + ->getMock(); + + $this->uow->expects($this->once()) + ->method('getEntityChangeSet') + ->will($this->returnValue(array('testEmailField' => array('val1', 'val1')))); + + $manager->expects($this->never()) + ->method('bindEmailAddress'); + + $owner = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'); + + $result = ReflectionUtil::callProtectedMethod( + $manager, + 'processInsertionOrUpdateEntity', + array('testEmailField', null, $owner, $this->em, $this->uow) + ); + + $this->assertEquals(false, $result); + } + + public function testProcessInsertionOrUpdateEntityEmailValueChanged() + { + $this->initOnFlush(); + + $manager = $this->createEmailOwnerManagerMockBuilder() + ->setMethods(array('bindEmailAddress')) + ->getMock(); + + $this->uow->expects($this->once()) + ->method('getEntityChangeSet') + ->will($this->returnValue(array('testEmailField' => array('val1', 'val2')))); + + $manager->expects($this->once()) + ->method('bindEmailAddress') + ->will($this->returnValue(true)); + + $owner = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'); + + $result = ReflectionUtil::callProtectedMethod( + $manager, + 'processInsertionOrUpdateEntity', + array('testEmailField', null, $owner, $this->em, $this->uow) + ); + + $this->assertEquals(true, $result); + } + + public function testCreateEmailAddress() + { + $owner = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'); + $addrManager = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Manager\EmailAddressManager') + ->disableOriginalConstructor() + ->getMock(); + $addr = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailAddress'); + + $addrManager->expects($this->once()) + ->method('newEmailAddress') + ->will($this->returnValue($addr)); + + $addr->expects($this->once()) + ->method('setEmail') + ->with($this->equalTo('test@example.com')) + ->will($this->returnValue($addr)); + $addr->expects($this->once()) + ->method('setOwner') + ->with($this->identicalTo($owner)) + ->will($this->returnValue($addr)); + + $manager = new EmailOwnerManager( + $this->getEmailOwnerProviderStorageMock(), + $addrManager + ); + + + $result = ReflectionUtil::callProtectedMethod( + $manager, + 'createEmailAddress', + array('test@example.com', $owner) + ); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockBuilder + */ + private function createEmailOwnerManagerMockBuilder() + { + return $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Manager\EmailOwnerManager') + ->setConstructorArgs( + array( + $this->getEmailOwnerProviderStorageMock(), + new EmailAddressManager('SomeNamespace', 'ProxyFor%s') + ) + ); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Provider/EmailOwnerProviderStorageTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Provider/EmailOwnerProviderStorageTest.php new file mode 100644 index 00000000000..97060d77a09 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Provider/EmailOwnerProviderStorageTest.php @@ -0,0 +1,24 @@ +getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $provider2 = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + + $storage = new EmailOwnerProviderStorage(); + $storage->addProvider($provider1); + $storage->addProvider($provider2); + + $result = $storage->getProviders(); + + $this->assertCount(2, $result); + $this->assertTrue($provider1 === $result[0]); + $this->assertTrue($provider2 === $result[1]); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Provider/EmailOwnerProviderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Provider/EmailOwnerProviderTest.php new file mode 100644 index 00000000000..c99497ae0b4 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Provider/EmailOwnerProviderTest.php @@ -0,0 +1,78 @@ +getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderStorage'); + $storage->expects($this->any()) + ->method('getProviders') + ->will($this->returnValue($providers)); + + return $storage; + } + + public function testFindEmailOwnerFirstProvider() + { + $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $result = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'); + $provider1 = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $provider1->expects($this->once()) + ->method('findEmailOwner') + ->with($this->identicalTo($em), $this->equalTo('test')) + ->will($this->returnValue($result)); + $provider2 = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $provider2->expects($this->never()) + ->method('findEmailOwner'); + + $provider = new EmailOwnerProvider($this->getEmailOwnerProviderStorageMock(array($provider1, $provider2))); + $this->assertEquals($result, $provider->findEmailOwner($em, 'test')); + } + + public function testFindEmailOwnerSecondProvider() + { + $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $result = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailOwnerInterface'); + $provider1 = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $provider1->expects($this->once()) + ->method('findEmailOwner') + ->with($this->identicalTo($em), $this->equalTo('test')) + ->will($this->returnValue(null)); + $provider2 = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $provider2->expects($this->once()) + ->method('findEmailOwner') + ->with($this->identicalTo($em), $this->equalTo('test')) + ->will($this->returnValue($result)); + + $provider = new EmailOwnerProvider($this->getEmailOwnerProviderStorageMock(array($provider1, $provider2))); + $this->assertEquals($result, $provider->findEmailOwner($em, 'test')); + } + + public function testFindEmailOwnerNotFound() + { + $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + $provider1 = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $provider1->expects($this->once()) + ->method('findEmailOwner') + ->with($this->identicalTo($em), $this->equalTo('test')) + ->will($this->returnValue(null)); + $provider2 = $this->getMock('Oro\Bundle\EmailBundle\Entity\Provider\EmailOwnerProviderInterface'); + $provider2->expects($this->once()) + ->method('findEmailOwner') + ->with($this->identicalTo($em), $this->equalTo('test')) + ->will($this->returnValue(null)); + + $provider = new EmailOwnerProvider($this->getEmailOwnerProviderStorageMock(array($provider1, $provider2))); + $this->assertNull($provider->findEmailOwner($em, 'test')); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/TestFixtures/EmailAddress.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/TestFixtures/EmailAddress.php new file mode 100644 index 00000000000..4ac61e62c78 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/TestFixtures/EmailAddress.php @@ -0,0 +1,14 @@ +created = $date; + $this->updated = $date; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/TestFixtures/TestEmailAddressProxy.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/TestFixtures/TestEmailAddressProxy.php new file mode 100644 index 00000000000..97637fd9f4b --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/TestFixtures/TestEmailAddressProxy.php @@ -0,0 +1,26 @@ +owner; + } + + public function setOwner(EmailOwnerInterface $owner = null) + { + $this->owner = $owner; + + return $this; + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Util/EmailUtilTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Util/EmailUtilTest.php new file mode 100644 index 00000000000..ba31e56c938 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Entity/Util/EmailUtilTest.php @@ -0,0 +1,26 @@ +assertEquals($pureEmailAddress, EmailUtil::extractPureEmailAddress($fullEmailAddress)); + } + + public static function extractPureEmailAddressProvider() + { + return array( + array('john@example.com', 'john@example.com'), + array('', 'john@example.com'), + array('John Smith ', 'john@example.com'), + array('"John Smith" ', 'john@example.com'), + ); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/ConfigureSubscriberTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/ConfigureSubscriberTest.php new file mode 100644 index 00000000000..70ac3d5f2bc --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/ConfigureSubscriberTest.php @@ -0,0 +1,164 @@ +cache = $this->getMockBuilder('Doctrine\Common\Cache\Cache') + ->disableOriginalConstructor()->getMock(); + + $this->subscriber = new ConfigSubscriber($this->cache, self::TEST_CACHE_KEY); + } + + public function tearDown() + { + unset($this->cache); + unset($this->subscriber); + } + + public function testGetSubscribedEvents() + { + $result = ConfigSubscriber::getSubscribedEvents(); + + foreach ($result as $eventProcessMethod) { + $this->assertTrue(is_callable(array($this->subscriber, $eventProcessMethod))); + } + } + + /** + * @dataProvider newEntityFieldsProvider + * @param ArrayCollection $fieldsCollection + * @param $shouldClearCache + */ + public function testNewEntityConfig(ArrayCollection $fieldsCollection, $shouldClearCache) + { + $cmMock = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') + ->disableOriginalConstructor()->getMock(); + + $cpMock = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider') + ->disableOriginalConstructor() + ->getMock(); + + $cpMock->expects($this->once())->method('filter') + ->will( + $this->returnCallback( + function ($callback) use ($fieldsCollection) { + return $fieldsCollection->filter($callback); + } + ) + ); + + $cmMock->expects($this->once())->method('getProvider') + ->with('email') + ->will($this->returnValue($cpMock)); + + $entityModel = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel') + ->disableOriginalConstructor() + ->getMock(); + + $event = new NewEntityConfigModelEvent($entityModel, $cmMock); + + $this->cache->expects($this->exactly((int)$shouldClearCache))->method('delete'); + + $this->subscriber->newEntityConfig($event); + } + + /** + * @return array + */ + public function newEntityFieldsProvider() + { + $config = $this->getMockForAbstractClass('Oro\Bundle\EntityConfigBundle\Config\ConfigInterface'); + $config->expects($this->at(0))->method('is')->with('available_in_template') + ->will($this->returnValue(true)); + $config->expects($this->at(1))->method('is')->with('available_in_template') + ->will($this->returnValue(false)); + $config->expects($this->at(2))->method('is')->with('available_in_template') + ->will($this->returnValue(false)); + + return array( + 'should clear cache' => array( + new ArrayCollection(array($config, $config)), + true + ), + 'cache should not be cleared' => array( + new ArrayCollection(array($config)), + false + ) + ); + } + + /** + * @dataProvider changeSetProvider + * @param $scope + * @param $change + * @param $shouldClearCache + */ + public function testPersistConfig($scope, $change, $shouldClearCache) + { + $cmMock = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') + ->disableOriginalConstructor()->getMock(); + $cmMock->expects($this->once())->method('calculateConfigChangeSet'); + $cmMock->expects($this->once())->method('getConfigChangeSet') + ->will($this->returnValue($change)); + + $configId = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface') + ->disableOriginalConstructor()->getMock(); + + $configId->expects($this->once())->method('getScope') + ->will($this->returnValue($scope)); + + $config = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigInterface') + ->disableOriginalConstructor()->getMock(); + + $config->expects($this->once())->method('getId') + ->will($this->returnValue($configId)); + + $event = new PersistConfigEvent($config, $cmMock); + $this->cache->expects($this->exactly((int)$shouldClearCache))->method('delete'); + + $this->subscriber->persistConfig($event); + } + + /** + * @return array + */ + public function changeSetProvider() + { + return array( + 'email config changed' => array( + 'scope' => 'email', + 'change' => array('available_in_template' => array()), + 'shouldClearCache' => true + ), + 'email config not changed' => array( + 'scope' => 'email', + 'change' => array(), + 'shouldClearCache' => false + ), + 'not email config' => array( + 'scope' => 'someConfigScope', + 'change' => array(), + 'shouldClearCache' => false + ) + ); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/EntitySubscriberTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/EntitySubscriberTest.php new file mode 100644 index 00000000000..df44628cdc8 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/EventListener/EntitySubscriberTest.php @@ -0,0 +1,48 @@ +emailOwnerManager = $this->getMockBuilder('Oro\Bundle\EmailBundle\Entity\Manager\EmailOwnerManager') + ->disableOriginalConstructor() + ->getMock(); + $this->subscriber = new EntitySubscriber($this->emailOwnerManager); + } + + public function testGetSubscribedEvents() + { + $this->assertEquals( + array( + //@codingStandardsIgnoreStart + Events::onFlush, + //@codingStandardsIgnoreEnd + ), + $this->subscriber->getSubscribedEvents() + ); + } + + public function testOnFlush() + { + $eventArgs = $this->getMockBuilder('Doctrine\ORM\Event\OnFlushEventArgs') + ->disableOriginalConstructor() + ->getMock(); + + $this->emailOwnerManager->expects($this->once()) + ->method('handleOnFlush') + ->with($this->identicalTo($eventArgs)); + + $this->subscriber->onFlush($eventArgs); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Fixtures/Entity/SomeEntity.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Fixtures/Entity/SomeEntity.php new file mode 100644 index 00000000000..c34f9f39a35 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Fixtures/Entity/SomeEntity.php @@ -0,0 +1,7 @@ +loader = $this->getMock('\Twig_Loader_String'); + + $this->securityPolicy = $this->getMockBuilder('\Twig_Sandbox_SecurityPolicy') + ->disableOriginalConstructor()->getMock(); + + $this->sandbox = $this->getMockBuilder('\Twig_Extension_Sandbox') + ->disableOriginalConstructor() + ->getMock(); + + $this->sandbox->expects($this->once())->method('getName') + ->will($this->returnValue('sandbox')); + $this->sandbox->expects($this->once())->method('getSecurityPolicy') + ->will($this->returnValue($this->securityPolicy)); + + $this->securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + + $token = $this->getMockForAbstractClass( + 'Symfony\Component\Security\Core\Authentication\Token\TokenInterface' + ); + $this->user = $this->getMockBuilder('Oro\Bundle\UserBundle\Entity\User') + ->disableOriginalConstructor()->getMock(); + $token->expects($this->any())->method('getUser') + ->will($this->returnValue($this->user)); + $this->securityContext->expects($this->any())->method('getToken') + ->will($this->returnValue($token)); + + + $this->configProvider = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider') + ->disableOriginalConstructor()->getMock(); + + $this->cache = $this->getMockBuilder('Doctrine\Common\Cache\Cache') + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * test configureSandbox method + */ + public function testConfigureSandboxCached() + { + $this->cache + ->expects($this->once()) + ->method('fetch') + ->with($this->cacheKey) + ->will($this->returnValue(serialize(array('somekey' => array())))); + + $this->getRendererInstance(); + } + + /** + * configureSanbox method with not cached scenario + */ + public function testConfigureSandboxNotCached() + { + $entityClass = 'Oro\Bundle\UserBundle\Entity\User'; + + $configIdMock = $this->getMockForAbstractClass('Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface'); + $configIdMock + ->expects($this->once())->method('getClassName') + ->will($this->returnValue($entityClass)); + + $configuredData = array( + $entityClass => array( + 'getsomecode' + ) + ); + + $this->cache + ->expects($this->once()) + ->method('fetch') + ->with($this->cacheKey) + ->will($this->returnValue(false)); + + $this->cache + ->expects($this->once()) + ->method('save') + ->with($this->cacheKey, serialize($configuredData)); + + $configurableEntities = array($configIdMock); + $this->configProvider + ->expects($this->once()) + ->method('getIds') + ->will($this->returnValue($configurableEntities)); + + $fieldsCollection = new ArrayCollection(); + + $this->configProvider->expects($this->once())->method('filter') + ->will( + $this->returnCallback( + function ($callback) use ($fieldsCollection) { + return $fieldsCollection->filter($callback); + } + ) + ); + + $field1Id = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface') + ->disableOriginalConstructor() + ->getMock(); + $field1Id->expects($this->once()) + ->method('getFieldName') + ->will($this->returnValue('someCode')); + + $field1 = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigInterface') + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $field2 = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigInterface') + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $field1->expects($this->once()) + ->method('is') + ->with('available_in_template') + ->will($this->returnValue(true)); + $field1->expects($this->once()) + ->method('getId') + ->will($this->returnValue($field1Id)); + + $field2->expects($this->once()) + ->method('is') + ->with('available_in_template') + ->will($this->returnValue(false)); + + $fieldsCollection->add($field1); + $fieldsCollection->add($field2); + + $this->getRendererInstance(); + } + + /** + * Compile message test + */ + public function testCompileMessage() + { + $this->cache + ->expects($this->once()) + ->method('fetch') + ->with($this->cacheKey) + ->will($this->returnValue(serialize(array('somekey' => array())))); + + $content = 'test content asfsdf {{ entity.name }}'; + $subject = 'subject'; + + $emailTemplate = $this->getMock('Oro\Bundle\EmailBundle\Entity\EmailTemplate'); + $emailTemplate->expects($this->once()) + ->method('getContent') + ->will($this->returnValue($content)); + $emailTemplate->expects($this->once()) + ->method('getType') + ->will($this->returnValue('txt')); + $emailTemplate->expects($this->once()) + ->method('getSubject') + ->will($this->returnValue($subject)); + + $entity = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + $templateParams = array( + 'entity' => $entity, + ); + + $renderer = $this->getRendererInstance(); + + $renderer->expects($this->at(0)) + ->method('render') + ->with( + '{% verbatim %}'.strip_tags($content).'{% endverbatim %}', + array_merge($templateParams, array('user' => $this->user)) + ); + $renderer->expects($this->at(1)) + ->method('render') + ->with( + '{% verbatim %}'.$subject.'{% endverbatim %}', + array_merge($templateParams, array('user' => $this->user)) + ); + + $result = $renderer->compileMessage($emailTemplate, $templateParams); + + $this->assertInternalType('array', $result); + $this->assertCount(2, $result); + } + + /** + * @return EmailRenderer + */ + public function getRendererInstance() + { + return $this->getMock( + 'Oro\Bundle\EmailBundle\Provider\EmailRenderer', + array('render'), + array( + $this->loader, + array(), + $this->configProvider, + $this->cache, + $this->cacheKey, + $this->securityContext, + $this->sandbox + ) + ); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/VariableProviderTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/VariableProviderTest.php new file mode 100644 index 00000000000..9f2a6ed98a9 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Provider/VariableProviderTest.php @@ -0,0 +1,169 @@ +securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + + $this->configProvider = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider') + ->disableOriginalConstructor()->getMock(); + $token = $this->getMockForAbstractClass( + 'Symfony\Component\Security\Core\Authentication\Token\TokenInterface' + ); + $this->user = $this->getMockBuilder('Oro\Bundle\UserBundle\Entity\User') + ->disableOriginalConstructor()->getMock(); + $token->expects($this->any())->method('getUser') + ->will($this->returnValue($this->user)); + $this->securityContext->expects($this->any())->method('getToken') + ->will($this->returnValue($token)); + + $this->provider = new VariablesProvider($this->securityContext, $this->configProvider); + } + + public function tearDown() + { + unset($this->securityContext); + unset($this->configProvider); + unset($this->user); + unset($this->provider); + } + + /** + * @dataProvider fieldsDataProvider + * @param $entityIsUser + */ + public function testGetTemplateVariables($entityIsUser) + { + $configId1Mock = $this->getMockForAbstractClass('Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface'); + $configId1Mock + ->expects($this->once())->method('getClassName') + ->will($this->returnValue(get_class($this->user))); + $configId2Mock = $this->getMockForAbstractClass('Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface'); + $configId2Mock + ->expects($this->once())->method('getClassName') + ->will($this->returnValue(self::TEST_ENTITY_NAME)); + $configId3Mock = $this->getMockForAbstractClass('Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface'); + $configId3Mock + ->expects($this->once())->method('getClassName') + ->will($this->returnValue(self::TEST_NOT_NEEDED_ENTITY_NAME)); + + $configurableEntities = array($configId1Mock, $configId2Mock, $configId3Mock); + + $this->configProvider->expects($this->once())->method('getIds') + ->will($this->returnValue($configurableEntities)); + + $field1Id = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface') + ->disableOriginalConstructor() + ->getMock(); + $field1Id->expects($this->any()) + ->method('getFieldName') + ->will($this->returnValue('someCode')); + + $field1 = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigInterface') + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $field2 = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigInterface') + ->disableOriginalConstructor() + ->getMockForAbstractClass(); + + $field1->expects($this->any()) + ->method('is') + ->with('available_in_template') + ->will($this->returnValue(true)); + $field1->expects($this->any()) + ->method('getId') + ->will($this->returnValue($field1Id)); + + $field2->expects($this->any()) + ->method('is') + ->with('available_in_template') + ->will($this->returnValue(false)); + + // fields for entity + $fieldsCollection = new ArrayCollection(); + $this->configProvider->expects($this->at(1))->method('filter')->will( + $this->returnCallback( + function ($callback) use ($fieldsCollection) { + return $fieldsCollection->filter($callback)->toArray(); + } + ) + ); + $fieldsCollection[] = $field1; + $fieldsCollection[] = $field2; + + if (!$entityIsUser) { + $field3Id = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface') + ->disableOriginalConstructor() + ->getMock(); + $field3Id->expects($this->any())->method('getFieldName')->will($this->returnValue('someAnotherCode')); + + $field3 = clone $field1; + $field3->expects($this->atLeastOnce())->method('is')->with('available_in_template') + ->will($this->returnValue(true)); + $field3->expects($this->atLeastOnce())->method('getId') + ->will($this->returnValue($field3Id)); + + $this->configProvider->expects($this->at(2))->method('filter')->will( + $this->returnCallback( + function ($callback) use ($fieldsCollection, $field3) { + $fieldsCollection[] = $field3; + return $fieldsCollection->filter($callback)->toArray(); + } + ) + ); + + $result = $this->provider->getTemplateVariables(self::TEST_ENTITY_NAME); + } else { + $result = $this->provider->getTemplateVariables(get_class($this->user)); + } + + $this->assertArrayHasKey('user', $result); + $this->assertArrayHasKey('entity', $result); + + $this->assertInternalType('array', $result['user']); + $this->assertInternalType('array', $result['entity']); + + if ($entityIsUser) { + $this->assertEquals($result['user'], $result['entity']); + } + } + + /** + * @return array + */ + public function fieldsDataProvider() + { + return array( + 'entity is not user' => array( + 'entityIsUser' => false + ), + 'entity is user' => array( + 'entityIsUser' => true + ) + ); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/ReflectionUtil.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/ReflectionUtil.php new file mode 100644 index 00000000000..3e3bd46748a --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/ReflectionUtil.php @@ -0,0 +1,48 @@ +getProperty('id'); + $prop->setAccessible(true); + + $prop->setValue($obj, $val); + } + + /** + * @param mixed $obj + * @param string $propName + * @return mixed + */ + public static function getProtectedProperty($obj, $propName) + { + $class = new \ReflectionClass($obj); + $prop = $class->getProperty($propName); + $prop->setAccessible(true); + + return $prop->getValue($obj); + } + + /** + * @param mixed $obj + * @param string $methodName + * @param array $args + * @return mixed + */ + public static function callProtectedMethod($obj, $methodName, array $args) + { + $class = new \ReflectionClass($obj); + $method = $class->getMethod($methodName); + $method->setAccessible(true); + + return $method->invokeArgs($obj, $args); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Validators/Constraints/VariableConstraintTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Validators/Constraints/VariableConstraintTest.php new file mode 100644 index 00000000000..74db14bff90 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Validators/Constraints/VariableConstraintTest.php @@ -0,0 +1,29 @@ +constraint = new VariablesConstraint(); + } + + public function tearDown() + { + unset($this->constraint); + } + + public function testConfiguration() + { + $this->assertEquals('oro_email.variables_validator', $this->constraint->validatedBy()); + $this->assertEquals(Constraint::CLASS_CONSTRAINT, $this->constraint->getTargets()); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Tests/Unit/Validators/VariablesValidatorTest.php b/src/Oro/Bundle/EmailBundle/Tests/Unit/Validators/VariablesValidatorTest.php new file mode 100644 index 00000000000..b00bc2082ed --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Tests/Unit/Validators/VariablesValidatorTest.php @@ -0,0 +1,119 @@ +twig = $this->getMockBuilder('\Twig_Environment') + ->disableOriginalConstructor()->getMock(); + $this->securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $token = $this->getMockForAbstractClass( + 'Symfony\Component\Security\Core\Authentication\Token\TokenInterface' + ); + $this->user = $this->getMockBuilder('Oro\Bundle\UserBundle\Entity\User') + ->disableOriginalConstructor()->getMock(); + $token->expects($this->any())->method('getUser') + ->will($this->returnValue($this->user)); + $this->securityContext->expects($this->any())->method('getToken') + ->will($this->returnValue($token)); + $this->context = $this->getMockForAbstractClass('Symfony\Component\Validator\ExecutionContextInterface'); + + $this->template = new EmailTemplate(); + $this->variablesConstraint = new VariablesConstraint(); + + $this->validator = new VariablesValidator($this->twig, $this->securityContext); + $this->validator->initialize($this->context); + } + + public function tearDown() + { + unset($this->twig); + unset($this->securityContext); + unset($this->user); + unset($this->template); + unset($this->validator); + unset($this->variablesConstraint); + unset($this->context); + } + + public function testValidateNotErrors() + { + $this->template->setContent(self::TEST_CONTENT) + ->setSubject(self::TEST_SUBJECT); + $this->template->setEntityName('Oro\Bundle\EmailBundle\Tests\Unit\Fixtures\Entity\SomeEntity'); + + $phpUnit = $this; + $user = $this->user; + $callback = function ($template, $params) use ($phpUnit, $user) { + $phpUnit->assertInternalType('string', $template); + + $phpUnit->assertArrayHasKey('entity', $params); + $phpUnit->assertArrayHasKey('user', $params); + + $phpUnit->assertInstanceOf( + 'Oro\Bundle\EmailBundle\Tests\Unit\Fixtures\Entity\SomeEntity', + $params['entity'] + ); + $phpUnit->assertInstanceOf(get_class($user), $params['user']); + }; + $this->twig->expects($this->at(0))->method('render')->with(self::TEST_SUBJECT) + ->will($this->returnCallback($callback)); + $this->twig->expects($this->at(1))->method('render')->with(self::TEST_CONTENT) + ->will($this->returnCallback($callback)); + + $this->context->expects($this->never())->method('addViolation'); + + $this->validator->validate($this->template, $this->variablesConstraint); + } + + public function testValidateErrors() + { + $trans = new EmailTemplateTranslation(); + $trans->setField('subject') + ->setContent(self::TEST_TRANS_SUBJECT); + $this->template->setContent(self::TEST_CONTENT) + ->setSubject(self::TEST_SUBJECT) + ->getTranslations()->add($trans); + + $this->twig->expects($this->at(0))->method('render')->with(self::TEST_SUBJECT); + $this->twig->expects($this->at(1))->method('render')->with(self::TEST_CONTENT); + $this->twig->expects($this->at(2))->method('render')->with(self::TEST_TRANS_SUBJECT) + ->will($this->throwException(new \Exception())); + + $this->context->expects($this->once())->method('addViolation')->with($this->variablesConstraint->message); + + $this->validator->validate($this->template, $this->variablesConstraint); + } +} diff --git a/src/Oro/Bundle/EmailBundle/Validator/Constraints/VariablesConstraint.php b/src/Oro/Bundle/EmailBundle/Validator/Constraints/VariablesConstraint.php new file mode 100644 index 00000000000..a9c5810e553 --- /dev/null +++ b/src/Oro/Bundle/EmailBundle/Validator/Constraints/VariablesConstraint.php @@ -0,0 +1,26 @@ +twig = $twig; + $this->securityContext = $securityContext; + } + + /** + * {@inheritdoc} + */ + public function validate($emailTemplate, Constraint $constraint) + { + /** @var EmailTemplate $emailTemplate */ + /** @var VariablesConstraint $constraint */ + + $fieldsToValidate = array( + 'subject' => $emailTemplate->getSubject(), + 'content' => $emailTemplate->getContent(), + ); + + foreach ($emailTemplate->getTranslations() as $trans) { + if (in_array($trans->getField(), array('subject', 'content'))) { + $fieldsToValidate[$trans->getLocale() . '.' . $trans->getField()] = $trans->getContent(); + } + } + + $relatedEntity = false; + if (class_exists($emailTemplate->getEntityName())) { + $className = $emailTemplate->getEntityName(); + $relatedEntity = new $className; + } + + $errors = array(); + foreach ($fieldsToValidate as $field => $value) { + try { + $this->twig->render( + $value, + array( + 'entity' => $relatedEntity, + 'user' => $this->getUser() + ) + ); + } catch (\Exception $e) { + $errors[$field] = true; + } + } + + if (!empty($errors)) { + $this->context->addViolation($constraint->message); + } + } + + /** + * Return current user + * + * @return UserInterface|bool + */ + private function getUser() + { + return $this->securityContext->getToken() && !is_string($this->securityContext->getToken()->getUser()) + ? $this->securityContext->getToken()->getUser() : false; + } +} diff --git a/src/Oro/Bundle/EmailBundle/composer.json b/src/Oro/Bundle/EmailBundle/composer.json index 72964bfbfff..e36aace22a8 100644 --- a/src/Oro/Bundle/EmailBundle/composer.json +++ b/src/Oro/Bundle/EmailBundle/composer.json @@ -7,7 +7,8 @@ "require": { "php": ">=5.3.3", "symfony/symfony": "2.1.*", - "a2lix/translation-form-bundle" : "1.*@dev" + "a2lix/translation-form-bundle" : "1.*@dev", + "oro/user-bundle": "dev-master" }, "autoload": { "psr-0": { "Oro\\Bundle\\EmailBundle": "" } diff --git a/src/Oro/Bundle/EntityBundle/Datagrid/EntityDatagrid.php b/src/Oro/Bundle/EntityBundle/Datagrid/EntityDatagrid.php index 88feb472072..89e33088a9b 100644 --- a/src/Oro/Bundle/EntityBundle/Datagrid/EntityDatagrid.php +++ b/src/Oro/Bundle/EntityBundle/Datagrid/EntityDatagrid.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\EntityBundle\Datagrid; -use Oro\Bundle\EntityConfigBundle\ConfigManager; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; use Oro\Bundle\GridBundle\Datagrid\DatagridManager; use Oro\Bundle\GridBundle\Field\FieldDescriptionCollection; diff --git a/src/Oro/Bundle/EntityBundle/Entity/AuditCommit.php b/src/Oro/Bundle/EntityBundle/Entity/AuditCommit.php deleted file mode 100644 index 64683a836a5..00000000000 --- a/src/Oro/Bundle/EntityBundle/Entity/AuditCommit.php +++ /dev/null @@ -1,160 +0,0 @@ -id; - } - - /** - * @param PersistentCollection|AuditDiff[] $diffs - * @return $this - */ - public function setDiffs($diffs) - { - $this->diffs = $diffs; - - return $this; - } - - /** - * @param AuditDiff $diff - * @return $this - */ - public function addDiff(AuditDiff $diff) - { - if (!$this->diffs->contains($diff)) { - $this->diffs->add($diff); - } - - return $this; - } - - /** - * @param AuditDiff $diff - * @return $this - */ - public function removeDiff(AuditDiff $diff) - { - if ($this->diffs->contains($diff)) { - $this->diffs->remove($diff); - } - - return $this; - } - - /** - * @return PersistentCollection|AuditDiff[] - */ - public function getDiffs() - { - return $this->diffs; - } - - /** - * @param \DateTime $logged - * @return $this - */ - public function setLogged($logged) - { - $this->logged = $logged; - - return $this; - } - - /** - * @return \DateTime - */ - public function getLogged() - { - return $this->logged; - } - - /** - * @param string $name - * @return $this - */ - public function setName($name) - { - $this->name = $name; - - return $this; - } - - /** - * @return string - */ - public function getName() - { - return $this->name; - } - - /** - * @param \Oro\Bundle\UserBundle\Entity\User $user - * @return $this - */ - public function setUser($user) - { - $this->user = $user; - - return $this; - } - - /** - * @return \Oro\Bundle\UserBundle\Entity\User - */ - public function getUser() - { - return $this->user; - } -} diff --git a/src/Oro/Bundle/EntityBundle/Entity/AuditDiff.php b/src/Oro/Bundle/EntityBundle/Entity/AuditDiff.php deleted file mode 100644 index 30a59a72485..00000000000 --- a/src/Oro/Bundle/EntityBundle/Entity/AuditDiff.php +++ /dev/null @@ -1,152 +0,0 @@ -id; - } - - /** - * @param string $action - * @return $this - */ - public function setAction($action) - { - $this->action = $action; - - return $this; - } - - /** - * @return string - */ - public function getAction() - { - return $this->action; - } - - /** - * @param AuditCommit $commit - * @return $this - */ - public function setCommit(AuditCommit $commit) - { - $this->commit = $commit; - - return $this; - } - - /** - * @return AuditCommit - */ - public function getCommit() - { - return $this->commit; - } - - /** - * @param string $diff - * @return $this - */ - public function setDiff($diff) - { - $this->diff = $diff; - - return $this; - } - - /** - * @return string - */ - public function getDiff() - { - return $this->diff; - } - - /** - * @param string $className - */ - public function setClassName($className) - { - $this->className = $className; - } - - /** - * @return string - */ - public function getClassName() - { - return $this->className; - } - - /** - * @param int $objectId - */ - public function setObjectId($objectId) - { - $this->objectId = $objectId; - } - - /** - * @return int - */ - public function getObjectId() - { - return $this->objectId; - } -} diff --git a/src/Oro/Bundle/EntityBundle/README.md b/src/Oro/Bundle/EntityBundle/README.md index 880d74f8e82..0c43ea57df3 100644 --- a/src/Oro/Bundle/EntityBundle/README.md +++ b/src/Oro/Bundle/EntityBundle/README.md @@ -1,2 +1,35 @@ OroEntityBundle ======================== + +Example for Resources/config/entity_extend.yml +TestClassExtend: + configs: + entity: + label: TestClassExtend + fields: + testStringField: + type: string + configs: + entity: + label: testStringField + options: + length: 200 + testIntegerField: + type: smallint + testHiddenField: + mode: hidden + type: string + testReadonlyField: + mode: readonly + type: string + +Oro\Bundle\UserBundle\Entity\User: + fields: + testField: + type: string + testHiddenField: + mode: hidden + type: string + testReadonlyField: + mode: readonly + type: string \ No newline at end of file diff --git a/src/Oro/Bundle/EntityBundle/Resources/config/entity_config.yml b/src/Oro/Bundle/EntityBundle/Resources/config/entity_config.yml index 770b9b61ed2..8d3499a9224 100644 --- a/src/Oro/Bundle/EntityBundle/Resources/config/entity_config.yml +++ b/src/Oro/Bundle/EntityBundle/Resources/config/entity_config.yml @@ -4,43 +4,59 @@ oro_entity_config: form: block_config: entity: + title: 'General' priority: 20 + items: icon: options: priority: 10 form: - type: text + type: oro_icon_select options: block: entity - required: true label: options: priority: 20 + constraints: + - NotBlank: ~ + - Length: + min: 2 + max: 50 grid: - type: string + type: html label: 'Label' filter_type: oro_grid_orm_string required: true sortable: true filterable: true show_filter: true + template: OroEntityConfigBundle:Config:propertyLabel.html.twig form: type: text options: block: entity required: true + plural_label: options: - priority: 30 + priority: 35 + constraints: + - Length: + min: 2 + max: 50 form: type: text options: block: entity required: true + description: options: priority: 50 + constraints: + - Length: + max: 500 grid: type: text label: 'Description' @@ -58,12 +74,17 @@ oro_entity_config: form: block_config: entity: - title: 'Field Information' + title: 'General' priority: 20 items: label: options: priority: 10 + constraints: + - NotBlank: ~ + - Length: + min: 2 + max: 50 grid: type: string label: 'Label' @@ -77,9 +98,13 @@ oro_entity_config: options: block: entity required: true + description: options: priority: 30 + constraints: + - Length: + max: 500 grid: type: text label: 'Description' @@ -96,17 +121,10 @@ oro_entity_config: datagrid: field: -# form: -# block_config: -# datagrid: -# title: 'Datagrid Config' -# subblocks: -# base: ~ items: is_searchable: options: default_value: false - is_bool: true grid: type: boolean label: 'Grid' diff --git a/src/Oro/Bundle/EntityBundle/Resources/config/services.yml b/src/Oro/Bundle/EntityBundle/Resources/config/services.yml index 137c6450342..f018aa64c66 100644 --- a/src/Oro/Bundle/EntityBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EntityBundle/Resources/config/services.yml @@ -1,10 +1,6 @@ parameters: services: - oro_entity.config.entity_config_provider: + oro_entity_config.link.provider_bag.method: tags: - - { name: oro_entity_config.provider, scope: entity } - - oro_entity.config.datagrid_config_provider: - tags: - - { name: oro_entity_config.provider, scope: datagrid } + - { name: oro_service_method, service: oro_entity_config.provider_bag, method: testAction } diff --git a/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php b/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php index cc630384793..71af4b5e93b 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php +++ b/src/Oro/Bundle/EntityConfigBundle/Audit/AuditManager.php @@ -2,16 +2,21 @@ namespace Oro\Bundle\EntityConfigBundle\Audit; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigLogDiff; +use Symfony\Component\Security\Core\SecurityContext; use Symfony\Component\Security\Core\User\UserInterface; -use Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy; +use Oro\Bundle\EntityConfigBundle\DependencyInjection\Utils\ServiceLink; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; + +use Oro\Bundle\EntityConfigBundle\Entity\ConfigLogDiff; use Oro\Bundle\EntityConfigBundle\Entity\ConfigLog; -use Oro\Bundle\EntityConfigBundle\ConfigManager; -use Oro\Bundle\EntityConfigBundle\Config\FieldConfigInterface; +use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface; use Oro\Bundle\EntityConfigBundle\Config\ConfigInterface; +/** + * Audit config data + */ class AuditManager { /** @@ -20,18 +25,18 @@ class AuditManager protected $configManager; /** - * @var ServiceProxy + * @var ServiceLink */ - protected $security; + protected $securityLink; /** * @param ConfigManager $configManager - * @param ServiceProxy $security + * @param ServiceLink $securityLink */ - public function __construct(ConfigManager $configManager, ServiceProxy $security) + public function __construct(ConfigManager $configManager, ServiceLink $securityLink) { $this->configManager = $configManager; - $this->security = $security; + $this->securityLink = $securityLink; } /** @@ -46,12 +51,12 @@ public function log() $log = new ConfigLog(); $log->setUser($this->getUser()); - foreach (array_merge($this->configManager->getUpdatedEntityConfig(), $this->configManager->getUpdatedFieldConfig()) as $config) { + foreach ($this->configManager->getUpdateConfig() as $config) { $this->logConfig($config, $log); } if ($log->getDiffs()->count()) { - $this->configManager->em()->persist($log); + $this->configManager->getEntityManager()->persist($log); } } @@ -63,12 +68,9 @@ protected function logConfig(ConfigInterface $config, ConfigLog $log) { $changes = $this->configManager->getConfigChangeSet($config); - $configContainer = $this->configManager->getProvider($config->getScope())->getConfigContainer(); - if ($config instanceof FieldConfigInterface) { - $internalValues = $configContainer->getFieldInternalValues(); - } else { - $internalValues = $configContainer->getEntityInternalValues(); - } + $configId = $config->getId(); + $configContainer = $this->configManager->getProvider($config->getId()->getScope())->getPropertyConfig(); + $internalValues = $configContainer->getInternalValues($configId); $changes = array_diff_key($changes, $internalValues); @@ -77,12 +79,12 @@ protected function logConfig(ConfigInterface $config, ConfigLog $log) } $diff = new ConfigLogDiff(); - $diff->setScope($config->getScope()); + $diff->setScope($configId->getScope()); $diff->setDiff($changes); - $diff->setClassName($config->getClassName()); + $diff->setClassName($configId->getClassName()); - if ($config instanceof FieldConfigInterface) { - $diff->setFieldName($config->getCode()); + if ($configId instanceof FieldConfigIdInterface) { + $diff->setFieldName($configId->getFieldName()); } $log->addDiff($diff); @@ -93,10 +95,12 @@ protected function logConfig(ConfigInterface $config, ConfigLog $log) */ protected function getUser() { - if (!$this->security->getService()->getToken() || !$this->security->getService()->getToken()->getUser()) { + /** @var SecurityContext $security */ + $security = $this->securityLink->getService(); + if (!$security->getToken() || !$security->getToken()->getUser()) { return false; } - return $this->security->getService()->getToken()->getUser(); + return $security->getToken()->getUser(); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php b/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php deleted file mode 100644 index 3a45b4ea511..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Cache/CacheInterface.php +++ /dev/null @@ -1,26 +0,0 @@ -dir = rtrim($dir, '\\/'); - } - - /** - * @param $className - * @param $scope - * @return EntityConfig - */ - public function loadConfigFromCache($className, $scope) - { - $path = $this->dir . '/' . strtr($className, '\\', '-') . '.' . $scope . '.cache.php'; - if (!file_exists($path)) { - return null; - } - - return include $path; - } - - /** - * @param EntityConfig $config - */ - public function putConfigInCache(EntityConfig $config) - { - $path = $this->dir . '/' . strtr($config->getClassName(), '\\', '-') . '.' . $config->getScope() . '.cache.php'; - file_put_contents($path, 'dir . '/' . strtr($className, '\\', '-') . '.' . $scope . '.cache.php'; - if (file_exists($path)) { - unlink($path); - } - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Command/BaseCommand.php b/src/Oro/Bundle/EntityConfigBundle/Command/BaseCommand.php index 9f66b761a92..94be8a8a6f3 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Command/BaseCommand.php +++ b/src/Oro/Bundle/EntityConfigBundle/Command/BaseCommand.php @@ -4,7 +4,7 @@ use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; -use Oro\Bundle\EntityConfigBundle\ConfigManager; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; abstract class BaseCommand extends ContainerAwareCommand { diff --git a/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php b/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php index 7a52c5f36e7..521f4b8684c 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php +++ b/src/Oro/Bundle/EntityConfigBundle/Command/UpdateCommand.php @@ -2,6 +2,8 @@ namespace Oro\Bundle\EntityConfigBundle\Command; +use Doctrine\ORM\Mapping\ClassMetadataInfo; + use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -27,8 +29,25 @@ public function execute(InputInterface $input, OutputInterface $output) { $output->writeln($this->getDescription()); - foreach ($this->getConfigManager()->em()->getMetadataFactory()->getAllMetadata() as $doctrineMetadata) { - $this->getConfigManager()->initConfigByDoctrineMetadata($doctrineMetadata); + /** @var ClassMetadataInfo[] $doctrineAllMetadata */ + $doctrineAllMetadata = $this->getConfigManager()->getEntityManager()->getMetadataFactory()->getAllMetadata(); + foreach ($doctrineAllMetadata as $doctrineMetadata) { + if (($metadata = $this->getConfigManager()->getEntityMetadata($doctrineMetadata->getName())) + && $metadata->name == $doctrineMetadata->getName() + && $metadata->configurable + ) { + $this->getConfigManager()->createConfigEntityModel($doctrineMetadata->getName()); + + foreach ($doctrineMetadata->getFieldNames() as $fieldName) { + $type = $doctrineMetadata->getTypeOfField($fieldName); + $this->getConfigManager()->createConfigFieldModel($doctrineMetadata->getName(), $fieldName, $type); + } + + foreach ($doctrineMetadata->getAssociationNames() as $fieldName) { + $type = $doctrineMetadata->isSingleValuedAssociation($fieldName) ? 'ref-one' : 'ref-many'; + $this->getConfigManager()->createConfigFieldModel($doctrineMetadata->getName(), $fieldName, $type); + } + } } $this->getConfigManager()->flush(); diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/AbstractConfig.php b/src/Oro/Bundle/EntityConfigBundle/Config/AbstractConfig.php deleted file mode 100644 index abd51c44fe5..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Config/AbstractConfig.php +++ /dev/null @@ -1,93 +0,0 @@ -values[$code])) { - return $this->values[$code]; - } elseif ($strict) { - throw new RuntimeException(sprintf( - "Config '%s' for class '%s' in scope '%s' is not found ", - $code, $this->getClassName(), $this->getScope() - )); - } - - return null; - } - - /** - * @param $code - * @param $value - * @return string - */ - public function set($code, $value) - { - $this->values[$code] = $value; - - return $this; - } - - /** - * @param $code - * @return bool - */ - public function has($code) - { - return isset($this->values[$code]); - } - - /** - * @param $code - * @return bool - */ - public function is($code) - { - return (bool) $this->get($code); - } - - /** - * @param array $exclude - * @param array $include - * @return array - */ - public function getValues(array $exclude = array(), array $include = array()) - { - switch (true) { - case count($exclude): - return array_diff_key($this->values, array_reverse($exclude)); - break; - case count($include): - return array_intersect_key($this->values, array_reverse($exclude)); - break; - default: - return $this->values; - } - } - - /** - * @param array $values - * @return $this - */ - public function setValues($values) - { - $this->values = $values; - - return $this; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Config.php b/src/Oro/Bundle/EntityConfigBundle/Config/Config.php new file mode 100644 index 00000000000..d905edca6aa --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Config.php @@ -0,0 +1,137 @@ +id = $id; + } + + /** + * @return ConfigIdInterface + */ + public function getId() + { + return $this->id; + } + + /** + * @param string $code + * @param bool $strict + * @throws RuntimeException + * @return mixed|null + */ + public function get($code, $strict = false) + { + if (isset($this->values[$code])) { + return $this->values[$code]; + } + + if ($strict) { + throw new RuntimeException(sprintf('Value "%s" for %s', $code, $this->getId()->toString())); + } + + return null; + } + + /** + * @param string $code + * @param mixed $value + * @return $this + */ + public function set($code, $value) + { + $this->values[$code] = $value; + + return $this; + } + + /** + * @param string $code + * @return bool + */ + public function has($code) + { + return isset($this->values[$code]); + } + + /** + * @param string $code + * @return bool + */ + public function is($code) + { + return (bool) $this->get($code); + } + + /** + * @param callable $filter + * @return array + */ + public function all(\Closure $filter = null) + { + return $filter ? array_filter($this->values, $filter) : $this->values; + } + + /** + * @param array $values + * @return $this + */ + public function setValues($values) + { + $this->values = $values; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize(array($this->id, $this->values)); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list($this->id, $this->values) = unserialize($serialized); + } + + /** + * Clone Config + */ + public function __clone() + { + $this->id = clone $this->id; + $this->values = array_map( + function ($value) { + return is_object($value) ? clone $value : $value; + }, + $this->values + ); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigCache.php b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigCache.php new file mode 100644 index 00000000000..a7db3419c66 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigCache.php @@ -0,0 +1,61 @@ +cache = $cache; + } + + /** + * @param ConfigIdInterface $configId + * @return bool|ConfigInterface + */ + public function loadConfigFromCache(ConfigIdInterface $configId) + { + return unserialize($this->cache->fetch($configId->toString())); + } + + /** + * @param ConfigInterface $config + * @return bool + */ + public function putConfigInCache(ConfigInterface $config) + { + return $this->cache->save($config->getId()->toString(), serialize($config)); + } + + /** + * @param ConfigIdInterface $configId + * @return bool + */ + public function removeConfigFromCache(ConfigIdInterface $configId) + { + return $this->cache->delete($configId->toString()); + } + + /** + * @return bool + */ + public function removeAll() + { + return $this->cache->deleteAll(); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php index 040b2a98972..20050b7593f 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php +++ b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigInterface.php @@ -2,50 +2,45 @@ namespace Oro\Bundle\EntityConfigBundle\Config; +use Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface; + interface ConfigInterface extends \Serializable { /** - * @return string - */ - public function getClassName(); - - /** - * @return string + * @return ConfigIdInterface */ - public function getScope(); + public function getId(); /** - * @param $code - * @param bool $strict - * @return string + * @param string $code + * @param bool $strict + * @return mixed */ public function get($code, $strict = false); /** - * @param $code - * @param $value - * @return string + * @param string $code + * @param mixed $value */ public function set($code, $value); /** - * @param $code + * @param string $code * @return bool */ public function has($code); /** - * @param $code + * @param string $code * @return bool */ public function is($code); /** - * @param array $exclude - * @param array $include + * @param callable $filter * @return array */ - public function getValues(array $exclude = array(), array $include = array()); + public function all(\Closure $filter = null); /** * @param $values diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigManager.php b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigManager.php new file mode 100644 index 00000000000..55c3fb81348 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigManager.php @@ -0,0 +1,549 @@ +metadataFactory = $metadataFactory; + $this->emLink = $emLink; + $this->eventDispatcher = $eventDispatcher; + + $this->providerBag = $providerBagLink; + $this->localCache = new ArrayCollection; + $this->persistConfigs = new \SplObjectStorage(); + $this->originalConfigs = new ArrayCollection; + $this->configChangeSets = new ArrayCollection; + + $this->modelManager = new ConfigModelManager($emLink); + $this->auditManager = new AuditManager($this, $securityLink); + } + + /** + * @param ConfigCache $cache + */ + public function setCache(ConfigCache $cache) + { + $this->cache = $cache; + } + + /** + * @return EntityManager + */ + public function getEntityManager() + { + return $this->modelManager->getEntityManager(); + } + + /** + * @return ConfigProviderBag + */ + public function getProviderBag() + { + return $this->providerBag->getService(); + } + + /** + * @return ConfigProvider[]|ArrayCollection + */ + public function getProviders() + { + return $this->getProviderBag()->getProviders(); + } + + /** + * @param $scope + * @return ConfigProvider + */ + public function getProvider($scope) + { + return $this->getProviderBag()->getProvider($scope); + } + + /** + * @return EventDispatcher + */ + public function getEventDispatcher() + { + return $this->eventDispatcher; + } + + /** + * @param $className + * @return EntityMetadata|null + */ + public function getEntityMetadata($className) + { + return class_exists($className) ? $this->metadataFactory->getMetadataForClass($className) : null; + } + + /** + * @param $className + * @param $fieldName + * @return null|FieldMetadata + */ + public function getFieldMetadata($className, $fieldName) + { + $metadata = $this->getEntityMetadata($className); + if ($metadata && isset ($metadata->propertyMetadata[$fieldName])) { + return $metadata->propertyMetadata[$fieldName]; + } + + return null; + } + + /** + * @return bool + */ + public function checkDatabase() + { + $tables = $this->getEntityManager()->getConnection()->getSchemaManager()->listTableNames(); + $table = $this->getEntityManager()->getClassMetadata(EntityConfigModel::ENTITY_NAME)->getTableName(); + + return in_array($table, $tables); + } + + /** + * @param string $className + * @param string $fieldName + * @return bool + */ + public function isConfigurable($className, $fieldName = null) + { + return (bool)$this->modelManager->findModel($className, $fieldName); + } + + /** + * @param $scope + * @param $className + * @return array + */ + public function getIds($scope, $className = null) + { + $entityModels = $this->modelManager->getModels($className); + + return array_map( + function (AbstractConfigModel $model) use ($scope) { + if ($model instanceof FieldConfigModel) { + return new FieldConfigId( + $model->getEntity()->getClassName(), + $scope, + $model->getFieldName(), + $model->getType() + ); + } else { + return new EntityConfigId($model->getClassName(), $scope); + } + }, + $entityModels + ); + } + + /** + * @param ConfigIdInterface $configId + * @return bool + */ + public function hasConfig(ConfigIdInterface $configId) + { + if ($this->localCache->containsKey($configId->toString())) { + return true; + } + + if (null !== $this->cache + && $config = $this->cache->loadConfigFromCache($configId) + ) { + return true; + } + + return (bool)$this->modelManager->getModelByConfigId($configId); + } + + /** + * @param ConfigIdInterface $configId + * @throws RuntimeException + * @throws LogicException + * @return ConfigInterface + */ + public function getConfig(ConfigIdInterface $configId) + { + if ($this->localCache->containsKey($configId->toString())) { + return $this->localCache->get($configId->toString()); + } + + if (!$this->modelManager->checkDatabase()) { + throw new LogicException( + 'Database is not synced, if you use ConfigManager, when a db schema may be hasn\'t synced.' + . ' check it by ConfigManager::modelManager::checkDatabase' + ); + } + + if (!$this->isConfigurable($configId->getClassName())) { + throw new RuntimeException(sprintf('Entity "%s" is not configurable', $configId->getClassName())); + } + + $resultConfig = null !== $this->cache + ? $this->cache->loadConfigFromCache($configId) + : null; + + if (!$resultConfig) { + $model = $this->modelManager->getModelByConfigId($configId); + + $config = new Config($configId); + $config->setValues($model->toArray($configId->getScope())); + + if (null !== $this->cache) { + $this->cache->putConfigInCache($config); + } + + $resultConfig = $config; + } + + //local cache + $this->localCache->set($resultConfig->getId()->toString(), $resultConfig); + + //for calculate change set + $this->originalConfigs->set($resultConfig->getId()->toString(), clone $resultConfig); + + return $resultConfig; + } + + + /** + * @param ConfigIdInterface $configId + */ + public function clearCache(ConfigIdInterface $configId) + { + if ($this->cache) { + $this->cache->removeConfigFromCache($configId); + } + } + + /** + * Remove All cache + */ + public function clearCacheAll() + { + if ($this->cache) { + $this->cache->removeAll(); + } + } + + /** + * @param ConfigInterface $config + */ + public function persist(ConfigInterface $config) + { + $this->persistConfigs->attach($config); + } + + /** + * @param ConfigInterface $config + * @return ConfigInterface + */ + public function merge(ConfigInterface $config) + { + $config = $this->doMerge($config); + $this->persistConfigs->attach($config); + + return $config; + } + + public function flush() + { + $models = array(); + + foreach ($this->persistConfigs as $config) { + $this->calculateConfigChangeSet($config); + + $this->eventDispatcher->dispatch(Events::PRE_PERSIST_CONFIG, new PersistConfigEvent($config, $this)); + + $models[] = $model = $this->modelManager->getModelByConfigId($config->getId()); + + //TODO::refactoring + $serializableValues = $this->getProvider($config->getId()->getScope()) + ->getPropertyConfig() + ->getSerializableValues($config->getId()); + $model->fromArray($config->getId()->getScope(), $config->all(), $serializableValues); + + if ($this->cache) { + $this->cache->removeConfigFromCache($config->getId()); + } + } + + $this->auditManager->log(); + + foreach ($models as $model) { + $this->getEntityManager()->persist($model); + } + + $this->getEntityManager()->flush(); + + $this->persistConfigs = new \SplObjectStorage(); + $this->configChangeSets = new ArrayCollection; + } + + + /** + * @param ConfigInterface $config + * @SuppressWarnings(PHPMD) + */ + public function calculateConfigChangeSet(ConfigInterface $config) + { + $originConfigValue = array(); + if ($this->originalConfigs->containsKey($config->getId()->toString())) { + $originConfig = $this->originalConfigs->get($config->getId()->toString()); + $originConfigValue = $originConfig->all(); + } + + foreach ($config->all() as $key => $value) { + if (!isset($originConfigValue[$key])) { + $originConfigValue[$key] = null; + } + } + + $diffNew = array_udiff_assoc( + $config->all(), + $originConfigValue, + function ($a, $b) { + return ($a == $b) ? 0 : 1; + } + ); + + $diffOld = array_udiff_assoc( + $originConfigValue, + $config->all(), + function ($a, $b) { + return ($a == $b) ? 0 : 1; + } + ); + + $diff = array(); + foreach ($diffNew as $key => $value) { + $oldValue = isset($diffOld[$key]) ? $diffOld[$key] : null; + $diff[$key] = array($oldValue, $value); + } + + + if (!$this->configChangeSets->containsKey($config->getId()->toString())) { + $this->configChangeSets->set($config->getId()->toString(), array()); + } + + if (count($diff)) { + $changeSet = array_merge($this->configChangeSets->get($config->getId()->toString()), $diff); + $this->configChangeSets->set($config->getId()->toString(), $changeSet); + } + } + + /** + * @param callable $filter + * @return ConfigInterface[]|ArrayCollection + */ + public function getUpdateConfig(\Closure $filter = null) + { + $result = iterator_to_array($this->persistConfigs, false); + + return $filter ? array_filter($result, $filter) : $result; + } + + /** + * @param ConfigInterface $config + * @return array + */ + public function getConfigChangeSet(ConfigInterface $config) + { + return $this->configChangeSets->containsKey($config->getId()->toString()) + ? $this->configChangeSets->get($config->getId()->toString()) + : array(); + } + + /** + * TODO:: check class name for custom entity + * @param string $className + * @param string $mode + * @return EntityConfigModel + */ + public function createConfigEntityModel($className, $mode = ConfigModelManager::MODE_DEFAULT) + { + if (!$entityModel = $this->modelManager->findModel($className)) { + $entityModel = $this->modelManager->createEntityModel($className, $mode); + + foreach ($this->getProviders() as $provider) { + + $metadata = $this->getEntityMetadata($className); + $defaultValues = array(); + if ($metadata && isset($metadata->defaultValues[$provider->getScope()])) { + $defaultValues = $metadata->defaultValues[$provider->getScope()]; + } + + $entityId = new EntityConfigId($className, $provider->getScope()); + $config = $provider->createConfig($entityId, $defaultValues); + + $this->localCache->set($config->getId()->toString(), $config); + } + + $this->eventDispatcher->dispatch( + Events::NEW_ENTITY_CONFIG_MODEL, + new NewEntityConfigModelEvent($entityModel, $this) + ); + } + + return $entityModel; + } + + /** + * @param string $className + * @param string $fieldName + * @param string $fieldType + * @param string $mode + * @return FieldConfigModel + */ + public function createConfigFieldModel($className, $fieldName, $fieldType, $mode = ConfigModelManager::MODE_DEFAULT) + { + if (!$fieldModel = $this->modelManager->findModel($className, $fieldName)) { + $fieldModel = $this->modelManager->createFieldModel($className, $fieldName, $fieldType, $mode); + + foreach ($this->getProviders() as $provider) { + $defaultValues = array(); + $metadata = $this->getFieldMetadata($className, $fieldName); + if ($metadata && isset($metadata->defaultValues[$provider->getScope()])) { + $defaultValues = $metadata->defaultValues[$provider->getScope()]; + } + + $fieldId = new FieldConfigId($className, $provider->getScope(), $fieldName, $fieldType); + $config = $provider->createConfig($fieldId, $defaultValues); + + $this->localCache->set($config->getId()->toString(), $config); + } + + $this->eventDispatcher->dispatch( + Events::NEW_FIELD_CONFIG_MODEL, + new NewFieldConfigModelEvent($fieldModel, $this) + ); + } + + return $fieldModel; + } + + /** + * @param ConfigInterface $config + * @return ConfigInterface + */ + private function doMerge(ConfigInterface $config) + { + foreach ($this->persistConfigs as $persistConfig) { + if ($config->getId()->toString() == $persistConfig->getId()->toString()) { + $config = array_merge($persistConfig->all(), $config->all()); + + break; + } + } + + return $config; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/ConfigModelManager.php b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigModelManager.php new file mode 100644 index 00000000000..74d5ade6fdc --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/ConfigModelManager.php @@ -0,0 +1,195 @@ +localCache = new ArrayCollection; + $this->proxyEm = $proxyEm; + } + + /** + * @return EntityManager + */ + public function getEntityManager() + { + return $this->proxyEm->getService(); + } + + + /** + * @return bool + */ + public function checkDatabase() + { + $tables = $this->getEntityManager()->getConnection()->getSchemaManager()->listTableNames(); + $table = $this->getEntityManager()->getClassMetadata(EntityConfigModel::ENTITY_NAME)->getTableName(); + + return in_array($table, $tables); + } + + /** + * @param $className + * @param null $fieldName + * @return null|AbstractConfigModel + */ + public function findModel($className, $fieldName = null) + { + $cacheKey = $className . $fieldName; + + if ($this->localCache->containsKey($cacheKey)) { + return $this->localCache->get($cacheKey); + } + + $entityConfigModelRepo = $this->getEntityManager()->getRepository(EntityConfigModel::ENTITY_NAME); + + $entity = $entityConfigModelRepo->findOneBy(array('className' => $className)); + + if ($fieldName) { + $fieldConfigModelRepo = $this->getEntityManager()->getRepository(FieldConfigModel::ENTITY_NAME); + + $result = $fieldConfigModelRepo->findOneBy( + array( + 'entity' => $entity, + 'fieldName' => $fieldName + ) + ); + } else { + $result = $entity; + } + + if ($result) { + $this->localCache->set($cacheKey, $result); + } + + return $result; + } + + /** + * @param $className + * @param null $fieldName + * @return null|AbstractConfigModel + * @throws RuntimeException + * @throws RuntimeException + */ + public function getModel($className, $fieldName = null) + { + if (!$model = $this->findModel($className, $fieldName)) { + $message = $fieldName + ? sprintf('FieldConfigModel "%s","%s" is not found ', $className, $fieldName) + : sprintf('EntityConfigModel "%s" is not found ', $className); + + throw new RuntimeException($message); + } + + return $model; + } + + /** + * @param ConfigIdInterface $configId + * @return AbstractConfigModel + */ + public function getModelByConfigId(ConfigIdInterface $configId) + { + $fieldName = $configId instanceof FieldConfigId ? $configId->getFieldName() : null; + + return $this->getModel($configId->getClassName(), $fieldName); + } + + /** + * @param null $className + * @return AbstractConfigModel[] + */ + public function getModels($className = null) + { + if ($className) { + return $this->getModel($className)->getFields()->toArray(); + } else { + $entityConfigModelRepo = $this->getEntityManager()->getRepository(EntityConfigModel::ENTITY_NAME); + + return (array) $entityConfigModelRepo->findAll(); + } + } + + /** + * @param string $className + * @param string $mode + * @throws \InvalidArgumentException + * @return EntityConfigModel + */ + public function createEntityModel($className, $mode = self::MODE_DEFAULT) + { + if (!in_array($mode, array(self::MODE_DEFAULT, self::MODE_READONLY))) { + throw new \InvalidArgumentException( + sprintf('EntityConfigModel give invalid parameter "mode" : "%s"', $mode) + ); + } + + $entityModel = new EntityConfigModel($className); + $entityModel->setMode($mode); + + $this->localCache->set($className, $entityModel); + + return $entityModel; + } + + /** + * @param string $className + * @param string $fieldName + * @param string $fieldType + * @param string $mode + * @throws \InvalidArgumentException + * @return FieldConfigModel + */ + public function createFieldModel($className, $fieldName, $fieldType, $mode = self::MODE_DEFAULT) + { + if (!in_array($mode, array(self::MODE_DEFAULT, self::MODE_HIDDEN, self::MODE_READONLY))) { + throw new \InvalidArgumentException( + sprintf('FieldConfigModel give invalid parameter "mode" : "%s"', $mode) + ); + } + + /** @var EntityConfigModel $entityModel */ + $entityModel = $this->getModel($className); + + $fieldModel = new FieldConfigModel($fieldName, $fieldType); + $fieldModel->setMode($mode); + $entityModel->addField($fieldModel); + + $this->localCache->set($className . $fieldName, $fieldModel); + + return $fieldModel; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfig.php b/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfig.php deleted file mode 100644 index 572a4f55ff5..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfig.php +++ /dev/null @@ -1,149 +0,0 @@ -fields = new ArrayCollection(); - $this->className = $className; - $this->scope = $scope; - } - - /** - * @param string $name - * @return $this - */ - public function setClassName($name) - { - $this->className = $name; - - return $this; - } - - /** - * @return string - */ - public function getClassName() - { - return $this->className; - } - - /** - * @return string|void - */ - public function getScope() - { - return $this->scope; - } - - /** - * @param FieldConfig[] $fields - * @return $this - */ - public function setFields($fields) - { - foreach ($fields as $field) { - $this->addField($field); - } - - return $this; - } - - /** - * @param FieldConfig $field - */ - public function addField(FieldConfig $field) - { - $this->fields[$field->getCode()] = $field; - } - - /** - * @param $name - * @return FieldConfig - */ - public function getField($name) - { - return $this->fields[$name]; - } - - /** - * @param $name - * @return bool - */ - public function hasField($name) - { - return isset($this->fields[$name]); - } - - /** - * @param callable $filter - * @return FieldConfig[]|ArrayCollection - */ - public function getFields(\Closure $filter = null) - { - return $filter ? $this->fields->filter($filter) : $this->fields; - } - - /** - * {@inheritdoc} - */ - public function serialize() - { - return serialize(array( - $this->className, - $this->scope, - $this->fields, - $this->values, - )); - } - - /** - * {@inheritdoc} - */ - public function unserialize($serialized) - { - list( - $this->className, - $this->scope, - $this->fields, - $this->values, - ) = unserialize($serialized); - } - - /** - * Clone Config - */ - public function __clone() - { - $this->values = array_map(function ($value) { - if (is_object($value)) { - return clone $value; - } else { - return $value; - } - }, $this->values); - - $this->fields = $this->fields->map(function ($field) { - return clone $field; - }, $this->fields); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php deleted file mode 100644 index 11d24164771..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Config/EntityConfigInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -className = $className; - $this->code = $code; - $this->type = $type; - $this->scope = $scope; - } - - /** - * @param string $className - * @return $this - */ - public function setClassName($className) - { - $this->className = $className; - - return $this; - } - - /** - * @return string - */ - public function getClassName() - { - return $this->className; - } - - /** - * @param string $code - * @return $this - */ - public function setCode($code) - { - $this->code = $code; - - return $this; - } - - /** - * @return string - */ - public function getCode() - { - return $this->code; - } - - /** - * @return string - */ - public function getScope() - { - return $this->scope; - } - - /** - * @param string $type - * @return $this - */ - public function setType($type) - { - $this->type = $type; - - return $this; - } - - /** - * @return string - */ - public function getType() - { - return $this->type; - } - - /** - * {@inheritdoc} - */ - public function serialize() - { - return serialize(array( - $this->code, - $this->className, - $this->type, - $this->scope, - $this->values, - )); - } - - /** - * {@inheritdoc} - */ - public function unserialize($serialized) - { - list( - $this->code, - $this->className, - $this->type, - $this->scope, - $this->values, - ) = unserialize($serialized); - } - - /** - * Clone Config - */ - public function __clone() - { - $this->values = array_map(function ($value) { - if (is_object($value)) { - return clone $value; - } else { - return $value; - } - }, $this->values); - - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php deleted file mode 100644 index e8e856d8e31..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Config/FieldConfigInterface.php +++ /dev/null @@ -1,16 +0,0 @@ -className = $className; + $this->scope = $scope; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->className; + } + + + /** + * @return string + */ + public function getScope() + { + return $this->scope; + } + + /** + * @return string + */ + public function toString() + { + return sprintf('entity_%s_%s', $this->scope, strtr($this->className, '\\', '-')); + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize( + array( + $this->className, + $this->scope, + ) + ); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list( + $this->className, + $this->scope, + ) = unserialize($serialized); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigIdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigIdInterface.php new file mode 100644 index 00000000000..81bd383c2cf --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/EntityConfigIdInterface.php @@ -0,0 +1,7 @@ +className = $className; + $this->scope = $scope; + $this->fieldName = $fieldName; + $this->fieldType = $fieldType; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->className; + } + + + /** + * @return string + */ + public function getScope() + { + return $this->scope; + } + + /** + * @return string + */ + public function getFieldName() + { + return $this->fieldName; + } + + /** + * @return string + */ + public function getFieldType() + { + return $this->fieldType; + } + + /** + * @param string $fieldType + * @return $this + */ + public function setFieldType($fieldType) + { + $this->fieldType = $fieldType; + + return $this; + } + + /** + * @return string + */ + public function toString() + { + return sprintf('field_%s_%s_%s', $this->scope, strtr($this->className, '\\', '-'), $this->fieldName); + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + return serialize( + array( + $this->className, + $this->scope, + $this->fieldName, + $this->fieldType, + ) + ); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + list( + $this->className, + $this->scope, + $this->fieldName, + $this->fieldType, + ) = unserialize($serialized); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigIdInterface.php b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigIdInterface.php new file mode 100644 index 00000000000..b53e64dba8f --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Config/Id/FieldConfigIdInterface.php @@ -0,0 +1,16 @@ +persistConfigs = new \SplQueue(); - $this->metadataFactory = $metadataFactory; - $this->proxyEm = $proxyEm; - $this->eventDispatcher = $eventDispatcher; - - $this->auditManager = new AuditManager($this, $security); - } - - /** - * @param CacheInterface $cache - */ - public function setCache(CacheInterface $cache) - { - $this->configCache = $cache; - } - - /** - * @return EntityManager - */ - public function em() - { - return $this->proxyEm->getService(); - } - - /** - * @return ConfigProvider[] - */ - public function getProviders() - { - return $this->providers; - } - - /** - * @param ConfigProvider $provider - */ - public function addProvider(ConfigProvider $provider) - { - $this->providers[$provider->getScope()] = $provider; - } - - /** - * @param $scope - * @return ConfigProvider - */ - public function getProvider($scope) - { - return $this->providers[$scope]; - } - - /** - * @return EventDispatcher - */ - public function getEventDispatcher() - { - return $this->eventDispatcher; - } - - /** - * @param $className - * @return \Metadata\ClassMetaData - */ - public function getClassMetadata($className) - { - return $this->metadataFactory->getMetadataForClass($className); - } - - /** - * @param $className - * @param $scope - * @throws Exception\RuntimeException - * @return EntityConfig - */ - public function getConfig($className, $scope) - { - /** @var ConfigClassMetadata $metadata */ - $metadata = $this->metadataFactory->getMetadataForClass($className); - if (!$metadata || $metadata->name != $className || !$metadata->configurable) { - throw new RuntimeException(sprintf("Entity '%s' is not Configurable", $className)); - } - - $resultConfig = null; - if (null !== $this->configCache - && $config = $this->configCache->loadConfigFromCache($className, $scope) - ) { - $resultConfig = $config; - } else { - $entityConfigRepo = $this->em()->getRepository(ConfigEntity::ENTITY_NAME); - - /** @var ConfigEntity $entity */ - $entity = $this->isSchemaSynced() ? $entityConfigRepo->findOneBy(array('className' => $className)) : null; - if ($entity) { - $config = $this->entityToConfig($entity, $scope); - - if (null !== $this->configCache) { - $this->configCache->putConfigInCache($config); - } - - $resultConfig = $config; - } else { - $resultConfig = new EntityConfig($className, $scope); - } - } - - $this->originalConfigs[spl_object_hash($resultConfig)] = clone $resultConfig; - - foreach ($resultConfig->getFields() as $field) { - $this->originalConfigs[spl_object_hash($field)] = clone $field; - } - - return $resultConfig; - } - - /** - * @param $className - * @return bool - */ - public function hasConfig($className) - { - /** @var ConfigClassMetadata $metadata */ - $metadata = $this->metadataFactory->getMetadataForClass($className); - - return $metadata ? ($metadata->configurable && $metadata->name == $className) : false; - } - - /** - * @param ClassMetadataInfo $doctrineMetadata - */ - public function initConfigByDoctrineMetadata(ClassMetadataInfo $doctrineMetadata) - { - /** @var ConfigClassMetadata $metadata */ - $metadata = $this->metadataFactory->getMetadataForClass($doctrineMetadata->getName()); - if ($metadata - && $metadata->name == $doctrineMetadata->getName() - && $metadata->configurable - && !$this->em()->getRepository(ConfigEntity::ENTITY_NAME)->findOneBy(array( - 'className' => $doctrineMetadata->getName())) - ) { - foreach ($this->getProviders() as $provider) { - $defaultValues = $provider->getConfigContainer()->getEntityDefaultValues(); - if (isset($metadata->defaultValues[$provider->getScope()])) { - $defaultValues = array_merge($defaultValues, $metadata->defaultValues[$provider->getScope()]); - } - - $provider->createEntityConfig($doctrineMetadata->getName(), $defaultValues); - } - - $this->eventDispatcher->dispatch( - Events::NEW_ENTITY, - new NewEntityEvent($doctrineMetadata->getName(), $this) - ); - - foreach ($doctrineMetadata->getFieldNames() as $fieldName) { - $type = $doctrineMetadata->getTypeOfField($fieldName); - - foreach ($this->getProviders() as $provider) { - $provider->createFieldConfig( - $doctrineMetadata->getName(), - $fieldName, - $type, - $provider->getConfigContainer()->getFieldDefaultValues() - ); - } - - $this->eventDispatcher->dispatch( - Events::NEW_FIELD, - new NewFieldEvent($doctrineMetadata->getName(), $fieldName, $type, $this) - ); - } - - foreach ($doctrineMetadata->getAssociationNames() as $fieldName) { - $type = $doctrineMetadata->isSingleValuedAssociation($fieldName) ? 'ref-one' : 'ref-many'; - - foreach ($this->getProviders() as $provider) { - $provider->createFieldConfig( - $doctrineMetadata->getName(), - $fieldName, - $type, - $provider->getConfigContainer()->getFieldDefaultValues() - ); - } - - $this->eventDispatcher->dispatch( - Events::NEW_FIELD, - new NewFieldEvent($doctrineMetadata->getName(), $fieldName, $type, $this) - ); - } - } - } - - /** - * @param $className - */ - public function clearCache($className) - { - if ($this->configCache) { - foreach ($this->getProviders() as $provider) { - $this->configCache->removeConfigFromCache($className, $provider->getScope()); - } - } - } - - /** - * @param ConfigInterface $config - */ - public function persist(ConfigInterface $config) - { - $this->persistConfigs->push($config); - - if ($config instanceof EntityConfigInterface) { - foreach ($config->getFields() as $fieldConfig) { - $this->persistConfigs->push($fieldConfig); - } - } - } - - /** - * @param ConfigInterface $config - */ - public function remove(ConfigInterface $config) - { - $this->removeConfigs[spl_object_hash($config)] = $config; - - if ($config instanceof EntityConfigInterface) { - foreach ($config->getFields() as $fieldConfig) { - $this->removeConfigs[spl_object_hash($fieldConfig)] = $fieldConfig; - } - } - } - - /** - * TODO:: remove configs - */ - public function flush() - { - $entities = array(); - - foreach ($this->persistConfigs as $config) { - $className = $config->getClassName(); - - if (isset($entities[$className])) { - $configEntity = $entities[$className]; - } else { - $configEntity = $entities[$className] = $this->findOrCreateConfigEntity($className); - } - - $this->eventDispatcher->dispatch(Events::PERSIST_CONFIG, new PersistConfigEvent($config, $this)); - - $this->calculateConfigChangeSet($config); - - $changes = $this->getConfigChangeSet($config); - - if (!count($changes)) { - continue; - } - - $values = array_intersect_key($config->getValues(), $changes); - - if ($config instanceof FieldConfigInterface) { - if (!$configField = $configEntity->getField($config->getCode())) { - $configField = new ConfigField($config->getCode(), $config->getType()); - $configEntity->addField($configField); - } - - $serializableValues = $this->getProvider($config->getScope())->getConfigContainer()->getFieldSerializableValues(); - $configField->fromArray($config->getScope(), $values, $serializableValues); - } else { - $serializableValues = $this->getProvider($config->getScope())->getConfigContainer()->getEntitySerializableValues(); - $configEntity->fromArray($config->getScope(), $values, $serializableValues); - } - - - if ($this->configCache) { - $this->configCache->removeConfigFromCache($className, $config->getScope()); - } - } - - $this->eventDispatcher->dispatch(Events::PRE_FLUSH, new FlushConfigEvent($this)); - - $this->auditManager->log(); - - foreach ($entities as $entity) { - $this->em()->persist($entity); - } - - $this->em()->flush(); - - $this->eventDispatcher->dispatch(Events::ON_FLUSH, new FlushConfigEvent($this)); - - $this->removeConfigs = array(); - $this->originalConfigs = array(); - $this->configChangeSets = array(); - $this->updatedConfigs = array(); - - - $this->eventDispatcher->dispatch(Events::POST_FLUSH, new FlushConfigEvent($this)); - } - - - /** - * @param ConfigInterface $config - */ - public function calculateConfigChangeSet(ConfigInterface $config) - { - $originConfigValue = array(); - if (isset($this->originalConfigs[spl_object_hash($config)])) { - $originConfig = $this->originalConfigs[spl_object_hash($config)]; - $originConfigValue = $originConfig->getValues(); - } - - foreach ($config->getValues() as $key => $value) { - if (!isset($originConfigValue[$key])) { - $originConfigValue[$key] = null; - } - } - - $diffNew = array_udiff_assoc($config->getValues(), $originConfigValue, function ($a, $b) { - return ($a == $b) ? 0 : 1; - }); - - $diffOld = array_udiff_assoc($originConfigValue, $config->getValues(), function ($a, $b) { - return ($a == $b) ? 0 : 1; - }); - - $diff = array(); - foreach ($diffNew as $key => $value) { - $oldValue = isset($diffOld[$key]) ? $diffOld[$key] : null; - $diff[$key] = array($oldValue, $value); - } - - - if (!isset($this->configChangeSets[spl_object_hash($config)])) { - $this->configChangeSets[spl_object_hash($config)] = array(); - } - - if (count($diff)) { - $this->configChangeSets[spl_object_hash($config)] = array_merge($this->configChangeSets[spl_object_hash($config)], $diff); - - if (!isset($this->updatedConfigs[spl_object_hash($config)])) { - $this->updatedConfigs[spl_object_hash($config)] = $config; - } - } - } - - /** - * @param null $scope - * @return ConfigInterface[]|EntityConfigInterface[] - */ - public function getUpdatedEntityConfig($scope = null) - { - return array_filter($this->updatedConfigs, function (ConfigInterface $config) use ($scope) { - if (!$config instanceof EntityConfigInterface) { - return false; - } - - if ($scope && $config->getScope() != $scope) { - return false; - } - - return true; - }); - } - - /** - * @param null $className - * @param null $scope - * @return ConfigInterface[]|FieldConfigInterface[] - */ - public function getUpdatedFieldConfig($scope = null, $className = null) - { - return array_filter($this->updatedConfigs, function (ConfigInterface $config) use ($className, $scope) { - if (!$config instanceof FieldConfigInterface) { - return false; - } - - if ($className && $config->getClassName() != $className) { - return false; - } - - if ($scope && $config->getScope() != $scope) { - return false; - } - - return true; - }); - } - - /** - * @param ConfigInterface $config - * @return array - */ - public function getConfigChangeSet(ConfigInterface $config) - { - return isset($this->configChangeSets[spl_object_hash($config)]) ? $this->configChangeSets[spl_object_hash($config)] : array(); - } - - /** - * @param ConfigEntity $entity - * @param $scope - * @return EntityConfig - */ - protected function entityToConfig(ConfigEntity $entity, $scope) - { - $config = new EntityConfig($entity->getClassName(), $scope); - $config->setValues($entity->toArray($scope)); - - foreach ($entity->getFields() as $field) { - $fieldConfig = new FieldConfig($entity->getClassName(), $field->getCode(), $field->getType(), $scope); - $fieldConfig->setValues($field->toArray($scope)); - $config->addField($fieldConfig); - } - - return $config; - } - - /** - * @param $className - * @return ConfigEntity - */ - protected function findOrCreateConfigEntity($className) - { - $entityConfigRepo = $this->em()->getRepository(ConfigEntity::ENTITY_NAME); - /** @var ConfigEntity $entity */ - $entity = $entityConfigRepo->findOneBy(array('className' => $className)); - if (!$entity) { - $metadata = $this->metadataFactory->getMetadataForClass($className); - $entity = new ConfigEntity($className); - $entity->setMode($metadata->viewMode); - - foreach ($this->getProviders() as $provider) { - $provider->createEntityConfig( - $className, - $provider->getConfigContainer()->getEntityDefaultValues() - ); - } - - $this->eventDispatcher->dispatch( - Events::NEW_ENTITY, - new NewEntityEvent($className, $this) - ); - } - - return $entity; - } - - /** - * @return bool - */ - protected function isSchemaSynced() - { - $tables = $this->em()->getConnection()->getSchemaManager()->listTableNames(); - $table = $this->em()->getClassMetadata(ConfigEntity::ENTITY_NAME)->getTableName(); - - return in_array($table, $tables); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php b/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php index 3114ce8c418..e7535ae48b8 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php +++ b/src/Oro/Bundle/EntityConfigBundle/Controller/AuditController.php @@ -2,19 +2,21 @@ namespace Oro\Bundle\EntityConfigBundle\Controller; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; +use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Oro\Bundle\UserBundle\Annotation\Acl; + +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; use Oro\Bundle\EntityConfigBundle\Datagrid\AuditDatagridManager; use Oro\Bundle\EntityConfigBundle\Datagrid\AuditFieldDatagridManager; /** * EntityBundle controller. - * @Route("/oro_entityconfig") + * @Route("/entity/config") * @Acl( * id="oro_entityconfig", * name="Entity config manipulation", @@ -27,9 +29,10 @@ class AuditController extends Controller * @Route( * "/audit/{entity}/{id}/{_format}", * name="oro_entityconfig_audit", - * requirements={"entity"="[a-zA-Z_]+", "id"="\d+"}, + * requirements={"entity"="[a-zA-Z0-9_]+", "id"="\d+"}, * defaults={"entity"="entity", "id"=0, "_format" = "html"} * ) + * @Template * @Acl( * id="oro_entityconfig_audit", * name="View entity history", @@ -56,20 +59,24 @@ public function auditAction($entity, $id) ) ); - $view = $datagridManager->getDatagrid()->createView(); + $datagridView = $datagridManager->getDatagrid()->createView(); + if ('json' == $this->getRequest()->getRequestFormat()) { + return $this->get('oro_grid.renderer')->renderResultsJsonResponse($datagridView); + } - return 'json' == $this->getRequest()->getRequestFormat() - ? $this->get('oro_grid.renderer')->renderResultsJsonResponse($view) - : $this->render('OroEntityConfigBundle:Audit:audit.html.twig', array('datagrid' => $view)); + return array( + 'datagrid' => $datagridView, + ); } /** * @Route( * "/audit_field/{entity}/{id}/{_format}", * name="oro_entityconfig_audit_field", - * requirements={"entity"="[a-zA-Z_]+", "id"="\d+"}, + * requirements={"entity"="[a-zA-Z0-9_]+", "id"="\d+"}, * defaults={"entity"="entity", "id"=0, "_format" = "html"} * ) + * @Template("OroEntityConfigBundle:Audit:audit.html.twig") * @Acl( * id="oro_entityconfig_audit_field", * name="View entity's field history", @@ -83,16 +90,16 @@ public function auditAction($entity, $id) */ public function auditFieldAction($entity, $id) { - /** @var ConfigField $fieldName */ + /** @var FieldConfigModel $fieldName */ $fieldName = $this->getDoctrine() - ->getRepository('OroEntityConfigBundle:ConfigField') + ->getRepository(FieldConfigModel::ENTITY_NAME) ->findOneBy(array('id' => $id)); /** @var $datagridManager AuditFieldDatagridManager */ $datagridManager = $this->get('oro_entity_config.audit_field_datagrid.manager'); $datagridManager->entityClass = str_replace('_', '\\', $entity); - $datagridManager->fieldName = $fieldName->getCode(); + $datagridManager->fieldName = $fieldName->getFieldName(); $datagridManager->getRouteGenerator()->setRouteParameters( array( @@ -101,10 +108,13 @@ public function auditFieldAction($entity, $id) ) ); - $view = $datagridManager->getDatagrid()->createView(); + $datagridView = $datagridManager->getDatagrid()->createView(); + if ('json' == $this->getRequest()->getRequestFormat()) { + return $this->get('oro_grid.renderer')->renderResultsJsonResponse($datagridView); + } - return 'json' == $this->getRequest()->getRequestFormat() - ? $this->get('oro_grid.renderer')->renderResultsJsonResponse($view) - : $this->render('OroEntityConfigBundle:Audit:audit.html.twig', array('datagrid' => $view)); + return array( + 'datagrid' => $datagridView, + ); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php b/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php index f45d1e3afb2..a11eb0783c1 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php +++ b/src/Oro/Bundle/EntityConfigBundle/Controller/ConfigController.php @@ -2,7 +2,8 @@ namespace Oro\Bundle\EntityConfigBundle\Controller; -use Oro\Bundle\EntityConfigBundle\Metadata\ConfigClassMetadata; +use Doctrine\ORM\QueryBuilder; +use Oro\Bundle\EntityConfigBundle\Metadata\EntityMetadata; use Symfony\Bundle\FrameworkBundle\Controller\Controller; use Symfony\Component\HttpFoundation\Request; @@ -17,12 +18,12 @@ use Oro\Bundle\EntityConfigBundle\Datagrid\EntityFieldsDatagridManager; use Oro\Bundle\EntityConfigBundle\Datagrid\ConfigDatagridManager; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; /** * EntityConfig controller. - * @Route("/oro_entityconfig") + * @Route("/entity/config") * @Acl( * id="oro_entityconfig", * name="Entity config manipulation", @@ -72,20 +73,19 @@ public function indexAction(Request $request) */ public function updateAction($id) { - $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + $entity = $this->getDoctrine()->getRepository(EntityConfigModel::ENTITY_NAME)->find($id); $request = $this->getRequest(); $form = $this->createForm( - 'oro_entity_config_config_entity_type', + 'oro_entity_config_type', null, array( - 'class_name' => $entity->getClassName(), - 'entity_id' => $entity->getId() + 'config_model' => $entity, ) ); if ($request->getMethod() == 'POST') { - $form->bind($request); + $form->submit($request); if ($form->isValid()) { //persist data inside the form @@ -93,7 +93,7 @@ public function updateAction($id) return $this->get('oro_ui.router')->actionRedirect( array( - 'route' => 'oro_entityconfig_update', + 'route' => 'oro_entityconfig_update', 'parameters' => array('id' => $id), ), array( @@ -104,7 +104,7 @@ public function updateAction($id) } /** @var ConfigProvider $entityConfigProvider */ - $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + $entityConfigProvider = $this->get('oro_entity_config.provider.entity'); return array( 'entity' => $entity, @@ -124,7 +124,7 @@ public function updateAction($id) * ) * @Template() */ - public function viewAction(ConfigEntity $entity) + public function viewAction(EntityConfigModel $entity) { /** @var EntityFieldsDatagridManager $datagridManager */ $datagridManager = $this->get('oro_entity_config.entityfieldsdatagrid.manager'); @@ -142,54 +142,66 @@ public function viewAction(ConfigEntity $entity) */ $entityName = $moduleName = ''; $className = explode('\\', $entity->getClassName()); - foreach ($className as $i => $name) { - if (count($className) - 1 == $i) { - $entityName = $name; - } elseif (!in_array($name, array('Bundle', 'Entity'))) { - $moduleName .= $name; + if (count($className) > 1) { + foreach ($className as $i => $name) { + if (count($className) - 1 == $i) { + $entityName = $name; + } elseif (!in_array($name, array('Bundle', 'Entity'))) { + $moduleName .= $name; + } } + } else { + $entityName = $className[0]; + $moduleName = 'Custom'; } - /** - * generate link for Entity grid - */ - - /** @var \Oro\Bundle\EntityConfigBundle\ConfigManager $configManager */ + /** @var \Oro\Bundle\EntityConfigBundle\Config\ConfigManager $configManager */ $configManager = $this->get('oro_entity_config.config_manager'); - /** @var ConfigClassMetadata $metadata */ - $metadata = $configManager->getClassMetadata($entity->getClassName()); - + // generate link for Entity grid $link = ''; - if ($metadata->routeName) { - $link = $this->generateUrl($metadata->routeName); + /** @var EntityMetadata $metadata */ + if (class_exists($entity->getClassName())) { + $metadata = $configManager->getEntityMetadata($entity->getClassName()); + + if ($metadata && $metadata->routeName) { + $link = $this->generateUrl($metadata->routeName); + } } /** @var ConfigProvider $entityConfigProvider */ - $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + $entityConfigProvider = $this->get('oro_entity_config.provider.entity'); /** @var ConfigProvider $extendConfigProvider */ - $extendConfigProvider = $this->get('oro_entity_extend.config.extend_config_provider'); - $extendConfig = $extendConfigProvider->getConfig($entity->getClassName()); + $extendConfigProvider = $this->get('oro_entity_config.provider.extend'); + $extendConfig = $extendConfigProvider->getConfig($entity->getClassName()); + + /** @var ConfigProvider $ownershipConfigProvider */ + $ownershipConfigProvider = $this->get('oro_entity_config.provider.ownership'); + - /* - var_dump($this->getRequest()->headers->get('referer')); - if (strstr('oro_entityextend/update', $this->getRequest()->headers->get('referer'))) { - $this->get('session')->getFlashBag()->add('success', 'Schema successfully updated.'); - }*/ + if (class_exists($entity->getClassName())) { + /** @var QueryBuilder $qb */ + $qb = $this->getDoctrine()->getManager()->createQueryBuilder(); + $qb->select('count(entity)'); + $qb->from($entity->getClassName(), 'entity'); + $entityCount = $qb->getQuery()->getSingleScalarResult(); + } else { + $entityCount = 0; + } return array( - 'entity' => $entity, - 'entity_config' => $entityConfigProvider->getConfig($entity->getClassName()), - 'entity_extend' => $extendConfig, - 'entity_count' => count($this->getDoctrine()->getRepository($entity->getClassName())->findAll()), - 'entity_fields' => $datagrid->createView(), - - 'unique_key' => $extendConfig->get('unique_key'), - 'link' => $link, - 'entity_name' => $entityName, - 'module_name' => $moduleName, - 'button_config' => $datagridManager->getLayoutActions($entity), + 'entity' => $entity, + 'entity_config' => $entityConfigProvider->getConfig($entity->getClassName()), + 'entity_extend' => $extendConfig, + 'entity_count' => $entityCount, + 'entity_fields' => $datagrid->createView(), + 'entity_ownership' => $ownershipConfigProvider->getConfig($entity->getClassName()), + 'unique_key' => $extendConfig->get('unique_key'), + 'link' => $link, + 'entity_name' => $entityName, + 'module_name' => $moduleName, + 'button_config' => $datagridManager->getLayoutActions($entity), ); } @@ -200,9 +212,9 @@ public function viewAction(ConfigEntity $entity) */ public function fieldsAction($id, Request $request) { - $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + $entity = $this->getDoctrine()->getRepository(EntityConfigModel::ENTITY_NAME)->find($id); - /** @var FieldsDatagridManager $datagridManager */ + /** @var EntityFieldsDatagridManager $datagridManager */ $datagridManager = $this->get('oro_entity_config.entityfieldsdatagrid.manager'); $datagridManager->setEntityId($id); @@ -241,22 +253,20 @@ public function fieldsAction($id, Request $request) */ public function fieldUpdateAction($id) { - $field = $this->getDoctrine()->getRepository(ConfigField::ENTITY_NAME)->find($id); + /** @var FieldConfigModel $field */ + $field = $this->getDoctrine()->getRepository(FieldConfigModel::ENTITY_NAME)->find($id); + $request = $this->getRequest(); - $form = $this->createForm( - 'oro_entity_config_config_field_type', + $form = $this->createForm( + 'oro_entity_config_type', null, array( - 'class_name' => $field->getEntity()->getClassName(), - 'field_name' => $field->getCode(), - 'field_type' => $field->getType(), - 'field_id' => $field->getId(), + 'config_model' => $field, ) ); - $request = $this->getRequest(); if ($request->getMethod() == 'POST') { - $form->bind($request); + $form->submit($request); if ($form->isValid()) { //persist data inside the form @@ -264,11 +274,11 @@ public function fieldUpdateAction($id) return $this->get('oro_ui.router')->actionRedirect( array( - 'route' => 'oro_entityconfig_field_update', + 'route' => 'oro_entityconfig_field_update', 'parameters' => array('id' => $id), ), array( - 'route' => 'oro_entityconfig_view', + 'route' => 'oro_entityconfig_view', 'parameters' => array('id' => $field->getEntity()->getId()) ) ); @@ -276,13 +286,19 @@ public function fieldUpdateAction($id) } /** @var ConfigProvider $entityConfigProvider */ - $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + $entityConfigProvider = $this->get('oro_entity_config.provider.entity'); + $entityConfig = $entityConfigProvider->getConfig($field->getEntity()->getClassName()); + $fieldConfig = $entityConfigProvider->getConfig( + $field->getEntity()->getClassName(), + $field->getFieldName() + ); return array( - 'entity_config' => $entityConfigProvider->getConfig($field->getEntity()->getClassName()), - 'field_config' => $entityConfigProvider->getFieldConfig($field->getEntity()->getClassName(), $field->getCode()), + 'entity_config' => $entityConfig, + 'field_config' => $fieldConfig, 'field' => $field, 'form' => $form->createView(), + 'formAction' => $this->generateUrl('oro_entityconfig_field_update', array('id' => $field->getId())) ); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagrid.php b/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagrid.php index f382890594e..28faf50fc99 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagrid.php +++ b/src/Oro/Bundle/EntityConfigBundle/Datagrid/AuditDatagrid.php @@ -11,7 +11,7 @@ use Oro\Bundle\GridBundle\Property\FixedProperty; use Oro\Bundle\GridBundle\Property\TwigTemplateProperty; -use Oro\Bundle\EntityConfigBundle\ConfigManager; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; use Oro\Bundle\GridBundle\Sorter\SorterInterface; abstract class AuditDatagrid extends DatagridManager @@ -46,23 +46,6 @@ public function __construct(ConfigManager $configManager) */ protected function configureFields(FieldDescriptionCollection $fieldsCollection) { - $fieldId = new FieldDescription(); - $fieldId->setName('id'); - $fieldId->setOptions( - array( - 'type' => FieldDescriptionInterface::TYPE_INTEGER, - 'label' => 'Commit Id', - 'field_name' => 'id', - 'filter_type' => FilterInterface::TYPE_NUMBER, - 'required' => false, - 'sortable' => true, - 'filterable' => false, - 'show_filter' => false, - 'show_column' => false, - ) - ); - $fieldsCollection->add($fieldId); - $fieldAuthor = new FieldDescription(); $fieldAuthor->setName('author'); $fieldAuthor->setOptions( diff --git a/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php b/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php index ae3a27c22f5..b962ff6a40c 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php +++ b/src/Oro/Bundle/EntityConfigBundle/Datagrid/ConfigDatagridManager.php @@ -4,18 +4,31 @@ use Doctrine\ORM\Query; -use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfig; +use Oro\Bundle\EntityConfigBundle\Config\ConfigModelManager; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; + +use Oro\Bundle\EntityConfigBundle\Provider\PropertyConfigContainer; + use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; -use Oro\Bundle\GridBundle\Action\ActionInterface; use Oro\Bundle\GridBundle\Datagrid\DatagridManager; +use Oro\Bundle\GridBundle\Datagrid\ResultRecord; + use Oro\Bundle\GridBundle\Field\FieldDescription; use Oro\Bundle\GridBundle\Field\FieldDescriptionCollection; use Oro\Bundle\GridBundle\Field\FieldDescriptionInterface; + use Oro\Bundle\GridBundle\Filter\FilterInterface; + +use Oro\Bundle\GridBundle\Property\TwigTemplateProperty; use Oro\Bundle\GridBundle\Property\UrlProperty; +use Oro\Bundle\GridBundle\Property\ActionConfigurationProperty; -use Oro\Bundle\EntityConfigBundle\ConfigManager; +use Oro\Bundle\GridBundle\Action\ActionInterface; +/** + * @SuppressWarnings(PHPMD.NPathComplexity) + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + */ class ConfigDatagridManager extends DatagridManager { /** @@ -41,7 +54,7 @@ public function getLayoutActions() $actions = array(); foreach ($this->configManager->getProviders() as $provider) { - foreach ($provider->getConfigContainer()->getEntityLayoutActions() as $config) { + foreach ($provider->getPropertyConfig()->getLayoutActions() as $config) { $actions[] = $config; } } @@ -59,17 +72,71 @@ protected function getProperties() new UrlProperty('update_link', $this->router, 'oro_entityconfig_update', array('id')), ); + $filters = array(); + $actions = array(); + foreach ($this->configManager->getProviders() as $provider) { - foreach ($provider->getConfigContainer()->getEntityGridActions() as $config) { + foreach ($provider->getPropertyConfig()->getGridActions() as $config) { $properties[] = new UrlProperty( strtolower($config['name']) . '_link', $this->router, $config['route'], (isset($config['args']) ? $config['args'] : array()) ); + + if (isset($config['filter'])) { + $filters[strtolower($config['name'])] = $config['filter']; + } + + $actions[strtolower($config['name'])] = true; + } + + if ($provider->getPropertyConfig()->getUpdateActionFilter()) { + $filters['update'] = $provider->getPropertyConfig()->getUpdateActionFilter(); } } + if (count($filters)) { + $properties[] = new ActionConfigurationProperty( + function (ResultRecord $record) use ($filters, $actions) { + if ($record->getValue('mode') == ConfigModelManager::MODE_READONLY) { + $actions = array_map( + function () { + return false; + }, + $actions + ); + + $actions['update'] = false; + } else { + foreach ($filters as $action => $filter) { + foreach ($filter as $key => $value) { + if (is_array($value)) { + $error = true; + foreach ($value as $v) { + if ($record->getValue($key) == $v) { + $error = false; + } + } + if ($error) { + $actions[$action] = false; + break; + } + } else { + if ($record->getValue($key) != $value) { + $actions[$action] = false; + break; + } + } + } + } + } + + return $actions; + } + ); + } + return $properties; } @@ -79,7 +146,7 @@ protected function getProperties() */ protected function getObjectName($scope = 'name') { - $options = array('name'=> array(), 'module'=> array()); + $options = array('name' => array(), 'module' => array()); $query = $this->createQuery()->getQueryBuilder() ->add('select', 'ce.className') @@ -90,15 +157,20 @@ protected function getObjectName($scope = 'name') foreach ((array) $result as $value) { $className = explode('\\', $value['className']); - $options['name'][$value['className']] = ''; + $options['name'][$value['className']] = ''; $options['module'][$value['className']] = ''; - foreach ($className as $index => $name) { - if (count($className)-1 == $index) { - $options['name'][$value['className']] = $name; - } elseif (!in_array($name, array('Bundle','Entity'))) { - $options['module'][$value['className']] .= $name; + if (count($className) > 1) { + foreach ($className as $index => $name) { + if (count($className) - 1 == $index) { + $options['name'][$value['className']] = $name; + } elseif (!in_array($name, array('Bundle', 'Entity'))) { + $options['module'][$value['className']] .= $name; + } } + } else { + $options['name'][$value['className']] = $value['className']; + $options['module'][$value['className']] = 'System'; } } @@ -112,8 +184,10 @@ protected function getDynamicFields(FieldDescriptionCollection $fieldsCollection { $fields = array(); foreach ($this->configManager->getProviders() as $provider) { - foreach ($provider->getConfigContainer()->getEntityItems() as $code => $item) { + foreach ($provider->getPropertyConfig()->getItems() as $code => $item) { if (isset($item['grid'])) { + $item['grid'] = $provider->getPropertyConfig()->initConfig($item['grid']); + $fieldObjectProvider = new FieldDescription(); $fieldObjectProvider->setName($code); $fieldObjectProvider->setOptions( @@ -126,6 +200,17 @@ protected function getDynamicFields(FieldDescriptionCollection $fieldsCollection ) ); + if (isset($item['grid']['type']) + && $item['grid']['type'] == FieldDescriptionInterface::TYPE_HTML + && isset($item['grid']['template']) + ) { + $templateDataProperty = new TwigTemplateProperty( + $fieldObjectProvider, + $item['grid']['template'] + ); + $fieldObjectProvider->setProperty($templateDataProperty); + } + if (isset($item['options']['priority']) && !isset($fields[$item['options']['priority']])) { $fields[$item['options']['priority']] = $fieldObjectProvider; } else { @@ -148,23 +233,6 @@ protected function configureFields(FieldDescriptionCollection $fieldsCollection) { $this->getDynamicFields($fieldsCollection); - $fieldObjectId = new FieldDescription(); - $fieldObjectId->setName('id'); - $fieldObjectId->setOptions( - array( - 'type' => FieldDescriptionInterface::TYPE_INTEGER, - 'label' => 'Id', - 'field_name' => 'id', - 'filter_type' => FilterInterface::TYPE_NUMBER, - 'required' => false, - 'sortable' => false, - 'filterable' => false, - 'show_filter' => false, - 'show_column' => false, - ) - ); - $fieldsCollection->add($fieldObjectId); - $fieldObjectName = new FieldDescription(); $fieldObjectName->setName('name'); $fieldObjectName->setOptions( @@ -275,7 +343,7 @@ protected function getRowActions() $actions = array($clickAction, $viewAction, $updateAction); foreach ($this->configManager->getProviders() as $provider) { - foreach ($provider->getConfigContainer()->getEntityGridActions() as $config) { + foreach ($provider->getPropertyConfig()->getGridActions() as $config) { $configItem = array( 'name' => strtolower($config['name']), 'acl_resource' => isset($config['acl_resource']) ? $config['acl_resource'] : 'root', @@ -294,6 +362,9 @@ protected function getRowActions() case 'redirect': $configItem['type'] = ActionInterface::TYPE_REDIRECT; break; + case 'ajax': + $configItem['type'] = ActionInterface::TYPE_AJAX; + break; } } else { $configItem['type'] = ActionInterface::TYPE_REDIRECT; @@ -312,13 +383,21 @@ protected function getRowActions() */ protected function prepareQuery(ProxyQueryInterface $query) { - $query->where('ce.mode <> :mode'); - $query->setParameter('mode', AbstractConfig::MODE_VIEW_HIDDEN); - foreach ($this->configManager->getProviders() as $provider) { - foreach ($provider->getConfigContainer()->getEntityItems() as $code => $item) { + foreach ($provider->getPropertyConfig()->getItems() as $code => $item) { $alias = 'cev' . $code; - $query->leftJoin('ce.values', $alias, 'WITH', $alias . ".code='" . $code . "' AND " . $alias . ".scope='" . $provider->getScope() . "'"); + + if (isset($item['grid']['query'])) { + $query->andWhere($alias . '.value ' . $item['grid']['query']['operator'] . ' :' . $alias); + $query->setParameter($alias, $item['grid']['query']['value']); + } + + $query->leftJoin( + 'ce.values', + $alias, + 'WITH', + $alias . ".code='" . $code . "' AND " . $alias . ".scope='" . $provider->getScope() . "'" + ); $query->addSelect($alias . '.value as ' . $code, true); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php b/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php index f13954b6394..b2d806d78e2 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php +++ b/src/Oro/Bundle/EntityConfigBundle/Datagrid/EntityFieldsDatagridManager.php @@ -4,17 +4,26 @@ use Doctrine\ORM\Query; -use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\GridBundle\Action\ActionInterface; + +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\GridBundle\Datagrid\DatagridManager; +use Oro\Bundle\GridBundle\Datagrid\ResultRecord; + use Oro\Bundle\GridBundle\Field\FieldDescription; use Oro\Bundle\GridBundle\Field\FieldDescriptionCollection; use Oro\Bundle\GridBundle\Field\FieldDescriptionInterface; + use Oro\Bundle\GridBundle\Filter\FilterInterface; + +use Oro\Bundle\GridBundle\Property\ActionConfigurationProperty; use Oro\Bundle\GridBundle\Property\UrlProperty; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; -use Oro\Bundle\EntityConfigBundle\ConfigManager; +use Oro\Bundle\EntityConfigBundle\Config\ConfigModelManager; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; + +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; +use Oro\Bundle\EntityConfigBundle\Provider\PropertyConfigContainer; class EntityFieldsDatagridManager extends DatagridManager { @@ -24,7 +33,7 @@ class EntityFieldsDatagridManager extends DatagridManager protected $fieldsCollection; /** - * @var ConfigEntity id + * @var integer id */ protected $entityId; @@ -47,23 +56,36 @@ public function setEntityId($id) } /** - * @param ConfigEntity $entity + * @param EntityConfigModel $entity * @return array */ - public function getLayoutActions(ConfigEntity $entity) + public function getLayoutActions(EntityConfigModel $entity) { $actions = array(); foreach ($this->configManager->getProviders() as $provider) { - foreach ($provider->getConfigContainer()->getFieldLayoutActions() as $config) { - if (isset($config['filter']) - && !$provider->getConfig($entity->getClassName())->is($config['filter']) - ) { - continue; + foreach ($provider->getPropertyConfig()->getLayoutActions(PropertyConfigContainer::TYPE_FIELD) as $config) { + if (isset($config['filter'])) { + foreach ($config['filter'] as $key => $value) { + if (is_array($value)) { + $error = true; + foreach ($value as $v) { + if ($provider->getConfig($entity->getClassName())->get($key) == $v) { + $error = false; + } + } + if ($error) { + continue 2; + } + } elseif ($provider->getConfig($entity->getClassName())->get($key) != $value) { + continue 2; + } + } } if (isset($config['entity_id']) && $config['entity_id'] == true) { $config['args'] = array('id' => $entity->getId()); } + $actions[] = $config; } } @@ -79,17 +101,69 @@ protected function getProperties() $properties = array( new UrlProperty('update_link', $this->router, 'oro_entityconfig_field_update', array('id')), ); + + $filters = array(); + $actions = array(); + foreach ($this->configManager->getProviders() as $provider) { - foreach ($provider->getConfigContainer()->getFieldGridActions() as $config) { + foreach ($provider->getPropertyConfig()->getGridActions(PropertyConfigContainer::TYPE_FIELD) as $config) { $properties[] = new UrlProperty( strtolower($config['name']) . '_link', $this->router, $config['route'], (isset($config['args']) ? $config['args'] : array()) ); + + if (isset($config['filter'])) { + $filters[strtolower($config['name'])] = $config['filter']; + } + + $actions[strtolower($config['name'])] = true; } } + if (count($filters)) { + $properties[] = new ActionConfigurationProperty( + function (ResultRecord $record) use ($filters, $actions) { + if ($record->getValue('mode') == ConfigModelManager::MODE_READONLY) { + $actions = array_map( + function () { + return false; + }, + $actions + ); + + $actions['update'] = false; + $actions['rowClick'] = false; + } else { + foreach ($filters as $action => $filter) { + foreach ($filter as $key => $value) { + if (is_array($value)) { + $error = true; + foreach ($value as $v) { + if ($record->getValue($key) == $v) { + $error = false; + } + } + if ($error) { + $actions[$action] = false; + break; + } + } else { + if ($record->getValue($key) != $value) { + $actions[$action] = false; + break; + } + } + } + } + } + + return $actions; + } + ); + } + return $properties; } @@ -98,30 +172,26 @@ protected function getProperties() */ protected function configureFields(FieldDescriptionCollection $fieldsCollection) { - $fieldId = new FieldDescription(); - $fieldId->setName('id'); - $fieldId->setOptions( + $fieldObjectClassName = new FieldDescription(); + $fieldObjectClassName->setName('className'); + $fieldObjectClassName->setOptions( array( 'type' => FieldDescriptionInterface::TYPE_TEXT, - 'label' => 'Id', - 'field_name' => 'id', - 'filter_type' => FilterInterface::TYPE_STRING, - 'required' => false, - 'sortable' => false, - 'filterable' => false, - 'show_filter' => false, + 'label' => 'ClassName', + 'field_name' => 'className', 'show_column' => false, + 'expression' => 'ce.className' ) ); - $fieldsCollection->add($fieldId); + $fieldsCollection->add($fieldObjectClassName); $fieldCode = new FieldDescription(); - $fieldCode->setName('code'); + $fieldCode->setName('fieldName'); $fieldCode->setOptions( array( 'type' => FieldDescriptionInterface::TYPE_TEXT, 'label' => 'Name', - 'field_name' => 'code', + 'field_name' => 'fieldName', 'filter_type' => FilterInterface::TYPE_STRING, 'required' => false, 'sortable' => true, @@ -158,14 +228,19 @@ protected function addDynamicRows($fieldsCollection) $fields = array(); foreach ($this->configManager->getProviders() as $provider) { - foreach ($provider->getConfigContainer()->getFieldItems() as $code => $item) { + foreach ($provider->getPropertyConfig()->getItems(PropertyConfigContainer::TYPE_FIELD) as $code => $item) { if (isset($item['grid'])) { $fieldObject = new FieldDescription(); $fieldObject->setName($code); - $fieldObject->setOptions(array_merge($item['grid'], array( - 'expression' => 'cfv_' . $code . '.value', - 'field_name' => $code, - ))); + $fieldObject->setOptions( + array_merge( + $item['grid'], + array( + 'expression' => 'cfv_' . $code . '.value', + 'field_name' => $code, + ) + ) + ); if (isset($item['options']['priority']) && !isset($fields[$item['options']['priority']])) { $fields[$item['options']['priority']] = $fieldObject; @@ -212,7 +287,7 @@ protected function getRowActions() $actions = array($clickAction, $updateAction); foreach ($this->configManager->getProviders() as $provider) { - foreach ($provider->getConfigContainer()->getFieldGridActions() as $config) { + foreach ($provider->getPropertyConfig()->getGridActions(PropertyConfigContainer::TYPE_FIELD) as $config) { $configItem = array( 'name' => strtolower($config['name']), 'acl_resource' => isset($config['acl_resource']) ? $config['acl_resource'] : 'root', @@ -231,6 +306,9 @@ protected function getRowActions() case 'redirect': $configItem['type'] = ActionInterface::TYPE_REDIRECT; break; + case 'ajax': + $configItem['type'] = ActionInterface::TYPE_AJAX; + break; } } else { $configItem['type'] = ActionInterface::TYPE_REDIRECT; @@ -250,14 +328,27 @@ protected function createQuery() { /** @var ProxyQueryInterface|Query $query */ $query = parent::createQuery(); + $query->where('cf.mode <> :mode'); + $query->setParameter('mode', ConfigModelManager::MODE_HIDDEN); $query->innerJoin('cf.entity', 'ce', 'WITH', 'ce.id=' . $this->entityId); $query->addSelect('ce.id as entity_id', true); foreach ($this->configManager->getProviders() as $provider) { - foreach ($provider->getConfigContainer()->getFieldItems() as $code => $item) { + foreach ($provider->getPropertyConfig()->getItems(PropertyConfigContainer::TYPE_FIELD) as $code => $item) { //$code = $provider->getScope() . $code; $alias = 'cfv_' . $code; - $query->leftJoin('cf.values', $alias, 'WITH', $alias . ".code='" . $code . "' AND " . $alias . ".scope='" . $provider->getScope() . "'"); + + if (isset($item['grid']['query'])) { + $query->andWhere($alias . '.value ' . $item['grid']['query']['operator'] . ' :' . $alias); + $query->setParameter($alias, $item['grid']['query']['value']); + } + + $query->leftJoin( + 'cf.values', + $alias, + 'WITH', + $alias . ".code='" . $code . "' AND " . $alias . ".scope='" . $provider->getScope() . "'" + ); $query->addSelect($alias . '.value as ' . $code, true); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/EntityConfigPass.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/EntityConfigPass.php index d2f314fbde9..dbb9f65ee1f 100644 --- a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/EntityConfigPass.php +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/EntityConfigPass.php @@ -7,44 +7,41 @@ use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -use Oro\Bundle\EntityConfigBundle\Exception\RuntimeException; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Yaml\Yaml; class EntityConfigPass implements CompilerPassInterface { - const TAG_NAME = 'oro_entity_config.provider'; - /** * {@inheritdoc} */ public function process(ContainerBuilder $container) { - $configManagerDefinition = $container->getDefinition('oro_entity_config.config_manager'); - - $tags = $container->findTaggedServiceIds(self::TAG_NAME); - foreach ($tags as $id => $tag) { - /** @var Definition $provider */ - $provider = $container->getDefinition($id); - - if (!isset($tag[0]['scope'])) { - throw new RuntimeException( - sprintf("Tag '%s' for service '%s' doesn't have required param 'scope'", self::TAG_NAME, $id) - ); + $providerBagDefinition = $container->getDefinition('oro_entity_config.provider_bag'); + + foreach ($container->getParameter('kernel.bundles') as $bundle) { + $reflection = new \ReflectionClass($bundle); + if (is_file($file = dirname($reflection->getFilename()) . '/Resources/config/entity_config.yml')) { + $bundleConfig = Yaml::parse(realpath($file)); + + if (isset($bundleConfig['oro_entity_config']) && count($bundleConfig['oro_entity_config'])) { + foreach ($bundleConfig['oro_entity_config'] as $scope => $config) { + $provider = new Definition('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider'); + $provider->setArguments( + array( + new Reference('oro_entity_config.config_manager'), + new Reference('service_container'), + $scope, + $config + ) + ); + + $container->setDefinition('oro_entity_config.provider.' . $scope, $provider); + + $providerBagDefinition->addMethodCall('addProvider', array($provider)); + } + } } - - if (!$container->hasDefinition('oro_entity_config.entity_config.' . $tag[0]['scope'])) { - throw new RuntimeException(sprintf( - "Resources/config/entity_config.yml not found or has wrong 'scope'. Service '%s' with tag '%s' and tag-scope '%s' ", - $id, self::TAG_NAME, $tag[0]['scope'] - )); - } - - $provider->setClass('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider'); - $provider->setArguments(array( - new Reference('oro_entity_config.config_manager'), - new Reference('oro_entity_config.entity_config.' . $tag[0]['scope']) - )); - - $configManagerDefinition->addMethodCall('addProvider', array($provider)); } } } diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceProxyPass.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceLinkPass.php similarity index 60% rename from src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceProxyPass.php rename to src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceLinkPass.php index 56971dcb257..73525b9e0c6 100644 --- a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceProxyPass.php +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceLinkPass.php @@ -9,9 +9,9 @@ use Oro\Bundle\EntityConfigBundle\Exception\RuntimeException; -class ServiceProxyPass implements CompilerPassInterface +class ServiceLinkPass implements CompilerPassInterface { - const TAG_NAME = 'oro_entity_config.proxy'; + const TAG_NAME = 'oro_service_link'; /** * {@inheritdoc} @@ -31,17 +31,24 @@ public function process(ContainerBuilder $container) } if (!$container->hasDefinition($tag[0]['service'])) { - throw new RuntimeException(sprintf( - "Target service '%s' is undefined. Proxy Service '%s' with tag '%s' and tag-service '%s' ", - $tag[0]['service'], $id, self::TAG_NAME, $tag[0]['service'] - )); + throw new RuntimeException( + sprintf( + "Target service '%s' is undefined. Link Service '%s' with tag '%s' and tag-service '%s' ", + $tag[0]['service'], + $id, + self::TAG_NAME, + $tag[0]['service'] + ) + ); } - $service->setClass('Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy'); - $service->setArguments(array( - new Reference('service_container'), - $tag[0]['service'] - )); + $service->setClass('Oro\Bundle\EntityConfigBundle\DependencyInjection\Utils\ServiceLink'); + $service->setArguments( + array( + new Reference('service_container'), + $tag[0]['service'] + ) + ); } } } diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceMethodPass.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceMethodPass.php new file mode 100644 index 00000000000..8f0ecfbe9bd --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Compiler/ServiceMethodPass.php @@ -0,0 +1,68 @@ +findTaggedServiceIds(self::TAG_NAME); + + foreach ($tags as $id => $tag) { + /** @var Definition $service */ + $service = $container->getDefinition($id); + + if (!isset($tag[0]['service'])) { + throw new RuntimeException( + sprintf("Tag '%s' for service '%s' doesn't have required param 'service'", self::TAG_NAME, $id) + ); + } + + if (!$container->hasDefinition($tag[0]['service'])) { + throw new RuntimeException( + sprintf( + "Target service '%s' is undefined. Service Method '%s' with tag '%s' and tag-service '%s' ", + $tag[0]['service'], + $id, + self::TAG_NAME, + $tag[0]['service'] + ) + ); + } + + $serviceDefinition = $container->getDefinition($tag[0]['service']); + $class = $container->getParameterBag()->resolveValue($serviceDefinition->getClass()); + if (!method_exists($class, $tag[0]['method'])) { + throw new RuntimeException( + sprintf( + 'Method "%s" for target service "%s" is undefined.' + . ' Service Method "%s:%s" with tag "%s" and tag-service "%s" ', + $tag[0]['method'], + $tag[0]['service'], + $id, + $tag[0]['method'], + self::TAG_NAME, + $tag[0]['service'] + ) + ); + } + + $service->setClass('Oro\Bundle\EntityConfigBundle\DependencyInjection\Utils\ServiceMethod'); + + $service->addMethodCall('setMethod', array($tag[0]['method'])); + $service->addMethodCall('setService', array(new Reference($tag[0]['service']))); + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/EntityConfigContainer.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/EntityConfigContainer.php deleted file mode 100644 index cc2ea77f68c..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/EntityConfigContainer.php +++ /dev/null @@ -1,296 +0,0 @@ -config = $config; - $this->scope = $scope; - } - - /** - * @return string - */ - public function getScope() - { - return $this->scope; - } - - /** - * @return array - */ - public function getConfig() - { - return $this->config; - } - - /** - * @return array - */ - public function getEntityItems() - { - $entityItems = array(); - if (isset($this->config['entity']) && isset($this->config['entity']['items'])) { - $entityItems = $this->config['entity']['items']; - } - - return $entityItems; - } - - /** - * @return array - */ - public function getEntityDefaultValues() - { - $result = array(); - foreach ($this->getEntityItems() as $code => $item) { - if (isset($item['options']['default_value'])) { - $result[$code] = $item['options']['default_value']; - } - } - - return $result; - } - - /** - * @return array - */ - public function getEntityInternalValues() - { - $result = array(); - foreach ($this->getEntityItems() as $code => $item) { - if (isset($item['options']['internal']) && $item['options']['internal']) { - $result[$code] = 0; - } - } - - return $result; - } - - /** - * @return array - */ - public function getEntitySerializableValues() - { - $result = array(); - foreach ($this->getEntityItems() as $code => $item) { - if (isset($item['options']['serializable'])) { - $result[$code] = (bool) $item['options']['serializable']; - } - } - - return $result; - } - - /** - * @return bool - */ - public function hasEntityForm() - { - return (boolean) array_filter($this->getEntityItems(), function ($item) { - return (isset($item['form']) && isset($item['form']['type'])); - }); - } - - /** - * @return array - */ - public function getEntityFormBlockConfig() - { - $entityFormBlockConfig = null; - if (isset($this->config['entity']) - && isset($this->config['entity']['form']) - && isset($this->config['entity']['form']['block_config']) - ) { - $entityFormBlockConfig = $this->config['entity']['form']['block_config']; - } - - return $entityFormBlockConfig; - } - - /** - * @return array - */ - public function getEntityGridActions() - { - $entityGridActions = array(); - if (isset($this->config['entity']) && isset($this->config['entity']['grid_action'])) { - $entityGridActions = $this->config['entity']['grid_action']; - } - - return $entityGridActions; - } - - /** - * @return array - */ - public function getEntityLayoutActions() - { - $entityLayoutActions = array(); - if (isset($this->config['entity']) && isset($this->config['entity']['layout_action'])) { - $entityLayoutActions = $this->config['entity']['layout_action']; - } - - return $entityLayoutActions; - } - - /** - * @return array - */ - public function getFieldItems() - { - $fieldItems = array(); - if (isset($this->config['field']) && isset($this->config['field']['items'])) { - $fieldItems = $this->config['field']['items']; - } - - return $fieldItems; - } - - /** - * @return array - */ - public function getFieldDefaultValues() - { - $result = array(); - foreach ($this->getFieldItems() as $code => $item) { - if (isset($item['options']['default_value'])) { - $result[$code] = $item['options']['default_value']; - } - } - - return $result; - } - - /** - * @return array - */ - public function getFieldInternalValues() - { - $result = array(); - foreach ($this->getFieldItems() as $code => $item) { - if (isset($item['options']['internal']) && $item['options']['internal']) { - $result[$code] = true; - } - } - - return $result; - } - - - /** - * @return array - */ - public function getFieldRequiredPropertyValues() - { - $result = array(); - foreach ($this->getFieldItems() as $code => $item) { - if (isset($item['options']['required_property'])) { - $result[$code] = $item['options']['required_property']; - } - } - - return $result; - } - - /** - * @return array - */ - public function getFieldSerializableValues() - { - $result = array(); - foreach ($this->getFieldItems() as $code => $item) { - if (isset($item['options']['serializable'])) { - $result[$code] = (bool) $item['options']['serializable']; - } - } - - return $result; - } - - /** - * @return bool - */ - public function hasFieldForm() - { - return (boolean) array_filter($this->getFieldItems(), function ($item) { - return (isset($item['form']) && isset($item['form']['type'])); - }); - } - - /** - * @return array - */ - public function getFieldFormConfig() - { - $fieldFormConfig = array(); - if (isset($this->config['field']) && isset($this->config['field']['form'])) { - $fieldFormConfig = $this->config['field']['form']; - } - - return $fieldFormConfig; - } - - /** - * @return array - */ - public function getFieldFormBlockConfig() - { - $entityFormBlockConfig = null; - if (isset($this->config['field']) - && isset($this->config['field']['form']) - && isset($this->config['field']['form']['block_config']) - ) { - $entityFormBlockConfig = $this->config['field']['form']['block_config']; - } - - return $entityFormBlockConfig; - } - - /** - * @return array - */ - public function getFieldGridActions() - { - $fieldGridActions = array(); - if (isset($this->config['field']) && isset($this->config['field']['grid_action'])) { - $fieldGridActions = $this->config['field']['grid_action']; - } - - return $fieldGridActions; - } - - /** - * @return array - */ - public function getFieldLayoutActions() - { - $fieldLayoutActions = array(); - if (isset($this->config['field']) && isset($this->config['field']['layout_action'])) { - $fieldLayoutActions = $this->config['field']['layout_action']; - } - - return $fieldLayoutActions; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/OroEntityConfigExtension.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/OroEntityConfigExtension.php index 7296d42de46..64727c73355 100644 --- a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/OroEntityConfigExtension.php +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/OroEntityConfigExtension.php @@ -3,8 +3,8 @@ namespace Oro\Bundle\EntityConfigBundle\DependencyInjection; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader; + use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Config\FileLocator; @@ -19,8 +19,6 @@ class OroEntityConfigExtension extends Extension */ public function load(array $configs, ContainerBuilder $container) { - $this->loadBundleConfig($container); - $configuration = new Configuration(); $config = $this->processConfiguration($configuration, $configs); @@ -34,26 +32,6 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('form_type.yml'); } - protected function loadBundleConfig(ContainerBuilder $container) - { - foreach ($container->getParameter('kernel.bundles') as $bundle) { - $reflection = new \ReflectionClass($bundle); - if (is_file($file = dirname($reflection->getFilename()) . '/Resources/config/entity_config.yml')) { - $bundleConfig = Yaml::parse(realpath($file)); - - if (isset($bundleConfig['oro_entity_config']) && count($bundleConfig['oro_entity_config'])) { - foreach ($bundleConfig['oro_entity_config'] as $scope => $config) { - $definition = new Definition( - 'Oro\Bundle\EntityConfigBundle\DependencyInjection\EntityConfigContainer', - array($scope, $config) - ); - $container->setDefinition('oro_entity_config.entity_config.' . $scope, $definition); - } - } - } - } - } - /** * @param ContainerBuilder $container * @param $config @@ -84,7 +62,12 @@ protected function configCache(ContainerBuilder $container, $config) $annotationCacheDir = $cacheDir . '/annotation'; if (!is_dir($annotationCacheDir)) { if (false === @mkdir($annotationCacheDir, 0777, true)) { - throw new RuntimeException(sprintf('Could not create annotation cache directory "%s".', $annotationCacheDir)); + throw new RuntimeException( + sprintf( + 'Could not create annotation cache directory "%s".', + $annotationCacheDir + ) + ); } } $container->setParameter('oro_entity_config.cache_dir.annotation', $annotationCacheDir); diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Proxy/ServiceProxy.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Utils/ServiceLink.php similarity index 59% rename from src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Proxy/ServiceProxy.php rename to src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Utils/ServiceLink.php index 20e597830c1..a49deff77a9 100644 --- a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Proxy/ServiceProxy.php +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Utils/ServiceLink.php @@ -1,15 +1,28 @@ serviceId = $serviceId; } + /** + * @return mixed + */ public function getService() { $this->init(); @@ -25,6 +41,9 @@ public function getService() return $this->service; } + /** + * Init service to internal cache + */ protected function init() { if ($this->service === null) { diff --git a/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Utils/ServiceMethod.php b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Utils/ServiceMethod.php new file mode 100644 index 00000000000..40c539387c9 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/DependencyInjection/Utils/ServiceMethod.php @@ -0,0 +1,58 @@ +arguments = func_get_args(); + } + + /** + * @param mixed $service + */ + public function setService($service) + { + $this->service = $service; + } + + /** + * @param string $method + */ + public function setMethod($method) + { + $this->method = $method; + } + + /** + * @return mixed + */ + public function execute() + { + return call_user_func_array(array($this->service, $this->method), $this->arguments); + } + + /** + * @return mixed + */ + public function __invoke() + { + return $this->execute(); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfig.php b/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfigModel.php similarity index 81% rename from src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfig.php rename to src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfigModel.php index f3e391927cb..f9409f94e7c 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfig.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/AbstractConfigModel.php @@ -9,15 +9,8 @@ * @ORM\MappedSuperclass * @ORM\HasLifecycleCallbacks */ -abstract class AbstractConfig +abstract class AbstractConfigModel { - /** - * type of config - */ - const MODE_VIEW_DEFAULT = 'default'; - const MODE_VIEW_HIDDEN = 'hidden'; - const MODE_VIEW_READONLY = 'readonly'; - /** * @var \DateTime $created * @ORM\Column(type="datetime") @@ -37,12 +30,12 @@ abstract class AbstractConfig protected $mode; /** - * @var ConfigValue[]|ArrayCollection + * @var ConfigModelValue[]|ArrayCollection */ protected $values; /** - * @param ConfigValue[] $values + * @param ConfigModelValue[] $values * @return $this */ public function setValues($values) @@ -57,7 +50,7 @@ public function setValues($values) } /** - * @param ConfigValue $value + * @param ConfigModelValue $value * @return $this */ public function addValue($value) @@ -88,7 +81,7 @@ public function getMode() /** * @param callable $filter - * @return array|ArrayCollection|ConfigValue[] + * @return array|ArrayCollection|ConfigModelValue[] */ public function getValues(\Closure $filter = null) { @@ -98,13 +91,15 @@ public function getValues(\Closure $filter = null) /** * @param $code * @param $scope - * @return ConfigValue + * @return ConfigModelValue */ public function getValue($code, $scope) { - $values = $this->getValues(function (ConfigValue $value) use ($code, $scope) { - return ($value->getScope() == $scope && $value->getCode() == $code); - }); + $values = $this->getValues( + function (ConfigModelValue $value) use ($code, $scope) { + return ($value->getScope() == $scope && $value->getCode() == $code); + } + ); return $values->first(); } @@ -169,9 +164,9 @@ public function fromArray($scope, array $values, array $serializableValues = arr $configValue->setValue($value); } else { - $configValue = new ConfigValue($code, $scope, $value, $serializable); + $configValue = new ConfigModelValue($code, $scope, $value, $serializable); - if ($this instanceof ConfigEntity) { + if ($this instanceof EntityConfigModel) { $configValue->setEntity($this); } else { $configValue->setField($this); @@ -188,9 +183,11 @@ public function fromArray($scope, array $values, array $serializableValues = arr */ public function toArray($scope) { - $values = $this->getValues(function (ConfigValue $value) use ($scope) { - return $value->getScope() == $scope; - }); + $values = $this->getValues( + function (ConfigModelValue $value) use ($scope) { + return $value->getScope() == $scope; + } + ); $result = array(); foreach ($values as $value) { diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php index bd2fc61b449..2c1d0bb4916 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLog.php @@ -5,11 +5,11 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; -use Oro\Bundle\UserBundle\Entity\User; +use Symfony\Component\Security\Core\User\UserInterface; /** - * @ORM\Table(name="oro_config_log") - * @ORM\Entity(repositoryClass="Oro\Bundle\EntityConfigBundle\Entity\Repository\ConfigLogRepository") + * @ORM\Table(name="oro_entity_config_log") + * @ORM\Entity * @ORM\HasLifecycleCallbacks() */ class ConfigLog @@ -25,8 +25,8 @@ class ConfigLog protected $id; /** - * @var User - * @ORM\ManyToOne(targetEntity="Oro\Bundle\UserBundle\Entity\User", cascade={"persist"}) + * @var UserInterface + * @ORM\ManyToOne(targetEntity="Symfony\Component\Security\Core\User\UserInterface", cascade={"persist"}) * @ORM\JoinColumn(name="user_id", referencedColumnName="id", onDelete="CASCADE") */ protected $user; @@ -76,10 +76,10 @@ public function getLoggedAt() } /** - * @param User $user + * @param UserInterface $user * @return $this */ - public function setUser($user) + public function setUser(UserInterface $user) { $this->user = $user; @@ -87,7 +87,7 @@ public function setUser($user) } /** - * @return User + * @return UserInterface */ public function getUser() { diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLogDiff.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLogDiff.php index 72e459184c9..c1c28e37f8e 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLogDiff.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigLogDiff.php @@ -5,7 +5,7 @@ use Doctrine\ORM\Mapping as ORM; /** - * @ORM\Table(name="oro_config_log_diff") + * @ORM\Table(name="oro_entity_config_log_diff") * @ORM\Entity */ class ConfigLogDiff diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigValue.php b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigModelValue.php similarity index 85% rename from src/Oro/Bundle/EntityConfigBundle/Entity/ConfigValue.php rename to src/Oro/Bundle/EntityConfigBundle/Entity/ConfigModelValue.php index a1c8212b4f4..bb399947a01 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigValue.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigModelValue.php @@ -5,12 +5,12 @@ use Doctrine\ORM\Mapping as ORM; /** - * @ORM\Table(name="oro_config_value") + * @ORM\Table(name="oro_entity_config_value") * @ORM\Entity */ -class ConfigValue +class ConfigModelValue { - const ENTITY_NAME = 'OroEntityConfigBundle:ConfigValue'; + const ENTITY_NAME = 'OroEntityConfigBundle:ConfigModelValue'; /** * @var integer @@ -21,8 +21,8 @@ class ConfigValue protected $id; /** - * @var ConfigEntity - * @ORM\ManyToOne(targetEntity="ConfigEntity", inversedBy="values") + * @var EntityConfigModel + * @ORM\ManyToOne(targetEntity="EntityConfigModel", inversedBy="values") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="entity_id", referencedColumnName="id") * }) @@ -30,8 +30,8 @@ class ConfigValue protected $entity; /** - * @var ConfigField - * @ORM\ManyToOne(targetEntity="ConfigField", inversedBy="values") + * @var FieldConfigModel + * @ORM\ManyToOne(targetEntity="FieldConfigModel", inversedBy="values") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="field_id", referencedColumnName="id") * }) @@ -83,7 +83,7 @@ public function getId() /** * Set code * @param string $code - * @return ConfigValue + * @return ConfigModelValue */ public function setCode($code) { @@ -103,7 +103,7 @@ public function getCode() /** * @param string $scope - * @return ConfigValue + * @return ConfigModelValue */ public function setScope($scope) { @@ -123,7 +123,7 @@ public function getScope() /** * Set data * @param string $value - * @return ConfigValue + * @return ConfigModelValue */ public function setValue($value) { @@ -142,7 +142,7 @@ public function getValue() } /** - * @param ConfigEntity $entity + * @param EntityConfigModel $entity * @return $this */ public function setEntity($entity) @@ -153,7 +153,7 @@ public function setEntity($entity) } /** - * @return ConfigEntity + * @return EntityConfigModel */ public function getEntity() { @@ -161,7 +161,7 @@ public function getEntity() } /** - * @param ConfigField $field + * @param FieldConfigModel $field * @return $this */ public function setField($field) @@ -172,7 +172,7 @@ public function setField($field) } /** - * @return ConfigField + * @return FieldConfigModel */ public function getField() { diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php b/src/Oro/Bundle/EntityConfigBundle/Entity/EntityConfigModel.php similarity index 64% rename from src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php rename to src/Oro/Bundle/EntityConfigBundle/Entity/EntityConfigModel.php index 4e26aff3d8e..e1b878039d7 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigEntity.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/EntityConfigModel.php @@ -5,14 +5,16 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; +use Oro\Bundle\EntityConfigBundle\Config\ConfigModelManager; + /** - * @ORM\Table(name="oro_config_entity") + * @ORM\Table(name="oro_entity_config") * @ORM\Entity * @ORM\HasLifecycleCallbacks() */ -class ConfigEntity extends AbstractConfig +class EntityConfigModel extends AbstractConfigModel { - const ENTITY_NAME = 'OroEntityConfigBundle:ConfigEntity'; + const ENTITY_NAME = 'OroEntityConfigBundle:EntityConfigModel'; /** * @var integer @@ -23,14 +25,14 @@ class ConfigEntity extends AbstractConfig protected $id; /** - * @var ConfigValue[]|ArrayCollection - * @ORM\OneToMany(targetEntity="ConfigValue", mappedBy="entity", cascade={"all"}) + * @var ConfigModelValue[]|ArrayCollection + * @ORM\OneToMany(targetEntity="ConfigModelValue", mappedBy="entity", cascade={"all"}) */ protected $values; /** - * @var ConfigField[]|ArrayCollection - * @ORM\OneToMany(targetEntity="ConfigField", mappedBy="entity", cascade={"all"}) + * @var FieldConfigModel[]|ArrayCollection + * @ORM\OneToMany(targetEntity="FieldConfigModel", mappedBy="entity", cascade={"all"}) */ protected $fields; @@ -45,7 +47,7 @@ public function __construct($className = null) $this->className = $className; $this->fields = new ArrayCollection(); $this->values = new ArrayCollection(); - $this->mode = self::MODE_VIEW_DEFAULT; + $this->mode = ConfigModelManager::MODE_DEFAULT; } /** @@ -76,7 +78,7 @@ public function getClassName() } /** - * @param ConfigField[] $fields + * @param FieldConfigModel[] $fields * @return $this */ public function setFields($fields) @@ -87,7 +89,7 @@ public function setFields($fields) } /** - * @param ConfigField $field + * @param FieldConfigModel $field * @return $this */ public function addField($field) @@ -100,7 +102,7 @@ public function addField($field) /** * @param callable $filter - * @return ConfigField[]|ArrayCollection + * @return FieldConfigModel[]|ArrayCollection */ public function getFields(\Closure $filter = null) { @@ -108,14 +110,16 @@ public function getFields(\Closure $filter = null) } /** - * @param $code - * @return ConfigField + * @param $fieldName + * @return FieldConfigModel */ - public function getField($code) + public function getField($fieldName) { - $fields = $this->getFields(function (ConfigField $field) use ($code) { - return $field->getCode() == $code; - }); + $fields = $this->getFields( + function (FieldConfigModel $field) use ($fieldName) { + return $field->getFieldName() == $fieldName; + } + ); return $fields->first(); } diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php b/src/Oro/Bundle/EntityConfigBundle/Entity/FieldConfigModel.php similarity index 56% rename from src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php rename to src/Oro/Bundle/EntityConfigBundle/Entity/FieldConfigModel.php index d6496e21888..48cdf882897 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/ConfigField.php +++ b/src/Oro/Bundle/EntityConfigBundle/Entity/FieldConfigModel.php @@ -6,28 +6,29 @@ use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\PersistentCollection; -use Oro\Bundle\EntityConfigBundle\Entity\AbstractConfig; + +use Oro\Bundle\EntityConfigBundle\Config\ConfigModelManager; /** - * @ORM\Table(name="oro_config_field") + * @ORM\Table(name="oro_entity_config_field") * @ORM\Entity * @ORM\HasLifecycleCallbacks() */ -class ConfigField extends AbstractConfig +class FieldConfigModel extends AbstractConfigModel { - const ENTITY_NAME = 'OroEntityConfigBundle:ConfigField'; + const ENTITY_NAME = 'OroEntityConfigBundle:FieldConfigModel'; /** * @var integer - * @ORM\Column(name="id", type="integer", nullable=false) + * @ORM\Column(type="integer") * @ORM\Id * @ORM\GeneratedValue(strategy="IDENTITY") */ protected $id; /** - * @var ConfigEntity - * @ORM\ManyToOne(targetEntity="ConfigEntity", inversedBy="fields") + * @var EntityConfigModel + * @ORM\ManyToOne(targetEntity="EntityConfigModel", inversedBy="fields") * @ORM\JoinColumns({ * @ORM\JoinColumn(name="entity_id", referencedColumnName="id") * }) @@ -35,16 +36,16 @@ class ConfigField extends AbstractConfig protected $entity; /** - * @var ConfigValue[]|PersistentCollection - * @ORM\OneToMany(targetEntity="ConfigValue", mappedBy="field", cascade={"all"}) + * @var ConfigModelValue[]|PersistentCollection + * @ORM\OneToMany(targetEntity="ConfigModelValue", mappedBy="field", cascade={"all"}) */ protected $values; /** * @var string - * @ORM\Column(type="string", length=255, nullable=false) + * @ORM\Column(name="field_name", type="string", length=255) */ - protected $code; + protected $fieldName; /** * @var string @@ -52,12 +53,12 @@ class ConfigField extends AbstractConfig */ protected $type; - public function __construct($code = null, $type = null) + public function __construct($fieldName = null, $type = null) { - $this->code = $code; - $this->type = $type; - $this->values = new ArrayCollection; - $this->mode = self::MODE_VIEW_DEFAULT; + $this->type = $type; + $this->mode = ConfigModelManager::MODE_DEFAULT; + $this->values = new ArrayCollection; + $this->fieldName = $fieldName; } /** @@ -69,12 +70,12 @@ public function getId() } /** - * @param string $code + * @param string $fieldName * @return $this */ - public function setCode($code) + public function setFieldName($fieldName) { - $this->code = $code; + $this->fieldName = $fieldName; return $this; } @@ -82,9 +83,9 @@ public function setCode($code) /** * @return string */ - public function getCode() + public function getFieldName() { - return $this->code; + return $this->fieldName; } /** @@ -107,7 +108,7 @@ public function getType() } /** - * @param ConfigEntity $entity + * @param EntityConfigModel $entity * @return $this */ public function setEntity($entity) @@ -118,7 +119,7 @@ public function setEntity($entity) } /** - * @return ConfigEntity + * @return EntityConfigModel */ public function getEntity() { diff --git a/src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php b/src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php deleted file mode 100644 index 54faacd19a0..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Entity/Repository/ConfigLogRepository.php +++ /dev/null @@ -1,39 +0,0 @@ -createQueryBuilder('cl') - ->select('cld.diff as diff, cld.className as className, cld.fieldName as fieldName, cld.scope as scope') - ->addSelect('cl.id as logId') - ->addSelect('u.username as username') - ->leftJoin('cl.diffs', 'cld') - ->leftJoin('cl.user', 'u'); - - if (!$className) { - return $qb; - } - - $qb->setParameter('className', $className); - $qb->andWhere('cld.className = :className'); - - if ($fieldName) { - $qb->setParameter('fieldName', $fieldName); - $qb->andWhere('cld.fieldName = :fieldName'); - } else { - $qb->andWhere('cld.fieldName IS NULL'); - } - - if ($scope) { - $qb->setParameter('scope', $scope); - $qb->andWhere('cld.scope = :scope'); - } - - return $qb; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/Events.php b/src/Oro/Bundle/EntityConfigBundle/Event/Events.php index 3d159cdf4fe..3a82e29c4a9 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Event/Events.php +++ b/src/Oro/Bundle/EntityConfigBundle/Event/Events.php @@ -7,10 +7,7 @@ final class Events /** * Config Event Names */ - const NEW_ENTITY = 'entity_config.new.entity'; - const NEW_FIELD = 'entity_config.new.field'; - const PERSIST_CONFIG = 'entity_config.persist.config'; - const PRE_FLUSH = 'entity_config.pre.flush'; - const ON_FLUSH = 'entity_config.on.flush'; - const POST_FLUSH = 'entity_config.post.flush'; + const NEW_ENTITY_CONFIG_MODEL = 'entity_config.new.entity.config.model'; + const NEW_FIELD_CONFIG_MODEL = 'entity_config.new.field.config.model'; + const PRE_PERSIST_CONFIG = 'entity_config.persist.config'; } diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/FlushConfigEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/FlushConfigEvent.php deleted file mode 100644 index 444ea656e46..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Event/FlushConfigEvent.php +++ /dev/null @@ -1,31 +0,0 @@ -configManager = $configManager; - } - - /** - * @return ConfigManager - */ - public function getConfigManager() - { - return $this->configManager; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityConfigModelEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityConfigModelEvent.php new file mode 100644 index 00000000000..1b8d33d2131 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityConfigModelEvent.php @@ -0,0 +1,43 @@ +configModel = $configModel; + $this->configManager = $configManager; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->configModel->getClassName(); + } + + /** + * @return ConfigManager + */ + public function getConfigManager() + { + return $this->configManager; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityEvent.php deleted file mode 100644 index 62c8bb99fba..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Event/NewEntityEvent.php +++ /dev/null @@ -1,47 +0,0 @@ -className = $className; - $this->configManager = $configManager; - } - - /** - * @return EntityConfig - */ - public function getClassName() - { - return $this->className; - } - - /** - * @return ConfigManager - */ - public function getConfigManager() - { - return $this->configManager; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldConfigModelEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldConfigModelEvent.php new file mode 100644 index 00000000000..9e27fd510bf --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldConfigModelEvent.php @@ -0,0 +1,59 @@ +configModel = $configModel; + $this->configManager = $configManager; + } + + /** + * @return string + */ + public function getClassName() + { + return $this->configModel->getEntity()->getClassName(); + } + + /** + * @return string + */ + public function getFieldName() + { + return $this->configModel->getFieldName(); + } + + /** + * @return string + */ + public function getFieldType() + { + return $this->configModel->getType(); + } + + /** + * @return ConfigManager + */ + public function getConfigManager() + { + return $this->configManager; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php deleted file mode 100644 index ed319606f60..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Event/NewFieldEvent.php +++ /dev/null @@ -1,77 +0,0 @@ -className = $className; - $this->fieldName = $fieldName; - $this->fieldType = $fieldType; - $this->configManager = $configManager; - } - - /** - * @return EntityConfig - */ - public function getClassName() - { - return $this->className; - } - - /** - * @return string - */ - public function getFieldName() - { - return $this->fieldName; - } - - /** - * @return string - */ - public function getFieldType() - { - return $this->fieldType; - } - - /** - * @return ConfigManager - */ - public function getConfigManager() - { - return $this->configManager; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Event/PersistConfigEvent.php b/src/Oro/Bundle/EntityConfigBundle/Event/PersistConfigEvent.php index b4b892b72ef..3de4a50a567 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Event/PersistConfigEvent.php +++ b/src/Oro/Bundle/EntityConfigBundle/Event/PersistConfigEvent.php @@ -4,8 +4,10 @@ use Symfony\Component\EventDispatcher\Event; +use Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface; use Oro\Bundle\EntityConfigBundle\Config\ConfigInterface; -use Oro\Bundle\EntityConfigBundle\ConfigManager; + +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; class PersistConfigEvent extends Event { @@ -19,7 +21,6 @@ class PersistConfigEvent extends Event */ protected $configManager; - public function __construct(ConfigInterface $config, ConfigManager $configManager) { $this->config = $config; @@ -34,6 +35,14 @@ public function getConfig() return $this->config; } + /** + * @return ConfigIdInterface + */ + public function getConfigId() + { + return $this->config->getId(); + } + /** * @return ConfigManager */ diff --git a/src/Oro/Bundle/EntityConfigBundle/Exception/LogicException.php b/src/Oro/Bundle/EntityConfigBundle/Exception/LogicException.php new file mode 100644 index 00000000000..a72b857bc0d --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Exception/LogicException.php @@ -0,0 +1,7 @@ +getForm()->getConfig()->getOptions(); - $className = $options['class_name']; - $fieldName = isset($options['field_name']) ? $options['field_name'] : null; - $data = $event->getData(); + $options = $event->getForm()->getConfig()->getOptions(); + $configModel = $options['config_model']; + + if ($configModel instanceof FieldConfigModel) { + $className = $configModel->getEntity()->getClassName(); + $fieldName = $configModel->getFieldName(); + } else { + $fieldName = null; + $className = $configModel->getClassName(); + } + + $data = $event->getData(); foreach ($this->configManager->getProviders() as $provider) { if (isset($data[$provider->getScope()])) { - if ($fieldName) { - $config = $provider->getFieldConfig($className, $fieldName); - } else { - $config = $provider->getConfig($className); - } + $config = $provider->getConfig($className, $fieldName); + $config->setValues($data[$provider->getScope()]); - //TODO::look after a EntityConfig changes in configManager + $this->configManager->persist($config); } } - $this->configManager->flush(); + if ($event->getForm()->isValid()) { + $this->configManager->flush(); + } } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Extension/ConfigFormExtension.php b/src/Oro/Bundle/EntityConfigBundle/Form/Extension/ConfigFormExtension.php deleted file mode 100644 index 78b6b446de7..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Form/Extension/ConfigFormExtension.php +++ /dev/null @@ -1,51 +0,0 @@ -vars['attr'] = array_merge($view->vars['attr'], array('data-allowedType' => $options['allowed_type'])); - - $types = explode(',', $options['allowed_type']); - $types = array_map('trim', $types); - if (!$fieldType || !in_array($fieldType, $types)) { - $view->vars['attr']['class'] = (isset($view->vars['attr']['class']) ? $view->vars['attr']['class'] : '') . 'hide'; - } - - $view->vars['attr'] = array_merge($view->vars['attr'], array('data-fieldType' => $fieldType)); - } - - } - - /** - * {@inheritdoc} - */ - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setOptional(array( - 'allowed_type', - 'field_type' - )); - } - - /** - * {@inheritdoc} - */ - public function getExtendedType() - { - return 'form'; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigEntityType.php b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigEntityType.php deleted file mode 100644 index 1f87fb62186..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigEntityType.php +++ /dev/null @@ -1,74 +0,0 @@ -configManager = $configManager; - } - - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) - { - $className = $options['class_name']; - $fieldId = $options['entity_id']; - - $data = array( - 'id' => $fieldId, - ); - - $builder->add('id', 'hidden'); - - foreach ($this->configManager->getProviders() as $provider) { - if ($provider->getConfigContainer()->hasEntityForm()) { - $builder->add( - $provider->getScope(), - new ConfigType($provider->getConfigContainer()->getEntityItems()), - array( - 'block_config' => (array) $provider->getConfigContainer()->getEntityFormBlockConfig() - ) - ); - $data[$provider->getScope()] = $provider->getConfig($className)->getValues(); - } - } - $builder->setData($data); - - $builder->addEventSubscriber(new ConfigSubscriber($this->configManager)); - } - - /** - * {@inheritdoc} - */ - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setRequired(array('class_name', 'entity_id')); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'oro_entity_config_config_entity_type'; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigFieldType.php b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigFieldType.php deleted file mode 100644 index b76033f4204..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigFieldType.php +++ /dev/null @@ -1,112 +0,0 @@ -configManager = $configManager; - } - - /** - * {@inheritdoc} - */ - public function buildForm(FormBuilderInterface $builder, array $options) - { - $className = $options['class_name']; - $fieldName = $options['field_name']; - $fieldType = $options['field_type']; - $fieldId = $options['field_id']; - - $data = array( - 'id' => $fieldId, - ); - - $builder->add('id', 'hidden'); - - foreach ($this->configManager->getProviders() as $provider) { - if ($provider->getConfigContainer()->hasFieldForm()) { - $items = $provider->getConfigContainer()->getFieldItems(); - - $defaultValues = $provider->getConfigContainer()->getFieldDefaultValues(); - - $allowedTypes = array_map( - function ($item) use ($fieldType) { - if (isset($item['form']['options']['allowed_type'])) { - return array_map('trim', explode(',', $item['form']['options']['allowed_type'])); - } - - return false; - }, - $items - ); - - foreach ($allowedTypes as $key => $allowedType) { - if (isset($defaultValues[$key]) && is_array($allowedType) && !in_array($fieldType, $allowedType)) { - unset($defaultValues[$key]); - } - } - - foreach ($provider->getConfigContainer()->getFieldRequiredPropertyValues() as $code => $property) { - list($scope, $propertyName) = explode('.', $property['property_path']); - - if ($this->configManager->getProvider($scope)->hasFieldConfig($className, $fieldName)) { - $value = $this->configManager->getProvider($scope)->getFieldConfig($className, $fieldName)->get($propertyName); - if ($value !== null && $value != $property['value']) { - unset($items[$code]); - } - } - } - - $builder->add( - $provider->getScope(), - new ConfigType($items, $fieldType), - array('block_config' => (array) $provider->getConfigContainer()->getFieldFormBlockConfig()) - ); - - $values = $provider->getFieldConfig($className, $fieldName)->getValues(); - - $data[$provider->getScope()] = array_merge($defaultValues, $values); - } - } - - $builder->setData($data); - - $builder->addEventSubscriber(new ConfigSubscriber($this->configManager)); - } - - /** - * {@inheritdoc} - */ - public function setDefaultOptions(OptionsResolverInterface $resolver) - { - $resolver->setRequired( - array('class_name', 'field_type', 'field_name', 'field_id') - ); - } - - /** - * {@inheritdoc} - */ - public function getName() - { - return 'oro_entity_config_config_field_type'; - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigScopeType.php b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigScopeType.php new file mode 100644 index 00000000000..fe40e379d3f --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigScopeType.php @@ -0,0 +1,200 @@ +items = $items; + $this->config = $config; + $this->configModel = $configModel; + $this->configManager = $configManager; + } + + /** + * {@inheritdoc} + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + foreach ($this->items as $code => $config) { + if (isset($config['form']['type'])) { + $options = isset($config['form']['options']) ? $config['form']['options'] : array(); + + if (isset($config['options']['required_property'])) { + $property = $config['options']['required_property']; + + $propertyOnForm = false; + + if (isset($property['config_id'])) { + $configId = $property['config_id']; + + $fieldName = isset($configId['field_name']) ? $configId['field_name'] : null; + if (!$fieldName && $this->config->getId() instanceof FieldConfigId) { + $fieldName = $this->config->getId()->getFieldName(); + } + + $className = isset($configId['class_name']) + ? $configId['class_name'] + : $this->config->getId()->getClassName(); + + $scope = isset($configId['scope']) + ? $configId['scope'] + : $this->config->getId()->getScope(); + + if ($fieldName) { + $configId = new FieldConfigId($className, $scope, $fieldName); + } else { + $configId = new EntityConfigId($className, $scope); + } + + //check if requirement property isset in this form + if ($className == $this->config->getId()->getClassName()) { + if ($fieldName) { + if ($this->config->getId() instanceof FieldConfigId + && $this->config->getId()->getFieldName() == $fieldName + ) { + $propertyOnForm = true; + } + } else { + $propertyOnForm = true; + } + } + } else { + $propertyOnForm = true; + + $configId = $this->config->getId(); + } + + $requireConfig = $this->configManager->getConfig($configId); + + if ($requireConfig->get($property['code']) != $property['value']) { + if ($propertyOnForm) { + $options['attr']['class'] = isset($options['attr']['class']) + ? $options['attr']['class'] . ' hide' + : 'hide'; + } else { + continue; + } + } + + if ($propertyOnForm) { + $options['attr']['data-requireProperty'] = $configId->toString() . $property['code']; + $options['attr']['data-requireValue'] = $property['value']; + } + } + + if (isset($config['constraints'])) { + $options['constraints'] = $this->parseValidator($config['constraints']); + } + + $options['attr']['data-property_id'] = $this->config->getId()->toString() . $code; + + $builder->add($code, $config['form']['type'], $options); + } + } + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_entity_config_scope_type'; + } + + /** + * @param $name + * @param $options + * @return mixed + */ + protected function newConstraint($name, $options) + { + if (strpos($name, '\\') !== false && class_exists($name)) { + $className = (string) $name; + } else { + $className = 'Symfony\\Component\\Validator\\Constraints\\' . $name; + } + + return new $className($options); + } + + /** + * @param array $nodes + * @return array + */ + protected function parseValidator(array $nodes) + { + $values = array(); + + foreach ($nodes as $name => $childNodes) { + if (is_numeric($name) && is_array($childNodes) && count($childNodes) == 1) { + $options = current($childNodes); + + if (is_array($options)) { + $options = $this->parseValidator($options); + } + + $values[] = $this->newConstraint(key($childNodes), $options); + } else { + if (is_array($childNodes)) { + $childNodes = $this->parseValidator($childNodes); + } + + $values[$name] = $childNodes; + } + } + + return $values; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigType.php b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigType.php index 46691c1411f..e75c2dd0421 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigType.php +++ b/src/Oro/Bundle/EntityConfigBundle/Form/Type/ConfigType.php @@ -2,29 +2,32 @@ namespace Oro\Bundle\EntityConfigBundle\Form\Type; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\OptionsResolver\OptionsResolverInterface; + +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; + +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; +use Oro\Bundle\EntityConfigBundle\Provider\PropertyConfigContainer; +use Oro\Bundle\EntityConfigBundle\Form\EventListener\ConfigSubscriber; + class ConfigType extends AbstractType { /** - * @var array + * @var ConfigManager */ - protected $items; + protected $configManager; /** - * @var string + * @param ConfigManager $configManager */ - protected $fieldType; - - /** - * @param $items - * @param $fieldType - */ - public function __construct($items, $fieldType = null) + public function __construct(ConfigManager $configManager) { - $this->items = $items; - $this->fieldType = $fieldType; + $this->configManager = $configManager; } /** @@ -32,16 +35,57 @@ public function __construct($items, $fieldType = null) */ public function buildForm(FormBuilderInterface $builder, array $options) { - foreach ($this->items as $code => $config) { - if (isset($config['form']) && isset($config['form']['type'])) { - $options = isset($config['form']['options']) ? $config['form']['options'] : array(); - - if ($this->fieldType) { - $options['field_type'] = $this->fieldType; - } - $builder->add($code, $config['form']['type'], $options); + $configModel = $options['config_model']; + $data = array(); + + if ($configModel instanceof FieldConfigModel) { + $className = $configModel->getEntity()->getClassName(); + $fieldName = $configModel->getFieldName(); + $fieldType = $configModel->getType(); + $configType = PropertyConfigContainer::TYPE_FIELD; + } else { + $className = $configModel->getClassName(); + $fieldName = null; + $fieldType = null; + $configType = PropertyConfigContainer::TYPE_ENTITY; + } + + foreach ($this->configManager->getProviders() as $provider) { + if ($provider->getPropertyConfig()->hasForm($configType, $fieldType)) { + $config = $provider->getConfig($className, $fieldName); + $builder->add( + $provider->getScope(), + new ConfigScopeType( + $provider->getPropertyConfig()->getFormItems($configType, $fieldType), + $config, + $this->configManager, + $configModel + ), + array( + 'block_config' => (array) $provider->getPropertyConfig()->getFormBlockConfig($configType) + ) + ); + $data[$provider->getScope()] = $config->all(); } } + + $builder->setData($data); + + $builder->addEventSubscriber(new ConfigSubscriber($this->configManager)); + } + + /** + * {@inheritdoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setRequired(array('config_model')); + + $resolver->setAllowedTypes( + array( + 'config_model' => 'Oro\Bundle\EntityConfigBundle\Entity\AbstractConfigModel' + ) + ); } /** @@ -49,6 +93,6 @@ public function buildForm(FormBuilderInterface $builder, array $options) */ public function getName() { - return 'oro_entity_config_config_type'; + return 'oro_entity_config_type'; } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Form/Type/EntityConfigTypeInterface.php b/src/Oro/Bundle/EntityConfigBundle/Form/Type/EntityConfigTypeInterface.php deleted file mode 100644 index 6ac290f8487..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Form/Type/EntityConfigTypeInterface.php +++ /dev/null @@ -1,7 +0,0 @@ -mode = $data['mode']; + } elseif (isset($data['value'])) { + $this->mode = $data['value']; + } + + if (isset($data['routeName'])) { + $this->routeName = $data['routeName']; + } + + if (isset($data['defaultValues'])) { + $this->defaultValues = $data['defaultValues']; + } + + if (!is_array($this->defaultValues)) { + throw new AnnotationException( + sprintf( + 'Annotation "Config" parameter "defaultValues" expect "array" but "%s" given', + gettype($this->defaultValues) + ) + ); + } + + $availableMode = array( + ConfigModelManager::MODE_DEFAULT, + ConfigModelManager::MODE_READONLY + ); + + if (!in_array($this->mode, $availableMode)) { + throw new AnnotationException( + sprintf('Annotation "Config" give invalid parameter "mode" : "%s"', $this->mode) + ); + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/ConfigField.php b/src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/ConfigField.php new file mode 100644 index 00000000000..96634bf2362 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/ConfigField.php @@ -0,0 +1,51 @@ +mode = $data['mode']; + } elseif (isset($data['value'])) { + $this->mode = $data['value']; + } + if (isset($data['defaultValues'])) { + $this->defaultValues = $data['defaultValues']; + } + + if (!is_array($this->defaultValues)) { + throw new AnnotationException( + sprintf( + 'Annotation "ConfigField" parameter "defaultValues" expect "array" but "%s" given', + gettype($this->defaultValues) + ) + ); + } + + $availableMode = array( + ConfigModelManager::MODE_DEFAULT, + ConfigModelManager::MODE_HIDDEN, + ConfigModelManager::MODE_READONLY + ); + + if (!in_array($this->mode, $availableMode)) { + throw new AnnotationException( + sprintf('Annotation "ConfigField" give invalid parameter "mode" : "%s"', $this->mode) + ); + } + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/Configurable.php b/src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/Configurable.php deleted file mode 100644 index 6d7ff5bc68c..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Metadata/Annotation/Configurable.php +++ /dev/null @@ -1,43 +0,0 @@ -viewMode = $data['viewMode']; - } elseif (isset($data['value'])) { - $this->viewMode = $data['value']; - } - - if (isset($data['routeName'])) { - $this->routeName = $data['routeName']; - } - - if (isset($data['defaultValues'])) { - $this->defaultValues = $data['defaultValues']; - } - - if (!is_array($this->defaultValues)) { - throw new AnnotationException(sprintf('Annotation "Configurable" parameter "defaultValues" expect "array" give "%s"', $this->defaultValues)); - } - - if (!in_array($this->viewMode, array(AbstractConfig::MODE_VIEW_DEFAULT, AbstractConfig::MODE_VIEW_HIDDEN, AbstractConfig::MODE_VIEW_READONLY))) { - throw new AnnotationException(sprintf('Annotation "Configurable" give invalid parameter "viewMode" : "%s"', $this->viewMode)); - } - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Metadata/Driver/AnnotationDriver.php b/src/Oro/Bundle/EntityConfigBundle/Metadata/Driver/AnnotationDriver.php index 60a3ff47ea3..4b31b4f023d 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Metadata/Driver/AnnotationDriver.php +++ b/src/Oro/Bundle/EntityConfigBundle/Metadata/Driver/AnnotationDriver.php @@ -6,15 +6,19 @@ use Metadata\Driver\DriverInterface; -use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Configurable; -use Oro\Bundle\EntityConfigBundle\Metadata\ConfigClassMetadata; +use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Config; +use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\ConfigField; + +use Oro\Bundle\EntityConfigBundle\Metadata\EntityMetadata; +use Oro\Bundle\EntityConfigBundle\Metadata\FieldMetadata; class AnnotationDriver implements DriverInterface { /** * Annotation reader uses a full class pass for parsing */ - const CONFIGURABLE = 'Oro\\Bundle\\EntityConfigBundle\\Metadata\\Annotation\\Configurable'; + const ENTITY_CONFIG = 'Oro\\Bundle\\EntityConfigBundle\\Metadata\\Annotation\\Config'; + const FIELD_CONFIG = 'Oro\\Bundle\\EntityConfigBundle\\Metadata\\Annotation\\ConfigField'; /** * @var Reader @@ -34,14 +38,26 @@ public function __construct(Reader $reader) */ public function loadMetadataForClass(\ReflectionClass $class) { - /** @var Configurable $annot */ - if ($annot = $this->reader->getClassAnnotation($class, self::CONFIGURABLE)) { - $metadata = new ConfigClassMetadata($class->getName()); + /** @var Config $annotation */ + if ($annotation = $this->reader->getClassAnnotation($class, self::ENTITY_CONFIG)) { + $metadata = new EntityMetadata($class->getName()); $metadata->configurable = true; - $metadata->defaultValues = $annot->defaultValues; - $metadata->routeName = $annot->routeName; - $metadata->viewMode = $annot->viewMode; + $metadata->defaultValues = $annotation->defaultValues; + $metadata->routeName = $annotation->routeName; + $metadata->mode = $annotation->mode; + + foreach ($class->getProperties() as $property) { + $propertyMetadata = new FieldMetadata($class->getName(), $property->getName()); + + /** @var ConfigField $annotation */ + if ($annotation = $this->reader->getPropertyAnnotation($property, self::FIELD_CONFIG)) { + $propertyMetadata->defaultValues = $annotation->defaultValues; + $propertyMetadata->mode = $annotation->mode; + } + + $metadata->addPropertyMetadata($propertyMetadata); + } return $metadata; } diff --git a/src/Oro/Bundle/EntityConfigBundle/Metadata/ConfigClassMetadata.php b/src/Oro/Bundle/EntityConfigBundle/Metadata/EntityMetadata.php similarity index 70% rename from src/Oro/Bundle/EntityConfigBundle/Metadata/ConfigClassMetadata.php rename to src/Oro/Bundle/EntityConfigBundle/Metadata/EntityMetadata.php index 9d6991f88a9..cad46be5787 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Metadata/ConfigClassMetadata.php +++ b/src/Oro/Bundle/EntityConfigBundle/Metadata/EntityMetadata.php @@ -5,7 +5,7 @@ use Metadata\MergeableClassMetadata; use Metadata\MergeableInterface; -class ConfigClassMetadata extends MergeableClassMetadata +class EntityMetadata extends MergeableClassMetadata { /** * @var bool @@ -20,7 +20,7 @@ class ConfigClassMetadata extends MergeableClassMetadata /** * @var string */ - public $viewMode; + public $mode; /** * @var array @@ -34,11 +34,11 @@ public function merge(MergeableInterface $object) { parent::merge($object); - if ($object instanceof ConfigClassMetadata) { + if ($object instanceof EntityMetadata) { $this->configurable = $object->configurable; $this->defaultValues = $object->defaultValues; $this->routeName = $object->routeName; - $this->viewMode = $object->viewMode; + $this->mode = $object->mode; } } @@ -47,13 +47,15 @@ public function merge(MergeableInterface $object) */ public function serialize() { - return serialize(array( - $this->configurable, - $this->defaultValues, - $this->routeName, - $this->viewMode, - parent::serialize(), - )); + return serialize( + array( + $this->configurable, + $this->defaultValues, + $this->routeName, + $this->mode, + parent::serialize(), + ) + ); } /** @@ -65,7 +67,7 @@ public function unserialize($str) $this->configurable, $this->defaultValues, $this->routeName, - $this->viewMode, + $this->mode, $parentStr ) = unserialize($str); diff --git a/src/Oro/Bundle/EntityConfigBundle/Metadata/FieldMetadata.php b/src/Oro/Bundle/EntityConfigBundle/Metadata/FieldMetadata.php new file mode 100644 index 00000000000..11eae925448 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Metadata/FieldMetadata.php @@ -0,0 +1,46 @@ +defaultValues, + $this->mode, + parent::serialize(), + ) + ); + } + + /** + * {@inheritdoc} + */ + public function unserialize($str) + { + list( + $this->defaultValues, + $this->mode, + $parentStr + ) = unserialize($str); + + parent::unserialize($parentStr); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/OroEntityConfigBundle.php b/src/Oro/Bundle/EntityConfigBundle/OroEntityConfigBundle.php index a239d654c89..9e67021f080 100644 --- a/src/Oro/Bundle/EntityConfigBundle/OroEntityConfigBundle.php +++ b/src/Oro/Bundle/EntityConfigBundle/OroEntityConfigBundle.php @@ -2,17 +2,20 @@ namespace Oro\Bundle\EntityConfigBundle; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; -use Oro\Bundle\EntityConfigBundle\DependencyInjection\Compiler\ServiceProxyPass; +use Oro\Bundle\EntityConfigBundle\DependencyInjection\Compiler\ServiceMethodPass; +use Oro\Bundle\EntityConfigBundle\DependencyInjection\Compiler\ServiceLinkPass; use Oro\Bundle\EntityConfigBundle\DependencyInjection\Compiler\EntityConfigPass; class OroEntityConfigBundle extends Bundle { public function build(ContainerBuilder $container) { + $container->addCompilerPass(new ServiceLinkPass); + $container->addCompilerPass(new ServiceMethodPass); $container->addCompilerPass(new EntityConfigPass); - $container->addCompilerPass(new ServiceProxyPass); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProvider.php b/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProvider.php index 5401c19ee39..0825ccb4c95 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProvider.php +++ b/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProvider.php @@ -4,13 +4,16 @@ use Doctrine\ORM\PersistentCollection; +use Oro\Bundle\EntityConfigBundle\Config\Config; use Oro\Bundle\EntityConfigBundle\Config\ConfigInterface; -use Oro\Bundle\EntityConfigBundle\Config\EntityConfig; -use Oro\Bundle\EntityConfigBundle\Config\FieldConfig; -use Oro\Bundle\EntityConfigBundle\ConfigManager; +use Oro\Bundle\EntityConfigBundle\Config\Id\EntityConfigId; +use Oro\Bundle\EntityConfigBundle\Config\Id\ConfigIdInterface; +use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigId; +use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface; -use Oro\Bundle\EntityConfigBundle\DependencyInjection\EntityConfigContainer; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; use Oro\Bundle\EntityConfigBundle\Exception\RuntimeException; +use Symfony\Component\DependencyInjection\ContainerInterface; class ConfigProvider implements ConfigProviderInterface { @@ -20,9 +23,9 @@ class ConfigProvider implements ConfigProviderInterface protected $configManager; /** - * @var EntityConfigContainer + * @var PropertyConfigContainer */ - protected $configContainer; + protected $propertyConfigContainer; /** * @var string @@ -30,108 +33,154 @@ class ConfigProvider implements ConfigProviderInterface protected $scope; /** - * @param ConfigManager $configManager - * @param EntityConfigContainer $configContainer + * @param ConfigManager $configManager + * @param ContainerInterface $container + * @param string $scope + * @param array $config */ - public function __construct(ConfigManager $configManager, EntityConfigContainer $configContainer) + public function __construct(ConfigManager $configManager, ContainerInterface $container, $scope, array $config) { - $this->configManager = $configManager; - $this->configContainer = $configContainer; - $this->scope = $configContainer->getScope(); + $this->scope = $scope; + $this->configManager = $configManager; + $this->propertyConfigContainer = new PropertyConfigContainer($config, $container); } /** - * @return EntityConfigContainer + * @return PropertyConfigContainer */ - public function getConfigContainer() + public function getPropertyConfig() { - return $this->configContainer; + return $this->propertyConfigContainer; } /** - * @param $className - * @return EntityConfig + * @return ConfigManager */ - public function getConfig($className) + public function getConfigManager() { - return $this->configManager->getConfig($this->getClassName($className), $this->scope); + return $this->configManager; } /** * @param $className * @return bool */ - public function hasConfig($className) + public function isConfigurable($className) { - return $this->configManager->hasConfig($this->getClassName($className)); + return $this->configManager->isConfigurable($this->getClassName($className)); } /** - * @param $className - * @param $code - * @return FieldConfig + * @param $className + * @param null $fieldName + * @param null $fieldType + * @return ConfigIdInterface */ - public function getFieldConfig($className, $code) + public function getId($className, $fieldName = null, $fieldType = null) { - return $this->getConfig($className)->getField($code); + return $fieldName + ? new FieldConfigId($this->getClassName($className), $this->getScope(), $fieldName, $fieldType) + : new EntityConfigId($this->getClassName($className), $this->getScope()); } /** - * @param $className - * @param $code - * @return FieldConfig + * @param $className + * @param null $fieldName + * @return bool */ - public function hasFieldConfig($className, $code) + public function hasConfig($className, $fieldName = null) { - return $this->hasConfig($className) - ? $this->getConfig($className)->hasField($code) - : false; + return $this->configManager->hasConfig($this->getId($className, $fieldName)); } /** - * @param $className - * @param array $values - * @param bool $flush - * @return EntityConfig + * @param $className + * @param null $fieldName + * @return ConfigInterface */ - public function createEntityConfig($className, array $values, $flush = false) + public function getConfig($className, $fieldName = null) { - $className = $this->getClassName($className); - $entityConfig = new EntityConfig($className, $this->scope); + return $this->configManager->getConfig($this->getId($className, $fieldName)); + } - foreach ($values as $key => $value) { - $entityConfig->set($key, $value); + /** + * @param ConfigIdInterface $configId + * @return ConfigInterface + */ + public function getConfigById(ConfigIdInterface $configId) + { + return $this->configManager->getConfig($configId); + } + + /** + * @param ConfigIdInterface $configId + * @param array $values + * @return Config + */ + public function createConfig(ConfigIdInterface $configId, array $values) + { + $config = new Config($configId); + if ($configId instanceof FieldConfigIdInterface) { + $type = PropertyConfigContainer::TYPE_FIELD; + $defaultValues = $this->getPropertyConfig()->getDefaultValues($type, $configId->getFieldType()); + } else { + $type = PropertyConfigContainer::TYPE_ENTITY; + $defaultValues = $this->getPropertyConfig()->getDefaultValues($type); } - $this->configManager->persist($entityConfig); + $values = array_merge($defaultValues, $values); - if ($flush) { - $this->configManager->flush(); + foreach ($values as $key => $value) { + $config->set($key, $value); } + + $this->merge($config); + + return $config; } /** - * @param $className - * @param $code - * @param $type - * @param array $values - * @param bool $flush - * @return FieldConfig + * @param null $className + * @return array|ConfigIdInterface[] */ - public function createFieldConfig($className, $code, $type, array $values = array(), $flush = false) + public function getIds($className = null) { - $className = $this->getClassName($className); - $fieldConfig = new FieldConfig($className, $code, $type, $this->scope); + return $this->configManager->getIds($this->getScope(), $className); + } - foreach ($values as $key => $value) { - $fieldConfig->set($key, $value); + /** + * @param null $className + * @return array|ConfigInterface[] + */ + public function getConfigs($className = null) + { + $result = array(); + + foreach ($this->getIds($className) as $configId) { + $result[] = $this->getConfigById($configId); } - $this->configManager->persist($fieldConfig); + return $result; + } - if ($flush) { - $this->configManager->flush(); - } + /** + * @param callable $map + * @param null $className + * @return array|ConfigInterface[] + */ + public function map(\Closure $map, $className = null) + { + return array_map($map, $this->getConfigs($className)); + } + + /** + * @param callable $filter + * @param null $className + * @return array|ConfigInterface[] + */ + public function filter(\Closure $filter, $className = null) + { + return array_filter($this->getConfigs($className), $filter); } /** @@ -152,12 +201,23 @@ public function getClassName($entity) } if (!is_string($className)) { - throw new RuntimeException('AbstractAdvancedConfigProvider::getClassName expects Object, PersistentCollection array of entities or string'); + throw new RuntimeException( + 'ConfigProvider::getClassName expects Object, PersistentCollection array of entities or string' + ); } return $className; } + /** + * @param $className + * @param null $fieldName + */ + public function clearCache($className, $fieldName = null) + { + $this->configManager->clearCache($this->getId($className, $fieldName)); + } + /** * @param ConfigInterface $config */ @@ -166,6 +226,14 @@ public function persist(ConfigInterface $config) $this->configManager->persist($config); } + /** + * @param ConfigInterface $config + */ + public function merge(ConfigInterface $config) + { + $this->configManager->merge($config); + } + /** * Flush configs */ diff --git a/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProviderBag.php b/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProviderBag.php new file mode 100644 index 00000000000..f4c555d57bb --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Provider/ConfigProviderBag.php @@ -0,0 +1,60 @@ +providers = new ArrayCollection(); + } + + /** + * @return ConfigProvider[]|ArrayCollection + */ + public function getProviders() + { + return $this->providers; + } + + public function testAction() + { + return array('hi', 'haha'); + } + + /** + * @param ConfigProvider $provider + * @return $this + */ + public function addProvider(ConfigProvider $provider) + { + $this->providers->set($provider->getScope(), $provider); + + return $this; + } + + /** + * @param $scope + * @return ConfigProvider + */ + public function getProvider($scope) + { + return $this->providers->get($scope); + } + + /** + * @param $scope + * @return bool + */ + public function hasProvider($scope) + { + return $this->providers->containsKey($scope); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Provider/PropertyConfigContainer.php b/src/Oro/Bundle/EntityConfigBundle/Provider/PropertyConfigContainer.php new file mode 100644 index 00000000000..1651cbf9bf0 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Provider/PropertyConfigContainer.php @@ -0,0 +1,332 @@ +config = $config; + $this->container = $container; + } + + /** + * @return array + */ + public function getConfig() + { + return $this->config; + } + + /** + * @param $config + */ + public function setConfig($config) + { + $this->config = $config; + } + + /** + * @param string $type + * @return array + */ + public function getItems($type = self::TYPE_ENTITY) + { + $type = $this->getConfigType($type); + + $items = array(); + if (isset($this->config[$type]) && isset($this->config[$type]['items'])) { + $items = $this->config[$type]['items']; + } + + return $items; + } + + /** + * @param string|ConfigIdInterface $type + * @param null $fieldType + * @return array + */ + public function getDefaultValues($type = self::TYPE_ENTITY, $fieldType = null) + { + $type = $this->getConfigType($type); + + $result = array(); + foreach ($this->getItems($type) as $code => $item) { + if (isset($item['options']['default_value']) + && ((!$fieldType || !isset($item['options']['allowed_type']) + || in_array($fieldType, $item['options']['allowed_type'])) + ) + ) { + $result[$code] = $item['options']['default_value']; + } + } + + return $result; + } + + /** + * @param string $type + * @return array + */ + public function getInternalValues($type = self::TYPE_ENTITY) + { + $type = $this->getConfigType($type); + + $result = array(); + foreach ($this->getItems($type) as $code => $item) { + if (isset($item['options']['internal']) && $item['options']['internal']) { + $result[$code] = 0; + } + } + + return $result; + } + + /** + * @param string $type + * @return array + */ + public function getSerializableValues($type = self::TYPE_ENTITY) + { + $type = $this->getConfigType($type); + + $result = array(); + foreach ($this->getItems($type) as $code => $item) { + if (isset($item['options']['serializable'])) { + $result[$code] = (bool)$item['options']['serializable']; + } + } + + return $result; + } + + /** + * @param string $type + * @param null $fieldType + * @return bool + */ + public function hasForm($type = self::TYPE_ENTITY, $fieldType = null) + { + $type = $this->getConfigType($type); + + return (boolean)$this->getFormItems($type, $fieldType); + } + + /** + * @param string $type + * @param null $fieldType + * @return bool + */ + public function getFormItems($type = self::TYPE_ENTITY, $fieldType = null) + { + $type = $this->getConfigType($type); + + return array_filter( + $this->getItems($type), + function ($item) use ($fieldType) { + if (!isset($item['form']) || !isset($item['form']['type'])) { + return false; + } + + if ($fieldType + && isset($item['options']['allowed_type']) + && !in_array($fieldType, $item['options']['allowed_type']) + ) { + return false; + } + + return true; + } + ); + } + + /** + * @param string $type + * @return array + */ + public function getFormConfig($type = self::TYPE_ENTITY) + { + $type = $this->getConfigType($type); + + $fieldFormConfig = array(); + if (isset($this->config[$type]) && isset($this->config[$type]['form'])) { + $fieldFormConfig = $this->config[$type]['form']; + } + + return $fieldFormConfig; + } + + /** + * @param string $type + * @return array + */ + public function getFormBlockConfig($type = self::TYPE_ENTITY) + { + $type = $this->getConfigType($type); + + $entityFormBlockConfig = null; + if (isset($this->config[$type]) + && isset($this->config[$type]['form']) + && isset($this->config[$type]['form']['block_config']) + ) { + $entityFormBlockConfig = $this->config[$type]['form']['block_config']; + } + + return $entityFormBlockConfig; + } + + /** + * @param string $type + * @return array + */ + public function getGridActions($type = self::TYPE_ENTITY) + { + $type = $this->getConfigType($type); + + $entityGridActions = array(); + if (isset($this->config[$type]) && isset($this->config[$type]['grid_action'])) { + $entityGridActions = $this->config[$type]['grid_action']; + } + + return $entityGridActions; + } + + /** + * @param string $type + * @return array + */ + public function getUpdateActionFilter($type = self::TYPE_ENTITY) + { + $type = $this->getConfigType($type); + + $entityGridActions = null; + if (isset($this->config[$type]) && isset($this->config[$type]['update_filter'])) { + $entityGridActions = $this->config[$type]['update_filter']; + } + + return $entityGridActions; + } + + /** + * @param string $type + * @return array + */ + public function getLayoutActions($type = self::TYPE_ENTITY) + { + $type = $this->getConfigType($type); + + $entityLayoutActions = array(); + if (isset($this->config[$type]) && isset($this->config[$type]['layout_action'])) { + $entityLayoutActions = $this->config[$type]['layout_action']; + } + + return $entityLayoutActions; + } + + /** + * @param string $type + * @return array + */ + public function getRequiredPropertyValues($type = self::TYPE_ENTITY) + { + $type = $this->getConfigType($type); + + $result = array(); + foreach ($this->getItems($type) as $code => $item) { + if (isset($item['options']['required_property'])) { + $result[$code] = $item['options']['required_property']; + } + } + + return $result; + } + + /** + * @param $type + * @return string + */ + protected function getConfigType($type) + { + if ($type instanceof ConfigIdInterface) { + return $type instanceof FieldConfigIdInterface + ? PropertyConfigContainer::TYPE_FIELD + : PropertyConfigContainer::TYPE_ENTITY; + } + + return $type; + } + + /** + * @return array + */ + public function getFieldLayoutActions() + { + $fieldLayoutActions = array(); + if (isset($this->config['field']) && isset($this->config['field']['layout_action'])) { + $fieldLayoutActions = $this->config['field']['layout_action']; + } + + return $fieldLayoutActions; + } + + /** + * @param $config + * @return array|string + */ + public function initConfig($config) + { + if (is_array($config)) { + $result = array(); + foreach ($config as $key => $value) { + $result[$key] = is_array($value) ? $this->initConfig($value) : $this->initParameter($value); + } + } else { + $result = $this->initParameter($config); + } + + return $result; + } + + /** + * @param $parameter + * @return mixed + */ + protected function initParameter($parameter) + { + if ($this->container->has($parameter)) { + $callableService = $this->container->get($parameter); + + return call_user_func($callableService); + } + + return $parameter; + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/README.md b/src/Oro/Bundle/EntityConfigBundle/README.md index e5920c0a180..d1db4014ca5 100644 --- a/src/Oro/Bundle/EntityConfigBundle/README.md +++ b/src/Oro/Bundle/EntityConfigBundle/README.md @@ -1,2 +1,90 @@ -OroEntityConfigBundle -======================== +EntityConfigBundle +================== +- Provide functionality to manage config for some resource +- Config it is meta info about resources (Config is Metadata) +- backend configuration and user interface configuration + +Config Parts +------------ +- Config - it is key-value storage +- ConfigId - resource Id it is identifier for some resources(Entity, Field) +- ConfigManager - config mananger +- ConfigProvider - get config form configManger filtred by scope add has helpfull function to manage + +Start working +------------- +add entity_config.yml file to the "Resource" folder of bundle +``` +oro_entity_config: + extend: #scope name + entity: #entities property + owner: + options: + priority: 40 + internal: true + default_value: 'System' + grid: + type: string + label: 'Type' + filter_type: oro_grid_orm_string + required: true + sortable: true + filterable: true + show_filter: true + form: + type: text + block: entity + options: + read_only: true + required: false + label: 'Type' +``` + +Use in Code +----------- +You manage your config(scope) through ConfigProvider +Config provider it is a service with name "oro_entity_config.provider" + scope + +``` +/** @var ConfigProvider $configProvider */ +$configProvider = $this->get('oro_entity_config.provider.extend'); +``` + +Provider function +----------------- +- isConfigurable($className) +- getId($className, $fieldName = null) +- hasConfig($className, $fieldName = null) +- getConfig($className, $fieldName = null) +- getConfigById($configid) +- createConfig($configId, array $values) +- getIds($className = null) +- getConfigs($className = null) +- map(\Closure $map, $className = null) +- filter(\Closure $map, $className = null) +- getClassName($entity/PersistColection/$className) +- clearCache($className, $fieldName = null) +- persist($config) +- merge($config) +- flush() + +Config function +----------------- +- getId() +- get($code, $strict = false) +- set($code, $value) +- has($code)cd bap +- is($code) +- all(\Closure $filter = null) +- public function setValues($values) + +ConfigManager function +---------------------- +- getConfigChangeSet($config) + +Events +------ +- Events::NEW_ENTITY_CONFIG_MODEL +- Events::NEW_FIELD_CONFIG_MODEL +- Events::PRE_PERSIST_CONFIG + diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/EntityConfigBundle/Resources/config/datagrid.yml index fb9ded6bb16..1c5a5aeb363 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/config/datagrid.yml @@ -12,7 +12,7 @@ services: tags: - name: oro_grid.datagrid.manager datagrid_name: entity - entity_name: Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity + entity_name: Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel entity_hint: entity route_name: oro_entityconfig_index query_entity_alias: ce @@ -24,10 +24,11 @@ services: tags: - name: oro_grid.datagrid.manager datagrid_name: field - entity_name: Oro\Bundle\EntityConfigBundle\Entity\ConfigField + entity_name: Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel entity_hint: fields route_name: oro_entityconfig_fields query_entity_alias: cf + identifier_field: id oro_entity_config.audit_datagrid.manager: class: %oro_entity_config.audit_datagrid.manager.class% @@ -37,7 +38,7 @@ services: - name: oro_grid.datagrid.manager datagrid_name: entity_audit_log entity_name: Oro\Bundle\EntityConfigBundle\Entity\ConfigLog - entity_hint: oro_entityconfig_diff + entity_hint: history route_name: oro_entityconfig_audit query_entity_alias: log @@ -49,6 +50,6 @@ services: - name: oro_grid.datagrid.manager datagrid_name: entity_audit_field_log entity_name: Oro\Bundle\EntityConfigBundle\Entity\ConfigLog - entity_hint: oro_entityconfig_field_diff + entity_hint: history route_name: oro_entityconfig_audit_field query_entity_alias: log diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/config/form_type.yml b/src/Oro/Bundle/EntityConfigBundle/Resources/config/form_type.yml index 6e3c8c379d5..398cdf18e58 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/config/form_type.yml +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/config/form_type.yml @@ -1,8 +1,9 @@ parameters: - oro_entity_config.extension.config.class: Oro\Bundle\EntityConfigBundle\Form\Extension\ConfigFormExtension + oro_entity_config.type.config.class: Oro\Bundle\EntityConfigBundle\Form\Type\ConfigType services: - oro_entity_config.extension.config: - class: %oro_entity_config.extension.config.class% + oro_entity_config.type.config: + class: %oro_entity_config.type.config.class% + arguments: [@oro_entity_config.config_manager] tags: - - { name: form.type_extension, alias: form } + - { name: form.type, alias: oro_entity_config_type } \ No newline at end of file diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/config/navigation.yml b/src/Oro/Bundle/EntityConfigBundle/Resources/config/navigation.yml index de93eaf7865..615ea636591 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/config/navigation.yml +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/config/navigation.yml @@ -17,3 +17,4 @@ oro_titles: oro_entityconfig_index: ~ oro_entityconfig_view: %%entityName%% oro_entityconfig_update: %%entityName%% + oro_entityconfig_field_update: %%entityName%% diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/config/services.yml b/src/Oro/Bundle/EntityConfigBundle/Resources/config/services.yml index 228c9d859c5..40442fa31c3 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/config/services.yml @@ -1,46 +1,47 @@ parameters: - oro_entity_config.config_manager.class: Oro\Bundle\EntityConfigBundle\ConfigManager + oro_entity_config.config_manager.class: Oro\Bundle\EntityConfigBundle\Config\ConfigManager + oro_entity_config.cache.cache.class: Oro\Bundle\EntityConfigBundle\Config\ConfigCache + oro_entity_config.provider_bag.class: Oro\Bundle\EntityConfigBundle\Provider\ConfigProviderBag oro_entity_config.cache.file_cache.class: Oro\Bundle\EntityConfigBundle\Cache\FileCache oro_entity_config.command.setup.class: Oro\Bundle\EntityConfigBundle\Command\SetupCommand - oro_entity_config.form.type.config_entity.class: Oro\Bundle\EntityConfigBundle\Form\Type\ConfigEntityType - oro_entity_config.form.type.config_field.class: Oro\Bundle\EntityConfigBundle\Form\Type\ConfigFieldType services: - oro_entity_config.proxy.entity_manager: + oro_entity_config.link.entity_manager: tags: - - { name: oro_entity_config.proxy, service: doctrine.orm.default_entity_manager} - oro_entity_config.proxy.security_context: + - { name: oro_service_link, service: doctrine.orm.default_entity_manager } + + oro_entity_config.link.security_context: + tags: + - { name: oro_service_link, service: security.context } + + oro_entity_config.link.provider_bag: tags: - - { name: oro_entity_config.proxy, service: security.context} + - { name: oro_service_link, service: oro_entity_config.provider_bag } oro_entity_config.config_manager: class: %oro_entity_config.config_manager.class% arguments: - @oro_entity_config.metadata.annotation_metadata_factory - @event_dispatcher - - @oro_entity_config.proxy.entity_manager - - @oro_entity_config.proxy.security_context + - @oro_entity_config.link.provider_bag + - @oro_entity_config.link.entity_manager + - @oro_entity_config.link.security_context calls: - - [setCache, [@oro_entity_config.cache.file_cache]] + - [setCache, [@oro_entity_config.cache.cache]] + + oro_entity_config.provider_bag: + class: %oro_entity_config.provider_bag.class% + + oro_entity_config.cache.cache: + class: %oro_entity_config.cache.cache.class% + arguments: [@oro_entity_config.cache.file_cache] oro_entity_config.cache.file_cache: - class: %oro_entity_config.cache.file_cache.class% - arguments: [%oro_entity_config.cache_dir.config%] - public: false + parent: oro.cache.abstract + calls: + - [setNamespace, ['oro_entity_config.cache']] oro_entity_config.command: class: %oro_entity_config.command.setup.class% calls: - [setContainer, [@service_container]] - - oro_entity_config.form.type.config_entity: - class: %oro_entity_config.form.type.config_entity.class% - arguments: [@oro_entity_config.config_manager] - tags: - - { name: form.type, alias: oro_entity_config_config_entity_type } - - oro_entity_config.form.type.config_field: - class: %oro_entity_config.form.type.config_field.class% - arguments: [@oro_entity_config.config_manager] - tags: - - { name: form.type, alias: oro_entity_config_config_field_type } diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/config/validation.yml b/src/Oro/Bundle/EntityConfigBundle/Resources/config/validation.yml new file mode 100644 index 00000000000..f2e3bc46153 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/config/validation.yml @@ -0,0 +1,27 @@ +Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: [fieldName, entity] + properties: + fieldName: + - NotBlank: ~ + - Regex: "/^[a-zA-Z0-9_]*$/i" + - Length: + min: 2 + max: 50 + type: + - NotBlank: ~ + - Choice: + choices: [string, integer, smallint, bigint, boolean, decimal, date, time, datetime, text, float] + message: Choose a valid Data Type. + +Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel: + constraints: + - Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity: [className] + properties: + className: + - NotBlank: ~ + - Regex: "/^[a-zA-Z0-9_]*$/i" + - Length: + min: 5 + max: 50 + diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/doc/config_provider.md b/src/Oro/Bundle/EntityConfigBundle/Resources/doc/config_provider.md index 825c8685c2c..8ac532598c5 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/doc/config_provider.md +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/doc/config_provider.md @@ -19,22 +19,22 @@ Usage in code (any Bundle): ConfigProvider methods: - hasConfig({Entity class name}) : checks if entity has config - getConfig({Entity class name}) : return configuration ( EntityConfig(AbstractConfig) instance ) + isConfigurable({Entity class name}) : checks if entity has config + getConfig({Entity class name}) : return configuration ( EntityConfig(Config) instance ) hasFieldConfig({Entity class name}, {Field code}) : checks if field of entity has config - getFieldConfig({Entity class name}, {Field code}) : return configuration for specified field ( FieldConfig(AbstractConfig) instance ) + getFieldConfig({Entity class name}, {Field code}) : return configuration for specified field ( FieldConfig(Config) instance ) - AbstractConfig->is({parameter}) : check if parameter exists or equal to TRUE, return boolean - AbstractConfig->has({parameter}) : check if parameter exists, return boolean + Config->is({parameter}) : check if parameter exists or equal to TRUE, return boolean + Config->has({parameter}) : check if parameter exists, return boolean - AbstractConfig->get({parameter}, {strict = FALSE}) : return parameters + Config->get({parameter}, {strict = FALSE}) : return parameters - if strict == TRUE and parameters NOT exists will be Exception - if strict == FALSE and parameters NOT exists will return NULL - AbstractConfig->set({parameter}, {value}) : set parameter and return AbstractConfig + Config->set({parameter}, {value}) : set parameter and return Config Simple usage example: - if ($entityAuditProvider->hasConfig(get_class($entity))) { + if ($entityAuditProvider->isConfigurable(get_class($entity))) { $audit_enabled = $entityAuditProvider->getConfig(get_class($entity))->is('auditable'); } diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/audit.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/audit.html.twig index adb1e357d01..d8d99939897 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/audit.html.twig +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/audit.html.twig @@ -1,14 +1,16 @@ -{% set gridId = "entity-grid-" ~ random() %} +
    + {% set gridId = "entity-grid-" ~ random() %} -{% block head_script %} - {% include 'OroGridBundle:Include:javascript.html.twig' with {'datagridView': datagrid, 'selector': '#' ~ gridId} %} - {% include 'OroGridBundle:Include:stylesheet.html.twig' %} -{% endblock %} + {% block head_script %} + {% include 'OroGridBundle:Include:javascript.html.twig' with {'datagridView': datagrid, 'selector': '#' ~ gridId} %} + {% include 'OroGridBundle:Include:stylesheet.html.twig' %} + {% endblock %} -{% block page_container %} -
    - {% block content %} -
    - {% endblock %} -
    -{% endblock %} + {% block page_container %} +
    + {% block content %} +
    + {% endblock %} +
    + {% endblock %} +
    diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/data.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/data.html.twig index 53dd9d28250..e09ac64ebad 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/data.html.twig +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Audit/data.html.twig @@ -1,10 +1,6 @@
      {% for val in value %} - {% if is_entity %} - {% set items = config_manager.getProvider(val.scope).getConfigContainer().getEntityItems() %} - {% else %} - {% set items = config_manager.getProvider(val.scope).getConfigContainer().getFieldItems() %} - {% endif %} + {% set items = config_manager.getProvider(val.scope).getPropertyConfig().getItems(is_entity ? 'entity' : 'type') %} {% for key, data in val.diff if (is_entity and val.fieldName() == null) or (is_entity == false and val.fieldName() == field_name) %} {% if items[key]['form']['options']['label'] is defined %} diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/fieldUpdate.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/fieldUpdate.html.twig index 3bd619383c5..04d6b3ece9b 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/fieldUpdate.html.twig +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/fieldUpdate.html.twig @@ -2,13 +2,12 @@ {% form_theme form with 'OroUIBundle:Form:fields.html.twig' %} {% set title = 'Update Entity' %} -{% oro_title_set({params : {"%username%": 'User' }}) %} - -{% set formAction = path('oro_entityconfig_field_update', {id: field.id}) %} +{% oro_title_set({params : {"%entityName%": entity_config.get('label')|default('N/A') }}) %} {% set entityClass = field.entity.className|replace('\\', '_') %} {% set audit_title = entity_config.get('label') %} {% set audit_path = 'oro_entityconfig_audit_field' %} +{% set audit_entity_id = field.id %} {% block navButtons %} {{ UI.button({'path' : path('oro_entityconfig_view', {id: field.entity.id}), 'title' : 'Cancel', 'label' : 'Cancel'}) }} @@ -22,7 +21,7 @@ 'entity' : 'entity', 'indexPath' : path('oro_entityconfig_index'), 'indexLabel' : 'Entities', - 'entityTitle' : field.id ? field_config.get('label')|default(field.code|capitalize) : 'New Field'|trans, + 'entityTitle' : field.id ? field_config.get('label')|default(field.fieldName|capitalize) : 'New Field'|trans, 'additional' : [ { 'indexPath' : path('oro_entityconfig_view', {id: field.entity.id}), diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/propertyLabel.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/propertyLabel.html.twig new file mode 100644 index 00000000000..ed42a1dd7f8 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/propertyLabel.html.twig @@ -0,0 +1,5 @@ +{{ value }} +{% if record.getValue('icon') is defined %} + +{% endif %} + diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/update.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/update.html.twig index 6489dc833d4..35ac5aeb5e4 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/update.html.twig +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/update.html.twig @@ -36,8 +36,7 @@ {% set dataBlocks = form_data_blocks(form) %} {% set data = { 'formErrors': form_errors(form)? form_errors(form) : null, - 'dataBlocks': dataBlocks, - 'hiddenData': form_rest(form) + 'dataBlocks': dataBlocks }%} {{ parent() }} diff --git a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/view.html.twig b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/view.html.twig index 43470774cae..f609cde1a96 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/view.html.twig +++ b/src/Oro/Bundle/EntityConfigBundle/Resources/views/Config/view.html.twig @@ -58,24 +58,16 @@ {% endfor %} {% set general_fields = [ - UI.attibuteRow('Icon', entity_config.get('icon')), + UI.attibuteRow('Name', entity_name), + UI.renderAttribute('Icon', ['

      (',entity_config.get('icon'),')

      ']|join), UI.attibuteRow('Label', entity_config.get('label')), UI.attibuteRow('Plural Label', entity_config.get('plural_label')), UI.attibuteRow('Type', entity_extend.get('owner')), UI.attibuteRow('Description', entity_config.get('description')), - - UI.attibuteRow('Name', entity_name), + UI.attibuteRow('Ownership Type', entity_ownership.get('owner_type')), UI.attibuteRow('Module', module_name), ]%} - {#{% for key,property in entity_config.getValues %}#} - {#{% set general_fields = general_fields|merge([UI.attibuteRow(key|replace('_',' ')|capitalize, property)])%}#} - {#{% endfor %}#} - {#{% set general_fields = general_fields|merge([UI.attibuteRow('Type', entity_extend.get('type'))])%}#} - {#{% for key,property in entity_extend.getValues %}#} - {#{% set general_fields = general_fields|merge([UI.attibuteRow(key|replace('_',' ')|capitalize, property)])%}#} - {#{% endfor %}#} - {% set data = { 'dataBlocks': [ { diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php deleted file mode 100644 index 3e2b8bcb46c..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/AbstractEntityManagerTest.php +++ /dev/null @@ -1,40 +0,0 @@ -em = $this->_getTestEntityManager(); - $this->em->getConfiguration()->setEntityNamespaces(array( - 'OroEntityConfigBundle' => 'Oro\\Bundle\\EntityConfigBundle\\Entity', - 'Fixture' => 'Oro\\Bundle\\EntityConfigBundle\\Tests\\Unit\\Fixture' - )); - $this->em->getConfiguration()->setMetadataDriverImpl($metadataDriver); - - $schema = $this->getMockBuilder('Doctrine\Tests\Mocks\SchemaManagerMock') - ->disableOriginalConstructor() - ->getMock(); - - $schema->expects($this->any())->method('listTableNames')->will($this->returnValue(array('oro_config_entity'))); - $this->em->getConnection()->getDriver()->setSchemaManager($schema); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Audit/AuditManagerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Audit/AuditManagerTest.php new file mode 100644 index 00000000000..bd498897a1f --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Audit/AuditManagerTest.php @@ -0,0 +1,92 @@ +getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + $user = $this->getMockBuilder('Oro\Bundle\UserBundle\Entity\User') + ->disableOriginalConstructor() + ->getMock(); + + $token = $this->getMockForAbstractClass('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token->expects($this->any())->method('getUser')->will($this->returnValue($user)); + + $securityContext = $this->getMockForAbstractClass('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext->expects($this->any())->method('getToken')->will($this->returnValue($token)); + + $securityProxy = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\DependencyInjection\Utils\ServiceLink') + ->disableOriginalConstructor() + ->getMock(); + $securityProxy->expects($this->any())->method('getService')->will($this->returnValue($securityContext)); + + $configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $provider = new ConfigProvider($configManager, new Container(), 'testScope', array()); + + $configManager->expects($this->any())->method('getEntityManager')->will($this->returnValue($em)); + + $configManager->expects($this->any())->method('getUpdateConfig')->will( + $this->returnValue( + array( + new Config(new EntityConfigId('testClass', 'testScope')), + new Config(new FieldConfigId('testClass', 'testScope', 'testField', 'string')), + ) + ) + ); + $configManager->expects($this->any())->method('getConfigChangeSet')->will($this->returnValue(array('key' => 'value'))); + $configManager->expects($this->any())->method('getProvider')->will($this->returnValue($provider)); + + $this->auditManager = new AuditManager($configManager, $securityProxy); + } + + protected function tearDown() + { + $this->auditManager = null; + } + + public function testLog() + { + $this->auditManager->log(); + } + + public function testLogWithoutUser() + { + $securityContext = $this->getMockForAbstractClass('Symfony\Component\Security\Core\SecurityContextInterface'); + $securityContext->expects($this->any())->method('getToken'); + + $securityProxy = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\DependencyInjection\Utils\ServiceLink') + ->disableOriginalConstructor() + ->getMock(); + $securityProxy->expects($this->any())->method('getService')->will($this->returnValue($securityContext)); + + $configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $auditManager = new AuditManager($configManager, $securityProxy); + + $auditManager->log(); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php deleted file mode 100644 index cf298811e9b..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Cache/FileCacheTest.php +++ /dev/null @@ -1,62 +0,0 @@ -cacheDir = sys_get_temp_dir() . '/__phpunit__config_file_cache'; - mkdir($this->cacheDir); - - $this->testConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'test'); - $this->fileCache = new FileCache($this->cacheDir); - } - - protected function tearDown() - { - rmdir($this->cacheDir); - } - - public function testCache() - { - $result = $this->fileCache->loadConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); - $this->assertEquals(null, $result); - - $this->fileCache->putConfigInCache($this->testConfig); - - $result = $this->fileCache->loadConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); - $this->assertEquals($this->testConfig, $result); - - $this->fileCache->removeConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); - $result = $this->fileCache->loadConfigFromCache(ConfigManagerTest::DEMO_ENTITY, 'test'); - $this->assertEquals(null, $result); - } - - public function testExceptionNotFoundDirectory() - { - $cacheDir = '/__phpunit__config_file_cache_wrong'; - $this->setExpectedException('\InvalidArgumentException', sprintf('The directory "%s" does not exist.', $cacheDir)); - $this->fileCache = new FileCache($cacheDir); - } - - public function testExceptionNotWritableDirectory() - { - $cacheDir = '/'; - $this->setExpectedException('\InvalidArgumentException', sprintf('The directory "%s" is not writable.', $cacheDir)); - $this->fileCache = new FileCache($cacheDir); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php new file mode 100644 index 00000000000..c4cb1a3e961 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/ConfigTest.php @@ -0,0 +1,63 @@ +config = new Config(new EntityConfigId('testClass', 'testScope')); + } + + public function testCloneConfig() + { + $values = array('firstKey' => 'firstValue', 'secondKey' => new \stdClass()); + $this->config->setValues($values); + + $clone = clone $this->config; + + $this->assertTrue($this->config == $clone); + $this->assertFalse($this->config === $clone); + + } + + public function testValueConfig() + { + $values = array('firstKey' => 'firstValue', 'secondKey' => 'secondValue', 'fourthKey' => new \stdClass()); + $this->config->setValues($values); + + $this->assertEquals($values, $this->config->all()); + $this->assertEquals( + array('firstKey' => 'firstValue'), + $this->config->all( + function ($value) { + return $value == 'firstValue'; + } + ) + ); + + $this->assertEquals('firstValue', $this->config->get('firstKey')); + $this->assertEquals('secondValue', $this->config->get('secondKey')); + + $this->assertEquals(true, $this->config->is('secondKey')); + + $this->assertEquals(true, $this->config->has('secondKey')); + $this->assertEquals(false, $this->config->has('thirdKey')); + + $this->assertEquals(null, $this->config->get('thirdKey')); + + $this->config->set('secondKey', 'secondValue2'); + $this->assertEquals('secondValue2', $this->config->get('secondKey')); + + $this->setExpectedException('Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); + $this->config->get('thirdKey', true); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php deleted file mode 100644 index 4b6596695de..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/EntityConfigTest.php +++ /dev/null @@ -1,43 +0,0 @@ -entityConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'testScope'); - } - - public function testSetClassName() - { - $this->entityConfig->setClassName('testClass'); - $this->assertEquals('testClass', $this->entityConfig->getClassName()); - } - - public function testField() - { - $fieldConfig = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', 'testScope'); - - $this->entityConfig->addField($fieldConfig); - - $this->assertEquals(true, $this->entityConfig->hasField('testField')); - $this->assertEquals($fieldConfig, $this->entityConfig->getField('testField')); - - $fieldConfig2 = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField2', 'string', 'testScope'); - - $this->entityConfig->setFields(array($fieldConfig2)); - - $this->assertEquals(true, $this->entityConfig->hasField('testField2')); - $this->assertEquals($fieldConfig2, $this->entityConfig->getField('testField2')); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php deleted file mode 100644 index 54c719096a6..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/FieldConfigTest.php +++ /dev/null @@ -1,67 +0,0 @@ -fieldConfig = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', 'testScope'); - } - - public function testGetConfig() - { - $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->fieldConfig->getClassName()); - $this->assertEquals('testField', $this->fieldConfig->getCode()); - $this->assertEquals('string', $this->fieldConfig->getType()); - $this->assertEquals('testScope', $this->fieldConfig->getScope()); - } - - public function testSetConfig() - { - $this->fieldConfig->setClassName('testClass'); - $this->fieldConfig->setCode('testField2'); - $this->fieldConfig->setType('datetime'); - - $this->assertEquals('testClass', $this->fieldConfig->getClassName()); - $this->assertEquals('testField2', $this->fieldConfig->getCode()); - $this->assertEquals('datetime', $this->fieldConfig->getType()); - } - - public function testValueConfig() - { - $values = array('firstKey' => 'firstValue', 'secondKey' => 'secondValue'); - $this->fieldConfig->setValues($values); - - $this->assertEquals($values, $this->fieldConfig->getValues()); - - $this->assertEquals('firstValue', $this->fieldConfig->get('firstKey')); - $this->assertEquals('secondValue', $this->fieldConfig->get('secondKey')); - - $this->assertEquals(true, $this->fieldConfig->is('secondKey')); - - $this->assertEquals(true, $this->fieldConfig->has('secondKey')); - $this->assertEquals(false, $this->fieldConfig->has('thirdKey')); - - $this->assertEquals(null, $this->fieldConfig->get('thirdKey')); - - $this->fieldConfig->set('secondKey', 'secondValue2'); - $this->assertEquals('secondValue2', $this->fieldConfig->get('secondKey')); - - $this->setExpectedException('Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); - $this->fieldConfig->get('thirdKey', true); - } - - public function testSerialize() - { - $this->assertEquals($this->fieldConfig, unserialize(serialize($this->fieldConfig))); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php new file mode 100644 index 00000000000..161b50f02c5 --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/EntityIdTest.php @@ -0,0 +1,30 @@ +entityId = new EntityConfigId('Test\Class', 'testScope'); + } + + public function testGetConfig() + { + $this->assertEquals('Test\Class', $this->entityId->getClassName()); + $this->assertEquals('testScope', $this->entityId->getScope()); + $this->assertEquals('entity_testScope_Test-Class', $this->entityId->toString()); + } + + public function testSerialize() + { + $this->assertEquals($this->entityId, unserialize(serialize($this->entityId))); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php new file mode 100644 index 00000000000..7c81665978e --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Config/Id/FieldIdTest.php @@ -0,0 +1,35 @@ +fieldId = new FieldConfigId('Test\Class', 'testScope', 'testField', 'string'); + } + + public function testGetConfig() + { + $this->assertEquals('Test\Class', $this->fieldId->getClassName()); + $this->assertEquals('testScope', $this->fieldId->getScope()); + $this->assertEquals('testField', $this->fieldId->getFieldName()); + $this->assertEquals('string', $this->fieldId->getFieldType()); + $this->assertEquals('field_testScope_Test-Class_testField', $this->fieldId->toString()); + + $this->fieldId->setFieldType('integer'); + $this->assertEquals('integer', $this->fieldId->getFieldType()); + } + + public function testSerialize() + { + $this->assertEquals($this->fieldId, unserialize(serialize($this->fieldId))); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php deleted file mode 100644 index 4a7776d7b29..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/ConfigManagerTest.php +++ /dev/null @@ -1,265 +0,0 @@ -getMockBuilder('Oro\Bundle\UserBundle\Entity\User') - ->disableOriginalConstructor() - ->getMock(); - - $this->securityContext = $this->getMockForAbstractClass( - 'Symfony\Component\Security\Core\SecurityContextInterface' - ); - $token = $this->getMockForAbstractClass( - 'Symfony\Component\Security\Core\Authentication\Token\TokenInterface' - ); - - $token->expects($this->any())->method('getUser')->will($this->returnValue($user)); - - $this->securityContext->expects($this->any())->method('getToken')->will($this->returnValue($token)); - - $this->securityProxy = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy') - ->disableOriginalConstructor() - ->getMock(); - $this->securityProxy->expects($this->any())->method('getService')->will($this->returnValue($this->securityContext)); - - $this->initConfigManager(); - - $this->configCache = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Cache\FileCache') - ->disableOriginalConstructor() - ->getMock(); - - $this->configCache->expects($this->any())->method('putConfigInCache')->will($this->returnValue(null)); - - $this->provider = new ConfigProvider($this->configManager, new EntityConfigContainer('test', array())); - } - - public function testGetConfigFoundConfigEntity() - { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); - $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); - $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); - } - - public function testGetConfigNotFoundConfigEntity() - { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); - $meta->setCustomRepositoryClass(self::NOT_FOUND_CONFIG_ENTITY_REPOSITORY); - $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); - } - - public function testGetConfigRuntimeException() - { - $this->setExpectedException('Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); - $this->configManager->getConfig(self::NO_CONFIGURABLE_ENTITY, 'test'); - } - - public function testGetConfigNotFoundCache() - { - $this->configCache->expects($this->any())->method('loadConfigFromCache')->will($this->returnValue(null)); - - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); - $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); - - $this->configManager->setCache($this->configCache); - $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); - } - - public function testGetConfigFoundCache() - { - $entityConfig = new EntityConfig(self::DEMO_ENTITY, 'test'); - $this->configCache->expects($this->any())->method('loadConfigFromCache')->will($this->returnValue($entityConfig)); - - $this->configManager->setCache($this->configCache); - $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); - } - - public function testHasConfig() - { - $this->assertEquals(true, $this->configManager->hasConfig(self::DEMO_ENTITY, 'test')); - } - - public function testGetEventDispatcher() - { - $this->assertInstanceOf('Symfony\Component\EventDispatcher\EventDispatcher', $this->configManager->getEventDispatcher()); - } - - public function testClearCache() - { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); - $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); - $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); - - $this->configCache->expects($this->any())->method('removeConfigFromCache')->will($this->returnValue(null)); - $this->configManager->setCache($this->configCache); - $this->configManager->addProvider($this->provider); - - $this->configManager->clearCache($meta->getName()); - } - - public function testAddAndGetProvider() - { - $this->configManager->addProvider($this->provider); - - $providers = $this->configManager->getProviders(); - $provider = $this->configManager->getProvider('test'); - - $this->assertEquals(array('test' => $this->provider), $providers); - $this->assertEquals($this->provider, $provider); - } - - public function testInitConfigByDoctrineMetadata() - { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); - $meta->setCustomRepositoryClass(self::NOT_FOUND_CONFIG_ENTITY_REPOSITORY); - - $this->configManager->addProvider($this->provider); - - $this->configManager->initConfigByDoctrineMetadata($this->em->getClassMetadata(self::DEMO_ENTITY)); - } - - public function testPersist() - { - $config = new EntityConfig(self::DEMO_ENTITY, 'test'); - $config->addField(new FieldConfig(self::DEMO_ENTITY, 'test', 'string', 'test')); - - $this->configManager->persist($config); - } - - public function testRemove() - { - $config = new EntityConfig(self::DEMO_ENTITY, 'test'); - $config->addField(new FieldConfig(self::DEMO_ENTITY, 'test', 'string', 'test')); - - $this->configManager->remove($config); - } - - public function testFlush() - { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); - - $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager') - ->disableOriginalConstructor() - ->getMock(); - - $this->em->expects($this->any())->method('flush')->will($this->returnValue(null)); - $this->em->expects($this->any())->method('getRepository') - ->will($this->returnValue(new NotFoundEntityConfigRepository($this->em, $meta))); - - $this->initConfigManager(); - - $this->configManager->addProvider($this->provider); - - $this->configCache->expects($this->any())->method('removeConfigFromCache')->will($this->returnValue(null)); - $this->configManager->setCache($this->configCache); - - $configEntity = new EntityConfig(self::DEMO_ENTITY, 'test'); - $configField = new FieldConfig(self::DEMO_ENTITY, 'test', 'string', 'test'); - - $this->configManager->persist($configEntity); - $this->configManager->persist($configField); - - $this->configManager->flush(); - } - - public function testChangeSet() - { - $meta = $this->em->getClassMetadata(ConfigEntity::ENTITY_NAME); - $meta->setCustomRepositoryClass(self::FOUND_CONFIG_ENTITY_REPOSITORY); - $config = $this->configManager->getConfig(self::DEMO_ENTITY, 'test'); - $configField = $config->getFields()->first(); - - $configField->set('test_field_value1', 'test_field_value1_new'); - - $config->set('test_value', 'test_value_new'); - $config->set('test_value1', 'test_value1_new'); - - $config->set('test_value_serializable', array('test_value' => 'test_value_new')); - - $this->configManager->calculateConfigChangeSet($config); - $this->configManager->calculateConfigChangeSet($configField); - - $result = array( - 'test_value' => array('test_value_origin', 'test_value_new'), - 'test_value_serializable' => array( - array('test_value' => 'test_value_origin'), - array('test_value' => 'test_value_new') - ), - 'test_value1' => array(null, 'test_value1_new'), - ); - - $this->assertEquals($result, $this->configManager->getConfigChangeSet($config)); - - $this->assertEquals(array(spl_object_hash($config) => $config), $this->configManager->getUpdatedEntityConfig()); - $this->assertEquals(array(), $this->configManager->getUpdatedEntityConfig('test1')); - - $this->assertEquals(array(spl_object_hash($configField) => $configField), $this->configManager->getUpdatedFieldConfig()); - $this->assertEquals(array(), $this->configManager->getUpdatedFieldConfig('test1')); - $this->assertEquals(array(), $this->configManager->getUpdatedFieldConfig(null, 'WrongClass')); - } - - protected function initConfigManager() - { - $metadataFactory = new MetadataFactory(new AnnotationDriver(new AnnotationReader)); - - $this->emProxy = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy') - ->disableOriginalConstructor() - ->getMock(); - $this->emProxy->expects($this->any())->method('getService')->will($this->returnValue($this->em)); - - $this->configManager = new ConfigManager($metadataFactory, new EventDispatcher, $this->emProxy, $this->securityProxy); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/Compiler/EntityConfigPassTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/Compiler/EntityConfigPassTest.php deleted file mode 100644 index 62d805861ad..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/Compiler/EntityConfigPassTest.php +++ /dev/null @@ -1,100 +0,0 @@ - array( - 0 => array('scope' => 'datagrid') - )); - - /** Setup */ - protected function setup() - { - $this->compiler = new EntityConfigPass(); - $this->builder = new ContainerBuilder(); - } - - public function testProcess() - { - $this->setDefinitions(); - - $containerMock = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder') - ->disableOriginalConstructor() - ->getMock(); - - $containerMock->expects($this->any()) - ->method('findTaggedServiceIds') - ->with('oro_entity_config.provider') - ->will($this->returnValue($this->config)); - - $containerMock->expects($this->any()) - ->method('hasDefinition') - ->with('oro_entity_config.entity_config.datagrid') - ->will($this->returnValue(true)); - - $containerMock->expects($this->any()) - ->method('getDefinition') - ->will($this->returnValue($this->builder->getDefinition('oro_grid.config.datagrid_config_provider'))); - - $compilerPass = new EntityConfigPass(); - $compilerPass->process($containerMock); - } - - public function testWarning() - { - $containerMock = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder') - ->disableOriginalConstructor() - ->getMock(); - - $containerMock->expects($this->any()) - ->method('findTaggedServiceIds') - ->with('oro_entity_config.provider') - ->will($this->returnValue(array('oro_entity_config.provider' => array()))); - - $this->setExpectedException('\Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); - - $compilerPass = new EntityConfigPass(); - $compilerPass->process($containerMock); - - } - - public function testException() - { - $containerMock = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder') - ->disableOriginalConstructor() - ->getMock(); - - $containerMock->expects($this->any()) - ->method('findTaggedServiceIds') - ->with('oro_entity_config.provider') - ->will($this->returnValue($this->config)); - - $this->setExpectedException('\Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); - - $compilerPass = new EntityConfigPass(); - $compilerPass->process($containerMock); - } - - protected function setDefinitions() - { - $defRegistry_0 = new Definition('Oro\Bundle\EntityConfigBundle\ConfigManager'); - $defRegistry_1 = new Definition('Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider'); - $definitions = array( - 'oro_entity_config.config_manager' => $defRegistry_0, - 'oro_grid.config.datagrid_config_provider' => $defRegistry_1 - ); - - $this->builder->setDefinitions($definitions); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/EntityConfigContainerTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/EntityConfigContainerTest.php deleted file mode 100644 index a2ccca275d6..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/DependencyInjection/EntityConfigContainerTest.php +++ /dev/null @@ -1,77 +0,0 @@ -config = Yaml::parse(__DIR__ . '/../Fixture/entity_config.yml'); - $this->config = reset($this->config['oro_entity_config']); - - $scope = key($this->config); - - $this->container = new EntityConfigContainer($scope, $this->config); - } - - public function testContainer() - { - $this->assertEquals(key($this->config), $this->container->getScope()); - - $this->assertEquals($this->config['field']['items'], $this->container->getFieldItems()); - - $this->assertEquals($this->config, $this->container->getConfig()); - - $this->assertTrue($this->container->hasEntityForm()); - $this->assertTrue($this->container->hasFieldForm()); - - $this->assertEquals( - $this->config['field']['form'], - $this->container->getFieldFormConfig() - ); - - $this->assertEquals( - $this->config['entity']['grid_action'], - $this->container->getEntityGridActions() - ); - - $this->assertEquals( - $this->config['entity']['layout_action'], - $this->container->getEntityLayoutActions() - ); - - $this->assertEquals( - $this->config['field']['grid_action'], - $this->container->getFieldGridActions() - ); - - $this->assertEquals( - $this->config['field']['layout_action'], - $this->container->getFieldLayoutActions() - ); - - $this->assertEquals( - $this->config['entity']['form']['block_config'], - $this->container->getEntityFormBlockConfig() - ); - - $this->assertEquals( - array('enabled' => true), - $this->container->getEntityDefaultValues() - ); - - $this->assertEquals( - array('enabled' => true, 'is_searchable' => false, 'is_filtrableble' => false), - $this->container->getFieldDefaultValues() - ); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigLogTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigLogTest.php new file mode 100644 index 00000000000..fa2917bf69f --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigLogTest.php @@ -0,0 +1,73 @@ +configLog = new ConfigLog(); + $this->configLogDiff = new ConfigLogDiff(); + } + + protected function tearDown() + { + $this->configLog = null; + $this->configLogDiff = null; + } + + public function testConfigLog() + { + $userMock = $this->getMockForAbstractClass('Symfony\Component\Security\Core\User\UserInterface'); + + $this->assertEmpty($this->configLog->getId()); + + $data = new \DateTime(); + $this->configLog->setLoggedAt($data); + $this->assertEquals($data, $this->configLog->getLoggedAt()); + + $this->configLog->setLoggedAt(null); + $this->configLog->prePersist(); + $this->assertInstanceOf('\DateTime', $this->configLog->getLoggedAt()); + + $this->configLog->setUser($userMock); + $this->assertEquals($userMock, $this->configLog->getUser()); + + $this->configLog->addDiff($this->configLogDiff); + $this->assertEquals($this->configLogDiff, $this->configLog->getDiffs()->first()); + + $diffsCollection = new ArrayCollection(array($this->configLogDiff)); + $this->configLog->setDiffs($diffsCollection); + $this->assertEquals($diffsCollection, $this->configLog->getDiffs()); + } + + public function testConfigDiff() + { + $this->assertEmpty($this->configLogDiff->getId()); + + $this->configLogDiff->setLog($this->configLog); + $this->assertEquals($this->configLog, $this->configLogDiff->getLog()); + + $this->configLogDiff->setClassName('className'); + $this->assertEquals('className', $this->configLogDiff->getClassName()); + + $this->configLogDiff->setFieldName('fieldName'); + $this->assertEquals('fieldName', $this->configLogDiff->getFieldName()); + + $this->configLogDiff->setScope('scope'); + $this->assertEquals('scope', $this->configLogDiff->getScope()); + + $this->configLogDiff->setDiff(array('key' => 'value')); + $this->assertEquals(array('key' => 'value'), $this->configLogDiff->getDiff()); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php index b9f9c37c18e..025a988464d 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Entity/ConfigTest.php @@ -2,26 +2,27 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Entity; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigValue; +use Oro\Bundle\EntityConfigBundle\Config\ConfigModelManager; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\ConfigModelValue; class ConfigTest extends \PHPUnit_Framework_TestCase { - /** @var ConfigEntity */ + /** @var EntityConfigModel */ private $configEntity; - /** @var ConfigField */ + /** @var FieldConfigModel */ private $configField; - /** @var ConfigValue */ + /** @var ConfigModelValue */ private $configValue; protected function setUp() { - $this->configEntity = new ConfigEntity(); - $this->configField = new ConfigField(); - $this->configValue = new ConfigValue(); + $this->configEntity = new EntityConfigModel(); + $this->configField = new FieldConfigModel(); + $this->configValue = new ConfigModelValue(); } public function testProperties() @@ -51,6 +52,9 @@ public function testProperties() /** test ConfigField */ $this->assertEmpty($this->configField->getId()); + $this->configField->setMode(ConfigModelManager::MODE_READONLY); + $this->assertEquals(ConfigModelManager::MODE_READONLY, $this->configField->getMode()); + /** test ConfigValue */ $this->assertEmpty($this->configValue->getId()); $this->assertEmpty($this->configValue->getScope()); @@ -60,6 +64,9 @@ public function testProperties() $this->assertFalse($this->configValue->getSerializable()); + $this->configValue->setSerializable(true); + $this->assertTrue($this->configValue->getSerializable()); + $this->assertEmpty($this->configValue->getEntity()); $this->configValue->setEntity($this->configEntity); @@ -73,7 +80,7 @@ public function test() { $this->assertEquals( 'test', - $this->configField->getCode($this->configField->setCode('test')) + $this->configField->getFieldName($this->configField->setFieldName('test')) ); $this->assertEquals( @@ -126,7 +133,7 @@ public function test() $this->configValue->toArray() ); - /** test AbstractConfig setValues() */ + /** test Config setValues() */ $this->configEntity->setValues(array($this->configValue)); $this->assertEquals( $this->configValue, @@ -142,7 +149,7 @@ public function testToFromArray() ->setScope('datagrid') ->setValue('a:2:{s:4:"code";s:8:"test_001";s:4:"type";s:6:"string";}'); - $values = array( + $values = array( 'is_searchable' => true, 'is_sortable' => false, 'doctrine' => $this->configValue @@ -151,7 +158,7 @@ public function testToFromArray() 'doctrine' => true ); - $this->configField->addValue(new ConfigValue('is_searchable', 'datagrid', false)); + $this->configField->addValue(new ConfigModelValue('is_searchable', 'datagrid', false)); $this->configField->fromArray('datagrid', $values, $serializable); $this->assertEquals( array( @@ -161,5 +168,16 @@ public function testToFromArray() ), $this->configField->toArray('datagrid') ); + + $this->configEntity->addValue(new ConfigModelValue('is_searchable', 'datagrid', false)); + $this->configEntity->fromArray('datagrid', $values, $serializable); + $this->assertEquals( + array( + 'is_searchable' => 1, + 'is_sortable' => 0, + 'doctrine' => $this->configValue + ), + $this->configEntity->toArray('datagrid') + ); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php index 5ea2f1167dd..b20fe884f13 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/EntityConfigEventTest.php @@ -2,9 +2,11 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Event; -use Oro\Bundle\EntityConfigBundle\ConfigManager; -use Oro\Bundle\EntityConfigBundle\Event\NewEntityEvent; -use Oro\Bundle\EntityConfigBundle\Tests\Unit\ConfigManagerTest; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; + +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; +use Oro\Bundle\EntityConfigBundle\Event\NewEntityConfigModelEvent; class EntityConfigEventTest extends \PHPUnit_Framework_TestCase { @@ -15,20 +17,16 @@ class EntityConfigEventTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') ->disableOriginalConstructor() ->getMock(); - - $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); - $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); - } public function testEvent() { - $event = new NewEntityEvent(ConfigManagerTest::DEMO_ENTITY, $this->configManager); + $event = new NewEntityConfigModelEvent(new EntityConfigModel('Test\Class'), $this->configManager); - $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $event->getClassName()); + $this->assertEquals('Test\Class', $event->getClassName()); $this->assertEquals($this->configManager, $event->getConfigManager()); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FieldConfigEventTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FieldConfigEventTest.php index 2319f365b54..7a5f8064500 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FieldConfigEventTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/FieldConfigEventTest.php @@ -2,9 +2,11 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Event; -use Oro\Bundle\EntityConfigBundle\ConfigManager; -use Oro\Bundle\EntityConfigBundle\Event\NewFieldEvent; -use Oro\Bundle\EntityConfigBundle\Tests\Unit\ConfigManagerTest; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; + +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; +use Oro\Bundle\EntityConfigBundle\Event\NewFieldConfigModelEvent; class FieldConfigEventTest extends \PHPUnit_Framework_TestCase { @@ -15,22 +17,26 @@ class FieldConfigEventTest extends \PHPUnit_Framework_TestCase protected function setUp() { - $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') ->disableOriginalConstructor() ->getMock(); - $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('isConfigurable')->will($this->returnValue(true)); $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); } public function testEvent() { - $event = new NewFieldEvent(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', $this->configManager); + $entityConfigModel = new EntityConfigModel('Test\Class'); + $fieldConfigModel = new FieldConfigModel('testField', 'string'); + $fieldConfigModel->setEntity($entityConfigModel); - $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $event->getClassName()); - $this->assertEquals($this->configManager, $event->getConfigManager()); + $event = new NewFieldConfigModelEvent($fieldConfigModel, $this->configManager); + + $this->assertEquals('Test\Class', $event->getClassName()); $this->assertEquals('testField', $event->getFieldName()); $this->assertEquals('string', $event->getFieldType()); + $this->assertEquals($this->configManager, $event->getConfigManager()); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/PersistConfigEventTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/PersistConfigEventTest.php new file mode 100644 index 00000000000..9d0862c6c6f --- /dev/null +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Event/PersistConfigEventTest.php @@ -0,0 +1,37 @@ +configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') + ->disableOriginalConstructor() + ->getMock(); + + $this->configManager->expects($this->any())->method('isConfigurable')->will($this->returnValue(true)); + $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); + + } + + public function testEvent() + { + $config = new Config(new EntityConfigId('Test/Class', 'test')); + $event = new PersistConfigEvent($config, $this->configManager); + + $this->assertEquals($config, $event->getConfig()); + $this->assertEquals($config->getId(), $event->getConfigId()); + $this->assertEquals($this->configManager, $event->getConfigManager()); + } +} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/DemoEntity.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/DemoEntity.php index ca3434bdb11..677e0b606b8 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/DemoEntity.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/DemoEntity.php @@ -4,14 +4,16 @@ use Doctrine\ORM\Mapping as ORM; -use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Configurable; +use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Config; /** * @ORM\Entity - * @Configurable + * @Config */ class DemoEntity { + const ENTITY_NAME = 'Oro\Bundle\EntityConfigBundle\Tests\Unit\Fixture\DemoEntity'; + /** * @var integer * @ORM\Column(name="id", type="integer") diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php index fc351acf604..aa0858a4745 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Fixture/Repository/FoundEntityConfigRepository.php @@ -4,9 +4,9 @@ use Doctrine\ORM\EntityRepository; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigValue; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\ConfigModelValue; use Oro\Bundle\EntityConfigBundle\Tests\Unit\ConfigManagerTest; class FoundEntityConfigRepository extends EntityRepository @@ -21,13 +21,23 @@ public function findOneBy(array $criteria, array $orderBy = null) public static function getResultConfigEntity() { if (!self::$configEntity) { - self::$configEntity = new ConfigEntity(ConfigManagerTest::DEMO_ENTITY); + self::$configEntity = new EntityConfigModel(ConfigManagerTest::DEMO_ENTITY); - $configField = new ConfigField('test', 'string'); + $configField = new FieldConfigModel('test', 'string'); self::$configEntity->addField($configField); - $configValue = new ConfigValue('test_value', 'test', 'test_value_origin'); - $configValueSerializable = new ConfigValue('test_value_serializable', 'test', array('test_value' => 'test_value_origin')); + $configValue = new ConfigModelValue( + 'test_value', + 'test', + 'test_value_origin' + ); + + $configValueSerializable = new ConfigModelValue( + 'test_value_serializable', + 'test', + array('test_value' => 'test_value_origin') + ); + self::$configEntity->addValue($configValue); self::$configEntity->addValue($configValueSerializable); } diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigEntityTypeTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigEntityTypeTest.php deleted file mode 100644 index 7bff85eb601..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigEntityTypeTest.php +++ /dev/null @@ -1,77 +0,0 @@ -factory = Forms::createFormFactoryBuilder() - ->addTypeExtension(new DataBlockExtension()) - ->getFormFactory(); - - $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') - ->disableOriginalConstructor() - ->getMock(); - - $config = Yaml::parse(file_get_contents(__DIR__ . '/../../Fixture/entity_config.yml')); - $scope = key($config['oro_entity_config']); - $config = reset($config['oro_entity_config']); - - $configProvider = new ConfigProvider($this->configManager, new EntityConfigContainer($scope, $config)); - $entityConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'datagrid'); - - $this->configManager->expects($this->any())->method('getConfig')->will($this->returnValue($entityConfig)); - $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); - $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); - $this->configManager->expects($this->any())->method('getProviders')->will($this->returnValue(array($configProvider))); - } - - public function testBindValidData() - { - $formData = array( - 'datagrid' => array('enabled' => true), - 'id' => null, - ); - - $type = new ConfigEntityType($this->configManager); - $form = $this->factory->create($type, null, array( - 'class_name' => ConfigManagerTest::DEMO_ENTITY, - 'entity_id' => 1 - )); - - $form->bind($formData); - - $this->assertTrue($form->isSynchronized()); - $this->assertEquals($formData, $form->getData()); - - $view = $form->createView(); - $children = $view->children; - - foreach (array_keys($formData) as $key) { - $this->assertArrayHasKey($key, $children); - } - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigFieldTypeTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigFieldTypeTest.php deleted file mode 100644 index 2902c268bee..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Form/Type/ConfigFieldTypeTest.php +++ /dev/null @@ -1,94 +0,0 @@ -factory = Forms::createFormFactoryBuilder() - ->addTypeExtension(new DataBlockExtension()) - ->addTypeExtension(new ConfigFormExtension()) - ->getFormFactory(); - - $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') - ->disableOriginalConstructor() - ->getMock(); - - $config = Yaml::parse(file_get_contents(__DIR__ . '/../../Fixture/entity_config.yml')); - $scope = key($config['oro_entity_config']); - $config = reset($config['oro_entity_config']); - - $configProvider = new ConfigProvider($this->configManager, new EntityConfigContainer($scope, $config)); - - $entityConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'datagrid'); - $entityConfig->addField(new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', 'datagrid')); - - $this->configManager->expects($this->any())->method('getConfig')->will($this->returnValue($entityConfig)); - $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); - $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); - $this->configManager->expects($this->any())->method('getProviders')->will($this->returnValue(array($configProvider))); - } - - public function testBindValidData() - { - $formData = array( - 'id' => null, - 'datagrid' => array( - 'enabled' => true, - 'is_searchable' => true, - 'is_filtrableble' => false, - ), - ); - - $type = new ConfigFieldType($this->configManager); - - $form = $this->factory->create( - $type, - null, - array( - 'class_name' => ConfigManagerTest::DEMO_ENTITY, - 'field_name' => 'testField', - 'field_type' => 'string', - 'field_id' => 1, - ) - ); - - $form->bind($formData); - - $this->assertTrue($form->isSynchronized()); - $this->assertEquals($formData, $form->getData()); - - $view = $form->createView(); - $children = $view->children; - - foreach (array_keys($formData) as $key) { - $this->assertArrayHasKey($key, $children); - } - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php deleted file mode 100644 index d961b750e84..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/Annotation/ConfigurableTest.php +++ /dev/null @@ -1,21 +0,0 @@ - 'hidden')); - $this->assertEquals('hidden', $annot->viewMode); - - $annot = new Configurable(array('viewMode' => 'hidden')); - $this->assertEquals('hidden', $annot->viewMode); - - $this->setExpectedException('\Oro\Bundle\EntityConfigBundle\Exception\AnnotationException'); - - $annot = new Configurable(array('viewMode' => 'wrong_value')); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/ClassMetadataTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/ClassMetadataTest.php index 7ff9d3dc08f..0425e204602 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/ClassMetadataTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Metadata/ClassMetadataTest.php @@ -2,20 +2,21 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Metadata; -use Oro\Bundle\EntityConfigBundle\Metadata\ConfigClassMetadata; -use Oro\Bundle\EntityConfigBundle\Tests\Unit\ConfigManagerTest; +use Oro\Bundle\EntityConfigBundle\Config\ConfigModelManager; +use Oro\Bundle\EntityConfigBundle\Metadata\EntityMetadata; +use Oro\Bundle\EntityConfigBundle\Tests\Unit\Fixture\DemoEntity; class ClassMetadataTest extends \PHPUnit_Framework_TestCase { /** - * @var ConfigClassMetadata + * @var EntityMetadata */ protected $classMetadata; public function setUp() { - $this->classMetadata = new ConfigClassMetadata(ConfigManagerTest::DEMO_ENTITY); - $this->classMetadata->viewMode = 'hidden'; + $this->classMetadata = new EntityMetadata(DemoEntity::ENTITY_NAME); + $this->classMetadata->mode = ConfigModelManager::MODE_DEFAULT; } public function testSerialize() @@ -25,10 +26,10 @@ public function testSerialize() public function testMerge() { - $newMetadata = new ConfigClassMetadata(ConfigManagerTest::DEMO_ENTITY); - $newMetadata->viewMode = 'readonly'; + $newMetadata = new EntityMetadata(DemoEntity::ENTITY_NAME); + $newMetadata->mode = ConfigModelManager::MODE_READONLY; $this->classMetadata->merge($newMetadata); - $this->assertEquals('readonly', $this->classMetadata->viewMode); + $this->assertEquals(ConfigModelManager::MODE_READONLY, $this->classMetadata->mode); } } diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/OroEntityConfigBundleTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/OroEntityConfigBundleTest.php deleted file mode 100644 index 5bb2aa0bc5a..00000000000 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/OroEntityConfigBundleTest.php +++ /dev/null @@ -1,37 +0,0 @@ -containerBuilder = new ContainerBuilder(); - $this->bundle = new OroEntityConfigBundle(); - } - - public function testBuild() - { - $this->bundle->build($this->containerBuilder); - - $pass = $this->containerBuilder->getCompilerPassConfig()->getBeforeOptimizationPasses(); - - $this->assertInstanceOf('Oro\Bundle\EntityConfigBundle\DependencyInjection\Compiler\EntityConfigPass', $pass[0]); - $this->assertInstanceOf('Oro\Bundle\EntityConfigBundle\DependencyInjection\Compiler\ServiceProxyPass', $pass[1]); - } -} diff --git a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Provider/ConfigProviderTest.php b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Provider/ConfigProviderTest.php index 43460e0b267..563d3a3019e 100644 --- a/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Provider/ConfigProviderTest.php +++ b/src/Oro/Bundle/EntityConfigBundle/Tests/Unit/Provider/ConfigProviderTest.php @@ -2,12 +2,14 @@ namespace Oro\Bundle\EntityConfigBundle\Tests\Unit\Provider; -use Oro\Bundle\EntityConfigBundle\Config\EntityConfig; -use Oro\Bundle\EntityConfigBundle\Config\FieldConfig; -use Oro\Bundle\EntityConfigBundle\ConfigManager; -use Oro\Bundle\EntityConfigBundle\DependencyInjection\EntityConfigContainer; +use Oro\Bundle\EntityConfigBundle\Config\Config; +use Oro\Bundle\EntityConfigBundle\Config\Id\EntityConfigId; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; + +use Oro\Bundle\EntityConfigBundle\Provider\PropertyConfigContainer; use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; -use Oro\Bundle\EntityConfigBundle\Tests\Unit\ConfigManagerTest; +use Oro\Bundle\EntityConfigBundle\Tests\Unit\Fixture\DemoEntity; +use Symfony\Component\DependencyInjection\Container; class ConfigProviderTest extends \PHPUnit_Framework_TestCase { @@ -17,12 +19,12 @@ class ConfigProviderTest extends \PHPUnit_Framework_TestCase protected $configManager; /** - * @var EntityConfig + * @var Config */ protected $entityConfig; /** - * @var FieldConfig + * @var Config */ protected $fieldConfig; @@ -32,16 +34,15 @@ class ConfigProviderTest extends \PHPUnit_Framework_TestCase protected $configProvider; /** - * @var EntityConfigContainer + * @var PropertyConfigContainer */ protected $configContainer; protected function setUp() { - $this->entityConfig = new EntityConfig(ConfigManagerTest::DEMO_ENTITY, 'testScope'); - $this->fieldConfig = new FieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', 'testScope'); + $this->entityConfig = new Config(new EntityConfigId(DemoEntity::ENTITY_NAME, 'test')); - $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\ConfigManager') + $this->configManager = $this->getMockBuilder('Oro\Bundle\EntityConfigBundle\Config\ConfigManager') ->disableOriginalConstructor() ->getMock(); @@ -49,36 +50,22 @@ protected function setUp() $this->configManager->expects($this->any())->method('hasConfig')->will($this->returnValue(true)); $this->configManager->expects($this->any())->method('flush')->will($this->returnValue(true)); - $this->configContainer = new EntityConfigContainer('test', array()); - $this->configProvider = new ConfigProvider($this->configManager, $this->configContainer); + $this->configContainer = new Container(); + $this->configProvider = new ConfigProvider($this->configManager, $this->configContainer, 'test', array()); } public function testConfig() { - $this->assertEquals(true, $this->configProvider->hasConfig(ConfigManagerTest::DEMO_ENTITY)); - $this->assertEquals($this->entityConfig, $this->configProvider->getConfig(ConfigManagerTest::DEMO_ENTITY)); - - $this->entityConfig->addField($this->fieldConfig); - - $this->assertEquals(true, $this->configProvider->hasFieldConfig(ConfigManagerTest::DEMO_ENTITY, 'testField')); - $this->assertEquals($this->fieldConfig, $this->configProvider->getFieldConfig( - ConfigManagerTest::DEMO_ENTITY, - 'testField' - )); + $this->assertEquals(true, $this->configProvider->hasConfig(DemoEntity::ENTITY_NAME)); + $this->assertEquals($this->entityConfig, $this->configProvider->getConfig(DemoEntity::ENTITY_NAME)); $this->assertEquals('test', $this->configProvider->getScope()); - - $this->assertEquals($this->configContainer, $this->configProvider->getConfigContainer()); } public function testCreateConfig() { - $this->configProvider->createEntityConfig( - ConfigManagerTest::DEMO_ENTITY, array('first' => 'test'), true - ); - - $this->configProvider->createFieldConfig( - ConfigManagerTest::DEMO_ENTITY, 'testField', 'string', array('first' => 'test'), true + $this->configProvider->createConfig( + new EntityConfigId(DemoEntity::ENTITY_NAME, 'test'), array('first' => 'test') ); $this->configProvider->flush(); @@ -86,15 +73,15 @@ public function testCreateConfig() public function testGetClassName() { - $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->configProvider->getClassName(ConfigManagerTest::DEMO_ENTITY)); + $this->assertEquals(DemoEntity::ENTITY_NAME, $this->configProvider->getClassName(DemoEntity::ENTITY_NAME)); - $className = ConfigManagerTest::DEMO_ENTITY; + $className = DemoEntity::ENTITY_NAME; $demoEntity = new $className(); - $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->configProvider->getClassName($demoEntity)); + $this->assertEquals(DemoEntity::ENTITY_NAME, $this->configProvider->getClassName($demoEntity)); - $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->configProvider->getClassName(array($demoEntity))); + $this->assertEquals(DemoEntity::ENTITY_NAME, $this->configProvider->getClassName(array($demoEntity))); $this->setExpectedException('Oro\Bundle\EntityConfigBundle\Exception\RuntimeException'); - $this->assertEquals(ConfigManagerTest::DEMO_ENTITY, $this->configProvider->getClassName(array())); + $this->assertEquals(DemoEntity::ENTITY_NAME, $this->configProvider->getClassName(array())); } } diff --git a/src/Oro/Bundle/EntityExtendBundle/Command/BackupCommand.php b/src/Oro/Bundle/EntityExtendBundle/Command/BackupCommand.php index d2a8869b3db..f1f8ce307fc 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Command/BackupCommand.php +++ b/src/Oro/Bundle/EntityExtendBundle/Command/BackupCommand.php @@ -64,7 +64,7 @@ protected function initialize(InputInterface $input, OutputInterface $output) //$tables = array('oro_config_entity', 'oro_config_field'); /** @var ConfigProvider $extendConfigProvider */ - $extendConfigProvider = $this->getContainer()->get('oro_entity_extend.config.extend_config_provider'); + $extendConfigProvider = $this->getContainer()->get('oro_entity_config.provider.extend'); /** @var EntityManager $em */ $em = $this->getContainer()->get('doctrine')->getManager('default'); diff --git a/src/Oro/Bundle/EntityExtendBundle/Command/CreateCommand.php b/src/Oro/Bundle/EntityExtendBundle/Command/CreateCommand.php new file mode 100644 index 00000000000..9e223deccf0 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Command/CreateCommand.php @@ -0,0 +1,173 @@ +setName('oro:entity-extend:create') + ->setDescription('Find description about custom entities and fields'); + } + + /** + * Runs command + * @param InputInterface $input + * @param OutputInterface $output + * @throws \InvalidArgumentException + * @return int|null|void + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln($this->getDescription()); + + $this->extendManager = $this->getContainer()->get('oro_entity_extend.extend.extend_manager'); + $this->configManager = $this->getContainer()->get('oro_entity_config.config_manager'); + + /** @var Kernel $kernel */ + $kernel = $this->getContainer()->get('kernel'); + foreach ($kernel->getBundles() as $bundle) { + $path = $bundle->getPath() . '/Resources/config/entity_extend.yml'; + if (is_file($path)) { + $config = Yaml::parse(realpath($path)); + + foreach ($config as $entityName => $entityOptions) { + $this->parseEntity($entityName, $entityOptions); + } + + $this->configManager->flush(); + + //fix state "Update" for existing class. + foreach ($config as $entityName => $entityOptions) { + $entityConfigProvider = $this->extendManager->getConfigProvider(); + $entityConfig = $entityConfigProvider->getConfig($entityName); + $entityConfig->set('state', ExtendManager::STATE_ACTIVE); + + $this->configManager->persist($entityConfig); + + foreach ($entityConfigProvider->getConfigs($entityName) as $fieldConfig) { + $fieldConfig->set('state', ExtendManager::STATE_ACTIVE); + $this->configManager->persist($fieldConfig); + } + } + $this->configManager->flush(); + + $output->writeln('Done'); + } + } + + $this->getApplication()->find('oro:entity-extend:update')->run($input, $output); + } + + /** + * @param $entityName + * @param $entityOptions + * @throws \InvalidArgumentException + */ + protected function parseEntity($entityName, $entityOptions) + { + if (!$this->configManager->isConfigurable($entityName)) { + $this->createEntityModel($entityName, $entityOptions); + $this->setDefaultConfig($entityOptions, $entityName); + + $entityConfig = $this->extendManager->getConfigProvider()->getConfig($entityName); + if (!class_exists($entityName)) { + $entityConfig->set('owner', ExtendManager::OWNER_CUSTOM); + } + $entityConfig->set('is_extend', true); + } + + foreach ($entityOptions['fields'] as $fieldName => $fieldConfig) { + if ($this->configManager->isConfigurable($entityName, $fieldName)) { + throw new \InvalidArgumentException( + sprintf('Field "%s" for Entity "%s" already added', $entityName, $fieldName) + ); + } + + $mode = isset($fieldConfig['mode']) ? $fieldConfig['mode'] : ConfigModelManager::MODE_DEFAULT; + $this->extendManager->getExtendFactory()->createField($entityName, $fieldName, $fieldConfig, $mode); + + $this->setDefaultConfig($entityOptions, $entityName, $fieldName); + + $config = $this->extendManager->getConfigProvider()->getConfig($entityName, $fieldName); + $config->set('state', ExtendManager::STATE_ACTIVE); + $this->configManager->persist($config); + } + } + + /** + * @param $entityName + * @param $entityConfig + * @return void + */ + protected function createEntityModel($entityName, $entityConfig) + { + $mode = isset($entityConfig['mode']) ? $entityConfig['mode'] : ConfigModelManager::MODE_DEFAULT; + + $this->configManager->createConfigEntityModel($entityName, $mode); + + if (class_exists($entityName)) { + $doctrineMetadata = $this->configManager->getEntityManager()->getClassMetadata($entityName); + foreach ($doctrineMetadata->getFieldNames() as $fieldName) { + $type = $doctrineMetadata->getTypeOfField($fieldName); + $this->configManager->createConfigFieldModel($doctrineMetadata->getName(), $fieldName, $type); + } + + foreach ($doctrineMetadata->getAssociationNames() as $fieldName) { + $type = $doctrineMetadata->isSingleValuedAssociation($fieldName) ? 'ref-one' : 'ref-many'; + $this->configManager->createConfigFieldModel($doctrineMetadata->getName(), $fieldName, $type); + } + } + } + + /** + * @param array $options + * @param string $entityName + * @param string $fieldName + */ + protected function setDefaultConfig($options, $entityName, $fieldName = null) + { + if ($fieldName) { + $config = isset($options['fields'][$fieldName]['configs']) + ? $options['fields'][$fieldName]['configs'] + : array(); + } else { + $config = isset($options['configs']) ? $options['configs'] : array(); + } + + foreach ($config as $scope => $values) { + $config = $this->configManager->getProvider($scope)->getConfig($entityName, $fieldName); + + foreach ($values as $key => $value) { + $config->set($key, $value); + } + } + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php b/src/Oro/Bundle/EntityExtendBundle/Command/UpdateCommand.php similarity index 66% rename from src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php rename to src/Oro/Bundle/EntityExtendBundle/Command/UpdateCommand.php index d6361ea7aa7..f46faa2c8bd 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Command/GenerateCommand.php +++ b/src/Oro/Bundle/EntityExtendBundle/Command/UpdateCommand.php @@ -4,14 +4,14 @@ use Doctrine\ORM\EntityManager; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; use Oro\Bundle\EntityExtendBundle\Extend\ExtendManager; use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -class GenerateCommand extends ContainerAwareCommand +class UpdateCommand extends ContainerAwareCommand { /** * @var EntityManager @@ -24,8 +24,8 @@ class GenerateCommand extends ContainerAwareCommand public function configure() { $this - ->setName('oro:entity-extend:generate') - ->setDescription('Generate class for doctrine'); + ->setName('oro:entity-extend:update') + ->setDescription('Generate class and yml for doctrine'); } /** @@ -44,12 +44,16 @@ public function execute(InputInterface $input, OutputInterface $output) /** @var ExtendManager $xm */ $xm = $this->getContainer()->get('oro_entity_extend.extend.extend_manager'); - /** @var ConfigEntity[] $configs */ - $configs = $em->getRepository(ConfigEntity::ENTITY_NAME)->findAll(); + /** @var EntityConfigModel[] $configs */ + $configs = $em->getRepository(EntityConfigModel::ENTITY_NAME)->findAll(); foreach ($configs as $config) { if ($xm->isExtend($config->getClassName())) { - $xm->getClassGenerator()->checkEntityCache($config->getClassName(), true); + $owner = $xm->getConfigProvider()->getConfig($config->getClassName())->get('owner', true); + $extend = ExtendManager::OWNER_CUSTOM != $owner; + $xm->getClassGenerator()->checkEntityCache($config->getClassName(), true, $extend); }; } + + $output->writeln('Done'); } } diff --git a/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php b/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php index fa7c297f70e..89d8cb2bfd4 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php +++ b/src/Oro/Bundle/EntityExtendBundle/Controller/ApplyController.php @@ -2,23 +2,26 @@ namespace Oro\Bundle\EntityExtendBundle\Controller; +use Doctrine\Bundle\DoctrineBundle\Registry; +use Oro\Bundle\EntityConfigBundle\Config\ConfigModelManager; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\Process\Process; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Oro\Bundle\UserBundle\Annotation\Acl; +use Oro\Bundle\EntityConfigBundle\Config\Config; use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; - -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; use Oro\Bundle\EntityExtendBundle\Tools\Schema; -use Symfony\Component\Process\Process; +use Oro\Bundle\EntityExtendBundle\Extend\ExtendManager; /** * EntityExtendBundle controller. - * @Route("/oro_entityextend") + * @Route("/entity/extend") * @Acl( * id="oro_entityextend", * name="Entity extend manipulation", @@ -44,15 +47,16 @@ class ApplyController extends Controller */ public function applyAction($id) { - /** @var ConfigEntity $entity */ - $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); + /** @var EntityConfigModel $entity */ + $entity = $this->getDoctrine()->getRepository(EntityConfigModel::ENTITY_NAME)->find($id); /** @var ConfigProvider $entityConfigProvider */ - $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + $entityConfigProvider = $this->get('oro_entity_config.provider.entity'); /** @var ConfigProvider $extendConfigProvider */ - $extendConfigProvider = $this->get('oro_entity_extend.config.extend_config_provider'); - $extendConfig = $extendConfigProvider->getConfig($entity->getClassName()); + $extendConfigProvider = $this->get('oro_entity_config.provider.extend'); + $extendConfig = $extendConfigProvider->getConfig($entity->getClassName()); + $extendFieldConfigs = $extendConfigProvider->getConfigs($entity->getClassName()); /** @var Schema $schemaTools */ $schemaTools = $this->get('oro_entity_extend.tools.schema'); @@ -62,43 +66,43 @@ public function applyAction($id) */ $validation = array(); - $fields = $extendConfig->getFields(); - foreach ($fields as $code => $field) { - - //$isSystem = $schemaTools->checkFieldIsSystem($field); - $isSystem = $field->get('owner') == 'System' ? true : false; + /** @var Config $fieldConfig */ + foreach ($extendFieldConfigs as $fieldConfig) { + $isSystem = $fieldConfig->get('owner') == 'System' ? true : false; if ($isSystem) { continue; } - if (in_array($field->get('state'), array('New', 'Updated', 'To be deleted'))) { - if ($field->get('state') == 'New') { + if (in_array($fieldConfig->get('state'), array('New', 'Requires update', 'To be deleted'))) { + if ($fieldConfig->get('state') == 'New') { $isValid = true; } else { - $isValid = $schemaTools->checkFieldCanDelete($field); + $isValid = $schemaTools->checkFieldCanDelete($fieldConfig->getId()); } if ($isValid) { $validation['success'][] = sprintf( "Field '%s(%s)' is valid. State -> %s", - $code, - $field->get('owner'), - $field->get('state') + $fieldConfig->getId()->getFieldName(), + $fieldConfig->get('owner'), + $fieldConfig->get('state') ); } else { $validation['error'][] = sprintf( "Warning. Field '%s(%s)' has data.", - $code, - $field->get('owner') + $fieldConfig->getId()->getFieldName(), + $fieldConfig->get('owner') ); } } } + $entityConfig = $entityConfigProvider->getConfig($entity->getClassName()); + return array( 'validations' => $validation, 'entity' => $entity, - 'entity_config' => $entityConfigProvider->getConfig($entity->getClassName()), + 'entity_config' => $entityConfig, 'entity_extend' => $extendConfig, ); } @@ -120,17 +124,23 @@ public function applyAction($id) */ public function updateAction($id) { - /** @var ConfigEntity $entity */ - $entity = $this->getDoctrine()->getRepository(ConfigEntity::ENTITY_NAME)->find($id); - $env = $this->get('kernel')->getEnvironment(); + /** @var EntityConfigModel $entity */ + $entity = $this->getDoctrine()->getRepository(EntityConfigModel::ENTITY_NAME)->find($id); + $env = $this->get('kernel')->getEnvironment(); $commands = array( - 'backup' => new Process('php ../app/console oro:entity-extend:backup '. str_replace('\\', '\\\\', $entity->getClassName()). ' --env '.$env), - 'generator' => new Process('php ../app/console oro:entity-extend:generate'. ' --env '.$env), - 'cacheClear' => new Process('php ../app/console cache:clear --no-warmup'. ' --env '.$env), - 'schemaUpdate' => new Process('php ../app/console doctrine:schema:update --force'. ' --env '.$env), - 'searchIndex' => new Process('php ../app/console oro:search:create-index --env '.$env), - 'cacheWarmup' => new Process('php ../app/console cache:warmup'. ' --env '.$env), + 'backup' => new Process( + 'php ../app/console oro:entity-extend:backup ' . str_replace( + '\\', + '\\\\', + $entity->getClassName() + ) . ' --env ' . $env + ), + 'update' => new Process('php ../app/console oro:entity-extend:update' . ' --env ' . $env), + 'cacheClear' => new Process('php ../app/console cache:clear --no-warmup' . ' --env ' . $env), + 'schemaUpdate' => new Process('php ../app/console doctrine:schema:update --force' . ' --env ' . $env), + 'searchIndex' => new Process('php ../app/console oro:search:create-index --env ' . $env), + 'cacheWarmup' => new Process('php ../app/console cache:warmup' . ' --env ' . $env), ); foreach ($commands as $command) { @@ -143,20 +153,37 @@ public function updateAction($id) } /** @var ConfigProvider $extendConfigProvider */ - $extendConfigProvider = $this->get('oro_entity_extend.config.extend_config_provider'); - $extendConfig = $extendConfigProvider->getConfig($entity->getClassName()); - - $extendConfig->set('state', 'Active'); + $extendConfigProvider = $this->get('oro_entity_config.provider.extend'); + $extendConfig = $extendConfigProvider->getConfig($entity->getClassName()); + $extendFieldConfigs = $extendConfigProvider->getConfigs($entity->getClassName()); + $entityState = $extendConfig->get('state'); + + foreach ($extendFieldConfigs as $fieldConfig) { + if ($fieldConfig->get('owner') != ExtendManager::OWNER_SYSTEM + && $fieldConfig->get('state') != ExtendManager::STATE_DELETED + ) { + $fieldConfig->set('state', ExtendManager::STATE_ACTIVE); + } - foreach ($extendConfig->getFields() as $field) { - if ($field->get('owner') != 'System') { - $field->set('state', 'Active'); + if ($fieldConfig->get('state') == ExtendManager::STATE_DELETED) { + $fieldConfig->set('is_deleted', true); } + + $extendConfigProvider->persist($fieldConfig); + } + + $extendConfigProvider->flush(); + + $extendConfig->set('state', $entityState); + if ($extendConfig->get('state') == ExtendManager::STATE_DELETED) { + $extendConfig->set('is_deleted', true); + } else { + $extendConfig->set('state', ExtendManager::STATE_ACTIVE); } $extendConfigProvider->persist($extendConfig); $extendConfigProvider->flush(); - return $this->redirect($this->generateUrl('oro_entityconfig_view', array('id' => $id))); + return $this->redirect($this->generateUrl('oro_entityconfig_index')); } } diff --git a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php index 3a956c4b018..b6a9f9991b6 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php +++ b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigEntityGridController.php @@ -2,23 +2,33 @@ namespace Oro\Bundle\EntityExtendBundle\Controller; +use FOS\Rest\Util\Codes; + use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\Response; + use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route; use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use Oro\Bundle\UserBundle\Annotation\Acl; -use Oro\Bundle\EntityConfigBundle\Config\FieldConfig; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; +use Oro\Bundle\EntityExtendBundle\Extend\ExtendManager; + +use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface; +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; + +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; +use Oro\Bundle\EntityExtendBundle\Form\Type\EntityType; use Oro\Bundle\EntityExtendBundle\Form\Type\UniqueKeyCollectionType; /** * Class ConfigGridController * @package Oro\Bundle\EntityExtendBundle\Controller - * @Route("/entityextend/entity") + * @Route("/entity/extend/entity") * @Acl( * id="oro_entityextend", * name="Entity extend manipulation", @@ -42,11 +52,12 @@ class ConfigEntityGridController extends Controller * ) * @Template */ - public function uniqueAction(ConfigEntity $entity) + public function uniqueAction(EntityConfigModel $entity) { /** @var ConfigProvider $configProvider */ - $configProvider = $this->get('oro_entity_extend.config.extend_config_provider'); + $configProvider = $this->get('oro_entity_config.provider.extend'); $entityConfig = $configProvider->getConfig($entity->getClassName()); + $fieldConfigIds = $configProvider->getIds($entity->getClassName()); $data = $entityConfig->has('unique_key') ? $entityConfig->get('unique_key') : array(); @@ -54,9 +65,10 @@ public function uniqueAction(ConfigEntity $entity) $form = $this->createForm( new UniqueKeyCollectionType( - $entityConfig->getFields( - function (FieldConfig $fieldConfig) { - return $fieldConfig->getType() != 'ref-many'; + array_filter( + $fieldConfigIds, + function (FieldConfigIdInterface $fieldConfigId) { + return $fieldConfigId->getFieldType() != 'ref-many'; } ) ), @@ -64,7 +76,7 @@ function (FieldConfig $fieldConfig) { ); if ($request->getMethod() == 'POST') { - $form->bind($request); + $form->submit($request); if ($form->isValid()) { $data = $form->getData(); @@ -78,15 +90,14 @@ function (FieldConfig $fieldConfig) { 'error', sprintf('Name for key should be unique, key "%s" is not unique.', $key['name']) ); + break; } if (empty($key['name'])) { $error = true; - $this->get('session')->getFlashBag()->add( - 'error', - 'Name of key can\'t be empty.' - ); + $this->get('session')->getFlashBag()->add('error', 'Name of key can\'t be empty.'); + break; } @@ -106,12 +117,162 @@ function (FieldConfig $fieldConfig) { } /** @var ConfigProvider $entityConfigProvider */ - $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + $entityConfigProvider = $this->get('oro_entity_config.provider.entity'); return array( 'form' => $form->createView(), 'entity_id' => $entity->getId(), - 'entity_config' => $entityConfigProvider->getConfig($entityConfig->getClassName()) + 'entity_config' => $entityConfigProvider->getConfig($entity->getClassName()) + ); + } + + /** + * @Route("/create", name="oro_entityextend_entity_create") + * @Acl( + * id="oro_entityextend_entity_create", + * name="Create custom entity", + * description="Create custom entity", + * parent="oro_entityextend" + * ) + * @Template + */ + public function createAction() + { + $request = $this->getRequest(); + + /** @var ConfigManager $configManager */ + $configManager = $this->get('oro_entity_config.config_manager'); + + $className = ''; + if ($request->getMethod() == 'POST') { + $className = $request->request->get('oro_entity_config_type[model][className]', null, true); + } + + $entityModel = $configManager->createConfigEntityModel($className); + $extendConfig = $configManager->getProvider('extend')->getConfig($className); + $extendConfig->set('owner', ExtendManager::OWNER_CUSTOM); + $extendConfig->set('state', ExtendManager::STATE_NEW); + $extendConfig->set('is_extend', true); + + $configManager->persist($extendConfig); + + $form = $this->createForm( + 'oro_entity_config_type', + null, + array( + 'config_model' => $entityModel, + ) + ); + + $cloneEntityModel = clone $entityModel; + $cloneEntityModel->setClassName(''); + $form->add( + 'model', + new EntityType, + array( + 'data' => $cloneEntityModel, + ) + ); + + if ($request->getMethod() == 'POST') { + $form->submit($request); + + if ($form->isValid()) { + //persist data inside the form + $this->get('session')->getFlashBag()->add('success', 'ConfigEntity successfully saved'); + + return $this->get('oro_ui.router')->actionRedirect( + array( + 'route' => 'oro_entityconfig_update', + 'parameters' => array('id' => $entityModel->getId()), + ), + array( + 'route' => 'oro_entityconfig_index' + ) + ); + } + } + + return array( + 'form' => $form->createView(), ); } + + /** + * @Route( + * "/remove/{id}", + * name="oro_entityextend_entity_remove", + * requirements={"id"="\d+"}, + * defaults={"id"=0} + * ) + * @Acl( + * id="oro_entityextend_entity_remove", + * name="Remove custom entity", + * description="Remove custom entity", + * parent="oro_entityextend" + * ) + */ + public function removeAction(EntityConfigModel $entity) + { + if (!$entity) { + throw $this->createNotFoundException('Unable to find EntityConfigModel entity.'); + } + + /** @var ExtendManager $extendManager */ + $extendManager = $this->get('oro_entity_extend.extend.extend_manager'); + /** @var ConfigManager $configManager */ + $configManager = $this->get('oro_entity_config.config_manager'); + + $entityConfig = $extendManager->getConfigProvider()->getConfig($entity->getClassName()); + + if ($entityConfig->get('owner') == ExtendManager::OWNER_SYSTEM) { + return new Response('', Codes::HTTP_FORBIDDEN); + } + + $entityConfig->set('state', ExtendManager::STATE_DELETED); + + $configManager->persist($entityConfig); + $configManager->flush(); + + return new JsonResponse(array('message' => 'Item was removed', 'successful' => true), Codes::HTTP_OK); + } + + /** + * @Route( + * "/unremove/{id}", + * name="oro_entityextend_entity_unremove", + * requirements={"id"="\d+"}, + * defaults={"id"=0} + * ) + * @Acl( + * id="oro_entityextend_entity_unremove", + * name="Unremove custom entity", + * description="Unremove custom entity", + * parent="oro_entityextend" + * ) + */ + public function unremoveAction(EntityConfigModel $entity) + { + if (!$entity) { + throw $this->createNotFoundException('Unable to find EntityConfigModel entity.'); + } + + /** @var ExtendManager $extendManager */ + $extendManager = $this->get('oro_entity_extend.extend.extend_manager'); + /** @var ConfigManager $configManager */ + $configManager = $this->get('oro_entity_config.config_manager'); + + $entityConfig = $extendManager->getConfigProvider()->getConfig($entity->getClassName()); + + if ($entityConfig->get('owner') == ExtendManager::OWNER_SYSTEM) { + return new Response('', Codes::HTTP_FORBIDDEN); + } + + $entityConfig->set('state', ExtendManager::STATE_UPDATED); + + $configManager->persist($entityConfig); + $configManager->flush(); + + return new JsonResponse(array('message' => 'Item was restored', 'successful' => true), Codes::HTTP_OK); + } } diff --git a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php index 45e28488c7d..08435e249bc 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php +++ b/src/Oro/Bundle/EntityExtendBundle/Controller/ConfigFieldGridController.php @@ -4,7 +4,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller; -use Symfony\Component\Form\FormError; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -12,18 +12,24 @@ use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template; use FOS\Rest\Util\Codes; + use Oro\Bundle\UserBundle\Annotation\Acl; + +use Oro\Bundle\EntityConfigBundle\Config\ConfigManager; + use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; + +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; +use Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel; + use Oro\Bundle\EntityExtendBundle\Form\Type\FieldType; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigEntity; -use Oro\Bundle\EntityConfigBundle\ConfigManager; use Oro\Bundle\EntityExtendBundle\Extend\ExtendManager; /** * Class ConfigGridController + * * @package Oro\Bundle\EntityExtendBundle\Controller - * @Route("/entityextend/field") + * @Route("/entity/extend/field") * @Acl( * id="oro_entityextend", * name="Entity extend manipulation", @@ -32,6 +38,10 @@ */ class ConfigFieldGridController extends Controller { + + const SESSION_ID_FIELD_TYPE = '_extendbundle_create_entity_%s_field_type'; + const SESSION_ID_FIELD_NAME = '_extendbundle_create_entity_%s_field_name'; + /** * @Route("/create/{id}", name="oro_entityextend_field_create", requirements={"id"="\d+"}, defaults={"id"=0}) * @Acl( @@ -40,9 +50,10 @@ class ConfigFieldGridController extends Controller * description="Update entity create custom field", * parent="oro_entityextend" * ) + * * @Template */ - public function createAction(ConfigEntity $entity) + public function createAction(EntityConfigModel $entity) { /** @var ExtendManager $extendManager */ $extendManager = $this->get('oro_entity_extend.extend.extend_manager'); @@ -50,56 +61,148 @@ public function createAction(ConfigEntity $entity) if (!$extendManager->isExtend($entity->getClassName())) { $this->get('session')->getFlashBag()->add('error', $entity->getClassName() . 'isn\'t extend'); - return $this->redirect($this->generateUrl('oro_entityconfig_fields', - array( - 'id' => $entity->getId() + return $this->redirect( + $this->generateUrl( + 'oro_entityconfig_fields', + array( + 'id' => $entity->getId() + ) ) - )); + ); } + $newFieldModel = new FieldConfigModel(); + $newFieldModel->setEntity($entity); + + $form = $this->createForm(new FieldType(), $newFieldModel); $request = $this->getRequest(); - $form = $this->createForm(new FieldType()); if ($request->getMethod() == 'POST') { $form->submit($request); if ($form->isValid()) { - $data = $form->getData(); - - if ($entity->getField($data['code'])) { - $form->get('code')->addError(new FormError(sprintf( - "Field '%s' already exist in entity '%s', ", $data['code'], $entity->getClassName() - ))); - } else { - $extendManager->getConfigFactory()->createFieldConfig($entity->getClassName(), $data); - - /** @var ConfigManager $configManager */ - $configManager = $this->get('oro_entity_config.config_manager'); - $configManager->clearCache($entity->getClassName()); + $request->getSession()->set( + sprintf(self::SESSION_ID_FIELD_NAME, $entity->getId()), + $newFieldModel->getFieldName() + ); + $request->getSession()->set( + sprintf(self::SESSION_ID_FIELD_TYPE, $entity->getId()), + $newFieldModel->getType() + ); - $this->get('session')->getFlashBag()->add('success', sprintf( - 'field "%s" has been added to entity "%', $data['code'], $entity->getClassName() - )); - - return $this->redirect($this->generateUrl('oro_entityconfig_field_update', + return $this->redirect( + $this->generateUrl( + 'oro_entityextend_field_update', array( - 'id' => $entity->getField($data['code'])->getId() + 'id' => $entity->getId() ) - )); - } + ) + ); + } } /** @var ConfigProvider $entityConfigProvider */ - $entityConfigProvider = $this->get('oro_entity.config.entity_config_provider'); + $entityConfigProvider = $this->get('oro_entity_config.provider.entity'); return array( - 'form' => $form->createView(), - 'entity_id' => $entity->getId(), + 'form' => $form->createView(), + 'entity_id' => $entity->getId(), 'entity_config' => $entityConfigProvider->getConfig($entity->getClassName()), ); } + /** + * @Route("/update/{id}", name="oro_entityextend_field_update", requirements={"id"="\d+"}, defaults={"id"=0}) + * @Acl( + * id="oro_entityextend_field_update", + * name="Update custom field", + * description="Update entity update custom field", + * parent="oro_entityextend" + * ) + */ + public function updateAction(EntityConfigModel $entity) + { + $request = $this->getRequest(); + + $fieldName = $request->getSession()->get(sprintf(self::SESSION_ID_FIELD_NAME, $entity->getId())); + $fieldType = $request->getSession()->get(sprintf(self::SESSION_ID_FIELD_TYPE, $entity->getId())); + + if (!$fieldName || !$fieldType) { + return $this->redirect( + $this->generateUrl( + 'oro_entityextend_field_create', + array( + 'id' => $entity->getId() + ) + ) + ); + } + + /** @var ConfigManager $configManager */ + $configManager = $this->get('oro_entity_config.config_manager'); + $newFieldModel = $configManager->createConfigFieldModel($entity->getClassName(), $fieldName, $fieldType); + + $extendFieldConfig = $configManager->getProvider('extend')->getConfig($entity->getClassName(), $fieldName); + $extendFieldConfig->set('owner', ExtendManager::OWNER_CUSTOM); + $extendFieldConfig->set('state', ExtendManager::STATE_NEW); + $extendFieldConfig->set('is_extend', true); + + $form = $this->createForm( + 'oro_entity_config_type', + null, + array( + 'config_model' => $newFieldModel, + ) + ); + + if ($request->getMethod() == 'POST') { + $form->submit($request); + + if ($form->isValid()) { + //persist data inside the form + $this->get('session')->getFlashBag()->add('success', 'ConfigField successfully saved'); + + $extendEntityConfig = $configManager->getProvider('extend')->getConfig($entity->getClassName()); + if ($extendEntityConfig->get('state') != ExtendManager::STATE_NEW) { + $extendEntityConfig->set('state', ExtendManager::STATE_UPDATED); + $configManager->persist($extendEntityConfig); + $configManager->flush(); + } + + return $this->get('oro_ui.router')->actionRedirect( + array( + 'route' => 'oro_entityconfig_field_update', + 'parameters' => array('id' => $newFieldModel->getId()), + ), + array( + 'route' => 'oro_entityconfig_view', + 'parameters' => array('id' => $entity->getId()) + ) + ); + } + } + + /** @var ConfigProvider $entityConfigProvider */ + $entityConfigProvider = $this->get('oro_entity_config.provider.entity'); + $entityConfig = $entityConfigProvider->getConfig($entity->getClassName()); + $fieldConfig = $entityConfigProvider->getConfig( + $entity->getClassName(), + $newFieldModel->getFieldName() + ); + + return $this->render( + 'OroEntityConfigBundle:Config:fieldUpdate.html.twig', + array( + 'entity_config' => $entityConfig, + 'field_config' => $fieldConfig, + 'field' => $newFieldModel, + 'form' => $form->createView(), + 'formAction' => $this->generateUrl('oro_entityextend_field_update', array('id' => $entity->getId())) + ) + ); + } + /** * @Route( * "/remove/{id}", @@ -114,28 +217,73 @@ public function createAction(ConfigEntity $entity) * parent="oro_entityextend" * ) */ - public function removeAction(ConfigField $field) + public function removeAction(FieldConfigModel $field) { if (!$field) { - throw $this->createNotFoundException('Unable to find ConfigField entity.'); + throw $this->createNotFoundException('Unable to find FieldConfigModel entity.'); } /** @var ExtendManager $extendManager */ $extendManager = $this->get('oro_entity_extend.extend.extend_manager'); + /** @var ConfigManager $configManager */ + $configManager = $this->get('oro_entity_config.config_manager'); + + $fieldConfig = $extendManager->getConfigProvider()->getConfig( + $field->getEntity()->getClassName(), + $field->getFieldName() + ); - $fieldConfig = $extendManager->getConfigProvider() - ->getFieldConfig($field->getEntity()->getClassName(), $field->getCode()); if (!$fieldConfig->is('is_extend')) { return new Response('', Codes::HTTP_FORBIDDEN); } - $this->getDoctrine()->getManager()->remove($field); - $this->getDoctrine()->getManager()->flush($field); + $fieldConfig->set('state', ExtendManager::STATE_DELETED); + + $configManager->persist($fieldConfig); + $configManager->flush(); + + return new JsonResponse(array('message' => 'Item was removed.', 'successful' => true), Codes::HTTP_OK); + } + + /** + * @Route( + * "/unremove/{id}", + * name="oro_entityextend_field_unremove", + * requirements={"id"="\d+"}, + * defaults={"id"=0} + * ) + * @Acl( + * id="oro_entityextend_field_unremove", + * name="UnRemove custom field", + * description="Update entity Unremove custom field", + * parent="oro_entityextend" + * ) + */ + public function unremoveAction(FieldConfigModel $field) + { + if (!$field) { + throw $this->createNotFoundException('Unable to find FieldConfigModel entity.'); + } + /** @var ExtendManager $extendManager */ + $extendManager = $this->get('oro_entity_extend.extend.extend_manager'); /** @var ConfigManager $configManager */ $configManager = $this->get('oro_entity_config.config_manager'); - $configManager->clearCache($fieldConfig->getClassName()); - return new Response('', Codes::HTTP_NO_CONTENT); + $fieldConfig = $extendManager->getConfigProvider()->getConfig( + $field->getEntity()->getClassName(), + $field->getFieldName() + ); + + if (!$fieldConfig->is('is_extend')) { + return new Response('', Codes::HTTP_FORBIDDEN); + } + + $fieldConfig->set('state', ExtendManager::STATE_UPDATED); + + $configManager->persist($fieldConfig); + $configManager->flush(); + + return new JsonResponse(array('message' => 'Item was restored', 'successful' => true), Codes::HTTP_OK); } } diff --git a/src/Oro/Bundle/EntityExtendBundle/DependencyInjection/OroEntityExtendExtension.php b/src/Oro/Bundle/EntityExtendBundle/DependencyInjection/OroEntityExtendExtension.php index 9a916e13e9a..6ab2d95fc4e 100644 --- a/src/Oro/Bundle/EntityExtendBundle/DependencyInjection/OroEntityExtendExtension.php +++ b/src/Oro/Bundle/EntityExtendBundle/DependencyInjection/OroEntityExtendExtension.php @@ -25,12 +25,10 @@ public function load(array $configs, ContainerBuilder $container) $config = $this->processConfiguration($configuration, $configs); $this->configBackend($container, $config); - $this->configCache($container, $config); $fileLocator = new FileLocator(__DIR__ . '/../Resources/config'); $loader = new Loader\YamlFileLoader($container, $fileLocator); $loader->load('services.yml'); - $loader->load('metadata.yml'); } protected function configBackend(ContainerBuilder $container, $config) @@ -44,32 +42,4 @@ protected function configBackend(ContainerBuilder $container, $config) // for DoctrineOrmMappingsPass end BackendCompilerPass. Detect with backend should be mapped and loaded $container->setParameter('oro_entity_extend.backend.' . strtolower($backend), true); } - - /** - * @param ContainerBuilder $container - * @param $config - * @throws RuntimeException - */ - protected function configCache(ContainerBuilder $container, $config) - { - $cacheDir = $container->getParameterBag()->resolveValue($config['cache_dir']); - - $fs = new Filesystem(); - $fs->remove($cacheDir); - - if (!is_dir($cacheDir)) { - if (false === @mkdir($cacheDir, 0777, true)) { - throw new RuntimeException(sprintf('Could not create cache directory "%s".', $cacheDir)); - } - } - $container->setParameter('oro_entity_extend.cache_dir', $cacheDir); - - $annotationCacheDir = $cacheDir . '/annotation'; - if (!is_dir($annotationCacheDir)) { - if (false === @mkdir($annotationCacheDir, 0777, true)) { - throw new RuntimeException(sprintf('Could not create annotation cache directory "%s".', $annotationCacheDir)); - } - } - $container->setParameter('oro_entity_extend.cache_dir.annotation', $annotationCacheDir); - } } diff --git a/src/Oro/Bundle/EntityExtendBundle/EventListener/ConfigSubscriber.php b/src/Oro/Bundle/EntityExtendBundle/EventListener/ConfigSubscriber.php index 23ecfe4130a..69d6f97e5f4 100644 --- a/src/Oro/Bundle/EntityExtendBundle/EventListener/ConfigSubscriber.php +++ b/src/Oro/Bundle/EntityExtendBundle/EventListener/ConfigSubscriber.php @@ -2,15 +2,14 @@ namespace Oro\Bundle\EntityExtendBundle\EventListener; -use Metadata\MetadataFactory; - use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Oro\Bundle\EntityConfigBundle\Event\PersistConfigEvent; -use Oro\Bundle\EntityConfigBundle\Event\NewEntityEvent; +use Oro\Bundle\EntityConfigBundle\Event\NewEntityConfigModelEvent; use Oro\Bundle\EntityConfigBundle\Event\Events; -use Oro\Bundle\EntityExtendBundle\Metadata\ExtendClassMetadata; +use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface; + use Oro\Bundle\EntityExtendBundle\Extend\ExtendManager; class ConfigSubscriber implements EventSubscriberInterface @@ -21,20 +20,11 @@ class ConfigSubscriber implements EventSubscriberInterface protected $extendManager; /** - * @var MetadataFactory + * @param ExtendManager $extendManager */ - protected $metadataFactory; - - protected $postFlushConfig = array(); - - /** - * @param ExtendManager $extendManager - * @param MetadataFactory $metadataFactory - */ - public function __construct(ExtendManager $extendManager, MetadataFactory $metadataFactory) + public function __construct(ExtendManager $extendManager) { - $this->extendManager = $extendManager; - $this->metadataFactory = $metadataFactory; + $this->extendManager = $extendManager; } /** @@ -43,31 +33,29 @@ public function __construct(ExtendManager $extendManager, MetadataFactory $metad public static function getSubscribedEvents() { return array( - Events::NEW_ENTITY => 'newEntityConfig', - Events::PERSIST_CONFIG => 'persistConfig', + Events::NEW_ENTITY_CONFIG_MODEL => 'newConfigModel', + Events::PRE_PERSIST_CONFIG => 'persistConfig', ); } /** - * @param NewEntityEvent $event + * @param NewEntityConfigModelEvent $event */ - public function newEntityConfig(NewEntityEvent $event) + public function newConfigModel(NewEntityConfigModelEvent $event) { - /** @var ExtendClassMetadata $metadata */ - $metadata = $this->metadataFactory->getMetadataForClass($event->getClassName()); - if ($metadata && $metadata->isExtend) { - $extendClass = $this->extendManager->getClassGenerator()->generateExtendClassName($event->getClassName()); - $proxyClass = $this->extendManager->getClassGenerator()->generateProxyClassName($event->getClassName()); - - $this->extendManager->getConfigProvider()->createEntityConfig( - $event->getClassName(), - $values = array( - 'is_extend' => true, - 'extend_class' => $extendClass, - 'proxy_class' => $proxyClass, - 'owner' => 'System', - ) + $config = $this->extendManager->getConfigProvider()->getConfig($event->getClassName()); + if ($config->get('is_extend')) { + $extendClass = $this->extendManager->getClassGenerator()->generateExtendClassName( + $event->getClassName() + ); + $proxyClass = $this->extendManager->getClassGenerator()->generateProxyClassName( + $event->getClassName() ); + + $config->set('extend_class', $extendClass); + $config->set('proxy_class', $proxyClass); + + $this->extendManager->getConfigProvider()->persist($config); } } @@ -76,19 +64,34 @@ public function newEntityConfig(NewEntityEvent $event) */ public function persistConfig(PersistConfigEvent $event) { - $event->getConfigManager()->calculateConfigChangeSet($event->getConfig()); $change = $event->getConfigManager()->getConfigChangeSet($event->getConfig()); - if ($event->getConfig()->getScope() == 'extend' + $scope = $event->getConfig()->getId()->getScope(); + $className = $event->getConfig()->getId()->getClassName(); + + if ($scope == 'extend' + && $event->getConfig()->getId() instanceof FieldConfigIdInterface && $event->getConfig()->is('is_extend') - && count(array_intersect_key(array_flip(array('length', 'precision', 'scale')), $change)) - && $event->getConfig()->get('state') != ExtendManager::STATE_NEW + && count(array_intersect_key(array_flip(array('length', 'precision', 'scale', 'state')), $change)) ) { - $entityConfig = $event->getConfigManager()->getProvider($event->getConfig()->getScope())->getConfig($event->getConfig()->getClassName()); - $event->getConfig()->set('state', ExtendManager::STATE_UPDATED); - $entityConfig->set('state', ExtendManager::STATE_UPDATED); + $entityConfig = $event->getConfigManager() + ->getProvider($scope) + ->getConfig($className); + + if ($event->getConfig()->get('state') != ExtendManager::STATE_NEW + && $event->getConfig()->get('state') != ExtendManager::STATE_DELETED + && !isset($change['state']) + ) { + $event->getConfig()->set('state', ExtendManager::STATE_UPDATED); + + $event->getConfigManager()->calculateConfigChangeSet($event->getConfig()); + } + + if ($entityConfig->get('state') != ExtendManager::STATE_NEW) { + $entityConfig->set('state', ExtendManager::STATE_UPDATED); - $event->getConfigManager()->persist($entityConfig); + $event->getConfigManager()->persist($entityConfig); + } } } } diff --git a/src/Oro/Bundle/EntityExtendBundle/Extend/ExtendFactory.php b/src/Oro/Bundle/EntityExtendBundle/Extend/ExtendFactory.php new file mode 100644 index 00000000000..f0432103e53 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Extend/ExtendFactory.php @@ -0,0 +1,68 @@ +extendManager = $extendManager; + } + + /** + * @param string $entityName + * @param string $fieldName + * @param string $fieldConfig + * @param string $mode + */ + public function createField($entityName, $fieldName, $fieldConfig, $mode = ConfigModelManager::MODE_DEFAULT) + { + $configProvider = $this->extendManager->getConfigProvider(); + $configManager = $this->extendManager->getConfigProvider()->getConfigManager(); + + $configManager->createConfigFieldModel($entityName, $fieldName, $fieldConfig['type'], $mode); + + $extendFieldConfig = $configProvider->getConfig($entityName, $fieldName); + $extendFieldConfig->set('owner', ExtendManager::OWNER_CUSTOM); + $extendFieldConfig->set('state', ExtendManager::STATE_NEW); + $extendFieldConfig->set('is_extend', true); + + if (isset($fieldConfig['options'])) { + foreach ($fieldConfig['options'] as $key => $value) { + $extendFieldConfig->set($key, $value); + } + } + + $configManager->persist($extendFieldConfig); + } + + /** + * @param string $entityName + * @param string $mode + */ + public function createEntity($entityName, $mode = ConfigModelManager::MODE_DEFAULT) + { + $configProvider = $this->extendManager->getConfigProvider(); + $configManager = $this->extendManager->getConfigProvider()->getConfigManager(); + + $configManager->createConfigEntityModel($entityName, $mode); + + $extendConfig = $configProvider->getConfig($entityName); + $extendConfig->set('owner', ExtendManager::OWNER_CUSTOM); + $extendConfig->set('state', ExtendManager::STATE_NEW); + $extendConfig->set('is_extend', true); + + $configManager->persist($extendConfig); + + $entityFieldConfig = $configManager->getProvider('entity')->getConfig($entityName, 'id'); + $entityFieldConfig->set('label', 'Id'); + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Extend/ExtendManager.php b/src/Oro/Bundle/EntityExtendBundle/Extend/ExtendManager.php index 2d37b76d276..1b97c8a19c7 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Extend/ExtendManager.php +++ b/src/Oro/Bundle/EntityExtendBundle/Extend/ExtendManager.php @@ -2,16 +2,15 @@ namespace Oro\Bundle\EntityExtendBundle\Extend; -use Oro\Bundle\EntityConfigBundle\DependencyInjection\Proxy\ServiceProxy; +use Oro\Bundle\EntityConfigBundle\DependencyInjection\Utils\ServiceLink; use Oro\Bundle\EntityExtendBundle\Entity\ExtendProxyInterface; -use Oro\Bundle\EntityExtendBundle\Extend\Factory\ConfigFactory; use Oro\Bundle\EntityExtendBundle\Tools\Generator\Generator; use Oro\Bundle\EntityConfigBundle\Provider\ConfigProvider; class ExtendManager { const STATE_NEW = 'New'; - const STATE_UPDATED = 'Updated'; + const STATE_UPDATED = 'Requires update'; const STATE_ACTIVE = 'Active'; const STATE_DELETED = 'Deleted'; @@ -24,9 +23,9 @@ class ExtendManager protected $proxyFactory; /** - * @var ConfigFactory + * @var ExtendFactory */ - protected $configFactory; + protected $extendFactory; /** * @var ConfigProvider @@ -39,17 +38,17 @@ class ExtendManager protected $generator; /** - * @var ServiceProxy + * @var ServiceLink */ protected $lazyEm; - public function __construct(ServiceProxy $lazyEm, ConfigProvider $configProvider, $backend, $entityCacheDir) + public function __construct(ServiceLink $lazyEm, ConfigProvider $configProvider, $backend, $entityCacheDir) { $this->lazyEm = $lazyEm; $this->configProvider = $configProvider; $this->proxyFactory = new ProxyObjectFactory($this); - $this->configFactory = new ConfigFactory($this); $this->generator = new Generator($configProvider, $backend, $entityCacheDir); + $this->extendFactory = new ExtendFactory($this); } /** @@ -77,11 +76,11 @@ public function getProxyFactory() } /** - * @return ConfigFactory + * @return ExtendFactory */ - public function getConfigFactory() + public function getExtendFactory() { - return $this->configFactory; + return $this->extendFactory; } /** @@ -99,7 +98,7 @@ public function getClassGenerator() public function isExtend($entityName) { if ($entityName - && $this->configProvider->hasConfig($entityName) + && $this->configProvider->isConfigurable($entityName) && $this->configProvider->getConfig($entityName)->is('is_extend') ) { return true; diff --git a/src/Oro/Bundle/EntityExtendBundle/Extend/Factory/ConfigFactory.php b/src/Oro/Bundle/EntityExtendBundle/Extend/Factory/ConfigFactory.php deleted file mode 100644 index ca6fab522ce..00000000000 --- a/src/Oro/Bundle/EntityExtendBundle/Extend/Factory/ConfigFactory.php +++ /dev/null @@ -1,62 +0,0 @@ -extendManager = $extendManager; - } - - public function createFieldConfig($className, $data) - { - $values = array(); - - $values['is_extend'] = true; - $values['owner'] = ExtendManager::OWNER_CUSTOM; - $values['state'] = ExtendManager::STATE_NEW; - - $constraint = array( - 'property' => array(), - 'constraint' => array() - ); - - if ($data['type'] == 'string') { - $constraint['property']['Symfony\Component\Validator\Constraints\Length'] = array('max' => 255); - } - - - if ($data['type'] == 'datetime') { - $constraint['property']['Symfony\Component\Validator\Constraints\DateTime'] = array(); - } - - if ($data['type'] == 'date') { - $constraint['property']['Symfony\Component\Validator\Constraints\Date'] = array(); - } - - $values['constraint'] = serialize($constraint); - - $entityConfig = $this->extendManager->getConfigProvider()->getConfig($className); - $entityConfig->set('state', ExtendManager::STATE_UPDATED); - $this->extendManager->getConfigProvider()->persist($entityConfig); - - $this->extendManager->getConfigProvider()->createFieldConfig( - $className, - $data['code'], - $data['type'], - $values, - true - ); - } -} diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Type/EntityType.php b/src/Oro/Bundle/EntityExtendBundle/Form/Type/EntityType.php new file mode 100644 index 00000000000..08941f13812 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Type/EntityType.php @@ -0,0 +1,50 @@ +add( + 'className', + 'text', + array( + 'label' => 'Name', + 'block' => 'entity', + 'subblock' => 'second' + ) + ); + } + + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'data_class' => 'Oro\Bundle\EntityConfigBundle\Entity\EntityConfigModel', + 'block_config' => array( + 'entity' => array( + 'title' => 'General', + 'subblocks' => array( + 'second' => array( + 'priority' => 10 + ) + ) + ) + ) + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_entity_extend_entity_type'; + } +} diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Type/FieldType.php b/src/Oro/Bundle/EntityExtendBundle/Form/Type/FieldType.php index 1727cda86fb..715e7730702 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Form/Type/FieldType.php +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Type/FieldType.php @@ -25,27 +25,37 @@ class FieldType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { - $builder->add('code', 'text', array( - 'label' => 'Field Name', - 'block' => 'type', - )); - $builder->add('type', 'choice', array( - 'choices' => array_combine(array_values($this->types), $this->types), - 'empty_value' => 'Please choice type...', - 'block' => 'type', - )); + $builder->add( + 'fieldName', + 'text', + array( + 'label' => 'Field Name', + 'block' => 'type', + ) + ); + $builder->add( + 'type', + 'choice', + array( + 'choices' => array_combine(array_values($this->types), $this->types), + 'empty_value' => 'Please choice type...', + 'block' => 'type', + ) + ); } public function setDefaultOptions(OptionsResolverInterface $resolver) { - $resolver->setDefaults(array( - 'block_config' => array( - 'type' => array( - 'title' => 'Doctrine Type', - 'priority' => 1, + $resolver->setDefaults( + array( + 'block_config' => array( + 'type' => array( + 'title' => 'General', + 'priority' => 1, + ) ) ) - )); + ); } /** diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyCollectionType.php b/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyCollectionType.php index 44d8f2253d6..7a117dc8c69 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyCollectionType.php +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyCollectionType.php @@ -5,13 +5,14 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; -use Oro\Bundle\EntityConfigBundle\Config\FieldConfig; +use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface; + use Oro\Bundle\EntityExtendBundle\Form\Type\UniqueKeyType; class UniqueKeyCollectionType extends AbstractType { /** - * @var FieldConfig[] + * @var FieldConfigIdInterface[] */ protected $fields; diff --git a/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyType.php b/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyType.php index edc7416d13e..9be7b84834b 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyType.php +++ b/src/Oro/Bundle/EntityExtendBundle/Form/Type/UniqueKeyType.php @@ -2,17 +2,16 @@ namespace Oro\Bundle\EntityExtendBundle\Form\Type; -use Doctrine\Common\Collections\ArrayCollection; - use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; -use Oro\Bundle\EntityConfigBundle\Config\FieldConfig; +use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface; class UniqueKeyType extends AbstractType { /** - * @var FieldConfig[]|ArrayCollection + * @var FieldConfigIdInterface[] + */ protected $fields; @@ -23,9 +22,14 @@ public function __construct($fields) public function buildForm(FormBuilderInterface $builder, array $options) { - $choices = $this->fields->map(function (FieldConfig $field) { - return ucfirst($field->getCode()); - }); + $choices = array_map( + function (FieldConfigIdInterface $field) { + return ucfirst($field->getFieldName()); + }, + $this->fields + ); + + $choices = array_combine($choices, $choices); $builder->add( 'name', @@ -40,7 +44,7 @@ public function buildForm(FormBuilderInterface $builder, array $options) 'choice', array( 'multiple' => true, - 'choices' => $choices->toArray(), + 'choices' => $choices, 'required' => true, ) ); diff --git a/src/Oro/Bundle/EntityExtendBundle/Metadata/Annotation/Extend.php b/src/Oro/Bundle/EntityExtendBundle/Metadata/Annotation/Extend.php deleted file mode 100644 index 3e7b4607adf..00000000000 --- a/src/Oro/Bundle/EntityExtendBundle/Metadata/Annotation/Extend.php +++ /dev/null @@ -1,13 +0,0 @@ -reader = $reader; - } - - /** - * {@inheritdoc} - */ - public function loadMetadataForClass(\ReflectionClass $class) - { - $metadata = new ExtendClassMetadata($class->getName()); - - if ($this->reader->getClassAnnotation($class, self::EXTEND)) { - $metadata->isExtend = true; - } - - return $metadata; - } -} diff --git a/src/Oro/Bundle/EntityExtendBundle/Metadata/ExtendClassMetadata.php b/src/Oro/Bundle/EntityExtendBundle/Metadata/ExtendClassMetadata.php deleted file mode 100644 index c4a66287e9f..00000000000 --- a/src/Oro/Bundle/EntityExtendBundle/Metadata/ExtendClassMetadata.php +++ /dev/null @@ -1,50 +0,0 @@ -isExtend = $object->isExtend; - } - } - - /** - * {@inheritdoc} - */ - public function serialize() - { - return serialize(array( - $this->isExtend, - parent::serialize(), - )); - } - - /** - * {@inheritdoc} - */ - public function unserialize($str) - { - list( - $this->isExtend, - $parentStr - ) = unserialize($str); - - parent::unserialize($parentStr); - } -} diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/config/assets.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/config/assets.yml index fce1b9b9717..ac511acb44d 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/config/assets.yml @@ -1,7 +1,6 @@ js: 'entityextend': - '@OroEntityExtendBundle/Resources/public/js/extend.apply.js' - - '@OroEntityExtendBundle/Resources/public/js/extend.field.create.js' css: 'entityextend': - '@OroEntityExtendBundle/Resources/public/css/extend.css' diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/config/entity_config.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/config/entity_config.yml index 7f821b4f981..ec7f673b72d 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Resources/config/entity_config.yml +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/config/entity_config.yml @@ -1,6 +1,27 @@ oro_entity_config: extend: entity: + grid_action: + - + name: 'Remove' + route: 'oro_entityextend_entity_remove' + type: 'ajax' + icon: 'trash' + acl_resource: 'root' + filter: { owner: 'Custom', state: ['New', 'Requires update', 'Active']} + args: ['id'] + - + name: 'Restore' + route: 'oro_entityextend_entity_unremove' + type: 'ajax' + icon: 'backward' + acl_resource: 'root' + filter: { owner: 'Custom', state: ['Deleted']} + args: ['id'] + layout_action: + - + name: 'Create Entity' + route: 'oro_entityextend_entity_create' items: owner: options: @@ -22,46 +43,85 @@ oro_entity_config: read_only: true required: false label: 'Type' - view: - type: string - stats: false - block: + is_extend: options: default_value: false internal: true + state: options: priority: 25 default_value: 'Active' - label: 'Status' + label: 'Schema status' grid: type: string - label: 'Status' + label: 'Schema status' filter_type: oro_grid_orm_choice - choices: {system: '', new: 'New', active: 'Active', updated: 'Updated', deleted: 'To be deleted'} + choices: {system: '', new: 'New', active: 'Active', updated: 'Requires update', deleted: 'To be deleted'} required: true sortable: true filterable: false show_filter: false + + is_deleted: + options: + default_value: false + internal: true + grid: + show_column: false + query: + operator: '!=' + value: true + extend_class: options: internal: true + unique_key: options: internal: true serializable: true + proxy_class: options: internal: true field: grid_action: - - {'name':'Remove', 'route':'oro_entityextend_field_remove', 'type':'delete', 'icon':'trash', 'acl_resource':'root', 'filter':'is_extend', 'args':['id']} + - + name: 'Remove' + route: 'oro_entityextend_field_remove' + type: 'ajax' + icon: 'trash' + acl_resource: 'root' + filter: { owner: 'Custom', state: ['New', 'Requires update', 'Active'] } + args: ['id'] + - + name: 'Resore' + route: 'oro_entityextend_field_unremove' + type: 'ajax' + icon: 'backward' + acl_resource: 'root' + filter: { owner: 'Custom', state: ['Deleted'] } + args: ['id'] layout_action: - - {'name':'Manage unique keys', 'route':'oro_entityextend_entity_unique_key', 'entity_id':true} - - {'name':'Create field', 'route':'oro_entityextend_field_create', 'entity_id':true, 'filter':'is_extend'} - - {'name':'Update schema', 'route':'oro_entityextend_update', 'entity_id':true, 'aClass':'btn-danger entity-extend-apply', 'void':true} + - + name: 'Manage unique keys' + route: 'oro_entityextend_entity_unique_key' + entity_id: true + - + name: 'Create field' + route: 'oro_entityextend_field_create' + entity_id: true + filter: { is_extend: true } + - + name: 'Update schema' + route: 'oro_entityextend_update' + entity_id: true + aClass: 'btn-danger entity-extend-apply' + void: true + filter: { state: ['New', 'Requires update', 'Deleted'] } items: owner: options: @@ -85,12 +145,12 @@ oro_entity_config: options: priority: 25 default_value: 'Active' - label: 'Status' + label: 'Schema status' grid: type: string - label: 'Status' + label: 'Schema status' filter_type: oro_grid_orm_choice - choices: { new: 'New', applied: 'Applied', updated: 'Updated', deleted: 'To be deleted'} + choices: { new: 'New', applied: 'Applied', updated: 'Requires update', deleted: 'To be deleted'} required: true sortable: true filterable: false @@ -100,66 +160,78 @@ oro_entity_config: options: priority: 40 default_value: false - is_bool: true internal: true -# grid: -# type: boolean -# label: 'Is Extend Field' -# filter_type: oro_grid_orm_boolean -# required: true -# sortable: true -# filterable: false -# show_filter: false -# form: -# type: choice -# block: entity -# options: -# choices: ['No', 'Yes'] -# read_only: true -# required: false -# label: "Is Extend Field" length: options: default_value: 255 + allowed_type: [string] required_property: - property_path: extend.owner + config_id: + scope: extend + code: owner value: Custom + constraints: + - Regex: "/^[0-9]*$/" + - Range: + min: 1 + max: 255 form: type: text options: required: false label: 'Length' - allowed_type: 'string' block: entity subblock: properties precision: options: default_value: 2 + allowed_type: [decimal] required_property: - property_path: extend.owner + config_id: + scope: extend + code: owner value: Custom + constraints: + - Regex: "/^[0-9]*$/" + - Range: + min: 0 form: type: text options: required: false label: 'Precision' - allowed_type: 'decimal' block: entity subblock: properties scale: options: default_value: 2 + allowed_type: [decimal] required_property: - property_path: extend.owner + config_id: + scope: extend + code: owner value: Custom + constraints: + - Regex: "/^[0-9]*$/" + - Range: + min: 0 form: type: text options: required: false label: 'Scale' - allowed_type: 'decimal' block: entity subblock: properties + + is_deleted: + options: + default_value: false + internal: true + grid: + show_column: false + query: + operator: '!=' + value: true diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/config/metadata.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/config/metadata.yml deleted file mode 100644 index f03d1322495..00000000000 --- a/src/Oro/Bundle/EntityExtendBundle/Resources/config/metadata.yml +++ /dev/null @@ -1,22 +0,0 @@ -parameters: - oro_entity_extend.metadata.metadata_factory.class: Metadata\MetadataFactory - oro_entity_extend.metadata.annotation_driver.class: Oro\Bundle\EntityExtendBundle\Metadata\Driver\AnnotationDriver - oro_entity_extend.metadata.cache.file_cache.class: Metadata\Cache\FileCache - -services: - oro_entity_extend.metadata.annotation_metadata_factory: - class: %oro_entity_extend.metadata.metadata_factory.class% - arguments: [@oro_entity_extend.metadata.annotation_driver] - public: false - calls: - - [setCache, [@oro_entity_extend.metadata.cache.file_cache.annotation]] - - oro_entity_extend.metadata.annotation_driver: - class: %oro_entity_extend.metadata.annotation_driver.class% - arguments: [@annotation_reader] - public: false - - oro_entity_extend.metadata.cache.file_cache.annotation: - class: %oro_entity_extend.metadata.cache.file_cache.class% - arguments: [%oro_entity_extend.cache_dir.annotation%] - public: false diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml b/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml index 826dfc8a341..0de8cc9d0aa 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/config/services.yml @@ -11,15 +11,18 @@ parameters: services: oro_entity_extend.extend.extend_manager: class: %oro_entity_extend.extend.extend_manager.class% - arguments: [@oro_entity_config.proxy.entity_manager, @oro_entity_extend.config.extend_config_provider, %oro_entity_extend.backend%, %kernel.root_dir%/entities] - - oro_entity_extend.config.extend_config_provider: - tags: - - { name: oro_entity_config.provider, scope: extend } + arguments: + - @oro_entity_config.link.entity_manager + - @oro_entity_config.provider.extend + - %oro_entity_extend.backend% + - %kernel.root_dir%/entities oro_entity_extend.tools.schema: class: %oro_entity_extend.tools.schema.class% - arguments: [@doctrine.orm.default_entity_manager, %oro_entity_extend.backend%, @oro_entity_extend.extend.extend_manager] + arguments: + - @doctrine.orm.default_entity_manager + - %oro_entity_extend.backend% + - @oro_entity_extend.extend.extend_manager oro_entity_extend.listener.doctrine_subscriber: class: %oro_entity_extend.listener.doctrine_subscriber.class% @@ -29,7 +32,8 @@ services: oro_entity_extend.listener.config_subscriber: class: %oro_entity_extend.listener.config_subscriber.class% - arguments: [@oro_entity_extend.extend.extend_manager, @oro_entity_extend.metadata.annotation_metadata_factory] + arguments: + - @oro_entity_extend.extend.extend_manager tags: - { name: kernel.event_subscriber} diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.field.create.js b/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.field.create.js deleted file mode 100644 index 811a84594a1..00000000000 --- a/src/Oro/Bundle/EntityExtendBundle/Resources/public/js/extend.field.create.js +++ /dev/null @@ -1,17 +0,0 @@ -$(function() { - var select = 'form#oro_entity_extend_field_type select#oro_entity_extend_field_type_type'; - $(select).change(function() { - var selected = $(select + ' option:selected').attr('value'); - $('div#oro_entity_extend_field_type_options_extend input[data-allowedtype]').each(function(index, el) { - if ($(el).data('allowedtype').indexOf(selected) != -1) { - - $(el).removeClass('hide'); - $(el).parents('.control-group:first').removeClass('hide'); - } - else { - $(el).addClass('hide'); - $(el).parents('.control-group:first').addClass('hide'); - } - }) - }) -}); diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigEntityGrid/create.html.twig b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigEntityGrid/create.html.twig new file mode 100644 index 00000000000..03af5399ed5 --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigEntityGrid/create.html.twig @@ -0,0 +1,40 @@ +{% extends 'OroUIBundle:actions:update.html.twig' %} +{% form_theme form with 'OroUIBundle:Form:fields.html.twig' %} +{% oro_title_set({params : {"%entityName%": 'N/A' }}) %} + +{% set title = 'Create Entity' %} +{% set formAction = path('oro_entityextend_entity_create') %} + +{% block navButtons %} + {{ UI.button({'path' : path('oro_entityconfig_index'), 'title' : 'Cancel', 'label' : 'Cancel'}) }} + + {{ UI.saveAndStayButton() }} + {{ UI.saveAndCloseButton() }} +{% endblock navButtons %} + +{% block pageHeader %} + {% set breadcrumbs = { + 'entity': 'entity', + 'indexPath': path('oro_entityconfig_index'), + 'indexLabel': 'Entities', + 'entityTitle': 'N/A', + } %} + + {{ parent() }} +{% endblock pageHeader %} + +{% block stats %} + {{ parent() }} +{% endblock stats %} + +{% block content_data %} + {% set id = 'configentity-create' %} + {% set dataBlocks = form_data_blocks(form) %} + {% set data = { + 'formErrors': form_errors(form)? form_errors(form) : null, + 'dataBlocks': dataBlocks, + 'hiddenData': form_rest(form) + }%} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigEntityGrid/update.html.twig b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigEntityGrid/update.html.twig new file mode 100644 index 00000000000..8137a80024a --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigEntityGrid/update.html.twig @@ -0,0 +1,44 @@ +{% extends 'OroUIBundle:actions:update.html.twig' %} +{% form_theme form with 'OroUIBundle:Form:fields.html.twig' %} +{% oro_title_set({params : {"%entityName%": entity_config.get('label')|default('N/A') }}) %} + +{% set title = 'Update Entity' %} +{% set formAction = path('oro_entityextend_entity_update') %} + +{% set entityClass = entity.classname|replace('\\', '_') %} +{% set audit_title = entity_config.get('label')|default('N/A') %} +{% set audit_path = 'oro_entityconfig_audit' %} + +{% block navButtons %} + {{ UI.button({'path' : path('oro_entityconfig_index'), 'title' : 'Cancel', 'label' : 'Cancel'}) }} + + {{ UI.saveAndStayButton() }} + {{ UI.saveAndCloseButton() }} +{% endblock navButtons %} + +{% block pageHeader %} + {% set breadcrumbs = { + 'entity': 'entity', + 'indexPath': path('oro_entityconfig_index'), + 'indexLabel': 'Entities', + 'entityTitle': entity_config.get('label')|default('N/A'), + } %} + + {{ parent() }} +{% endblock pageHeader %} + +{% block stats %} + {{ parent() }} +{% endblock stats %} + +{% block content_data %} + {% set id = 'configentity-update' %} + {% set dataBlocks = form_data_blocks(form) %} + {% set data = { + 'formErrors': form_errors(form)? form_errors(form) : null, + 'dataBlocks': dataBlocks, + 'hiddenData': form_rest(form) + }%} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/create.html.twig b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/create.html.twig index 5294484f96e..a974c4a23d0 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/create.html.twig +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/create.html.twig @@ -8,7 +8,7 @@ {% block navButtons %} {{ UI.button({'path' : path('oro_entityconfig_view', {'id':entity_id}), 'title' : 'Cancel', 'label' : 'Cancel'}) }} - {{ UI.buttonType({'type': 'submit', 'class': 'btn-success', 'label': 'Save'}) }} + {{ UI.buttonType({'type': 'submit', 'class': 'btn-success', 'label': 'Continue'}) }} {% endblock navButtons %} {% block pageHeader %} @@ -30,23 +30,10 @@ {% block content_data %} {% set id = 'configfield-create' %} - {% set dataBlocks = [ - { - 'title': 'Doctrine Type', - 'class': 'active', - 'subblocks': [{ - 'title': '', - 'data': [ - form_row(form.code), - form_row(form.type), - ] - }] - } - ] %} {% set data = { 'formErrors': form_errors(form)? form_errors(form) : null, - 'dataBlocks': dataBlocks, + 'dataBlocks': form_data_blocks(form), 'hiddenData': form_rest(form) } %} diff --git a/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/update.html.twig b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/update.html.twig new file mode 100644 index 00000000000..8f39c76505b --- /dev/null +++ b/src/Oro/Bundle/EntityExtendBundle/Resources/views/ConfigFieldGrid/update.html.twig @@ -0,0 +1,41 @@ +{% extends 'OroUIBundle:actions:update.html.twig' %} +{% form_theme form with 'OroUIBundle:Form:fields.html.twig' %} + +{% set title = 'Add Field ' %} +{% oro_title_set({params : {"%username%": 'User' }}) %} + +{% set formAction = path('oro_entityextend_field_create', {id: entity_id}) %} + +{% block navButtons %} + {{ UI.button({'path' : path('oro_entityconfig_view', {'id':entity_id}), 'title' : 'Cancel', 'label' : 'Cancel'}) }} + {{ UI.buttonType({'type': 'submit', 'class': 'btn-success', 'label': 'Save'}) }} +{% endblock navButtons %} + +{% block pageHeader %} + {% set breadcrumbs = { + 'entity': 'entity', + 'indexPath': path('oro_entityconfig_index'), + 'indexLabel': 'Entities', + 'entityTitle': 'New Field', + 'additional': [ + { + 'indexPath' : path('oro_entityconfig_view', {id: entity_id}), + 'indexLabel' : entity_config.get('label')|default('N/A') + }, + ] + } %} + + {{ parent() }} +{% endblock pageHeader %} + +{% block content_data %} + {% set id = 'configfield-create' %} + + {% set data = { + 'formErrors': form_errors(form)? form_errors(form) : null, + 'dataBlocks': form_data_blocks(form), + 'hiddenData': form_rest(form) + } %} + + {{ parent() }} +{% endblock content_data %} diff --git a/src/Oro/Bundle/EntityExtendBundle/Tools/Generator/Generator.php b/src/Oro/Bundle/EntityExtendBundle/Tools/Generator/Generator.php index be3cdda2e8d..6ea84dc5d46 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tools/Generator/Generator.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tools/Generator/Generator.php @@ -2,6 +2,8 @@ namespace Oro\Bundle\EntityExtendBundle\Tools\Generator; +use Oro\Bundle\EntityExtendBundle\Extend\ExtendManager; +use Oro\Bundle\EntityConfigBundle\Config\ConfigModelManager; use Symfony\Component\Yaml\Yaml; use CG\Core\DefaultGeneratorStrategy; @@ -48,66 +50,170 @@ public function __construct(ConfigProvider $configProvider, $backend, $entityCac } /** - * @param $entityName + * @param $entityName * @param bool $force + * @param bool $extend */ - public function checkEntityCache($entityName, $force = false) + public function checkEntityCache($entityName, $force = false, $extend = true) { $extendClass = $this->generateExtendClassName($entityName); $proxyClass = $this->generateProxyClassName($entityName); - - $validator = $this->entityCacheDir. DIRECTORY_SEPARATOR . 'validator.yml'; - if (!file_exists($validator)) { - file_put_contents( - $validator, - '' - ); - } - //$validatorYml = Yaml::parse($validator); + $validators = $this->entityCacheDir . DIRECTORY_SEPARATOR . 'validator.yml'; if ((!class_exists($extendClass) || !class_exists($proxyClass)) || $force) { /** write Dynamic class */ file_put_contents( - $this->entityCacheDir. DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $extendClass) . '.php', + $this->entityCacheDir . DIRECTORY_SEPARATOR . str_replace( + '\\', + DIRECTORY_SEPARATOR, + $extendClass + ) . '.php', "generateDynamicClass($entityName, $extendClass) ); /** write Dynamic yml */ file_put_contents( - $this->entityCacheDir. DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $extendClass) . '.orm.yml', + $this->entityCacheDir . DIRECTORY_SEPARATOR . str_replace( + '\\', + DIRECTORY_SEPARATOR, + $extendClass + ) . '.orm.yml', Yaml::dump($this->generateDynamicYml($entityName, $extendClass), 5) ); /** write Proxy class */ file_put_contents( - $this->entityCacheDir. DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $proxyClass) . '.php', - "generateProxyClass($entityName, $proxyClass) + $this->entityCacheDir . DIRECTORY_SEPARATOR . str_replace( + '\\', + DIRECTORY_SEPARATOR, + $proxyClass + ) . '.php', + "generateProxyClass($entityName, $proxyClass, $extend) ); } + + if (!file_exists($validators)) { + file_put_contents($validators, ''); + } + + file_put_contents( + $validators, + Yaml::dump($this->generateValidator($entityName, Yaml::parse($validators))) + ); } + /** + * @param $entityName + * @return string + */ public function generateExtendClassName($entityName) { return 'Extend\\Entity\\' . $this->backend . '\\' . $this->generateClassName($entityName); } + /** + * @param $entityName + * @return string + */ public function generateProxyClassName($entityName) { return 'Extend\\Proxy\\' . $this->generateClassName($entityName); } + /** + * @param $entityName + * @return string + */ protected function generateClassName($entityName) { return str_replace('\\', '', $entityName); } + /** + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @param $entityName + * @param $validators + * @return mixed + */ + protected function generateValidator($entityName, $validators) + { + /** Constraints */ + $yml['constraints'] = array(); + + $uniqueKeys = $this->configProvider->getConfig($entityName)->get('unique_key'); + if ($uniqueKeys) { + foreach ($uniqueKeys['keys'] as $keys) { + $yml['constraints'][]['ExtendUniqueEntity'] = $keys['key']; + } + } + + /** properties */ + $yml['properties'] = array(); + + if ($fieldIds = $this->configProvider->getIds($entityName)) { + foreach ($fieldIds as $fieldId) { + if ($this->configProvider->getConfigById($fieldId)->is('is_extend')) { + $config = $this->configProvider->getConfigById($fieldId); + switch ($fieldId->getFieldType()) { + case 'integer': + case 'smallint': + case 'bigint': + $yml['properties'][$fieldId->getfieldName()][] = array( + 'Regex' => '/\d+/' + ); + break; + case 'string': + $yml['properties'][$fieldId->getfieldName()][] = array( + 'Length' => array('max' => $config->get('length')) + ); + break; + case 'decimal': + $yml['properties'][$fieldId->getfieldName()][] = array( + 'Regex' => '/\d{1,' . $config->get('precision') . '}\.\d{1,' . $config->get( + 'scale' + ) . '}/' + ); + break; + case 'date': + $yml['properties'][$fieldId->getfieldName()][] = array( + 'Date' => '~' + ); + break; + case 'time': + $yml['properties'][$fieldId->getfieldName()][] = array( + 'Time' => '~' + ); + break; + case 'datetime': + $yml['properties'][$fieldId->getfieldName()][] = array( + 'DateTime' => '~' + ); + break; + case 'boolean': + case 'text': + case 'float': + break; + } + } + } + } + + $validators[$entityName] = $yml; + + return $validators; + } + + /** + * @param $entityName + * @param $extendClass + * @return array + */ protected function generateDynamicYml($entityName, $extendClass) { $yml = array( $extendClass => array( 'type' => 'entity', 'table' => 'oro_extend_' . strtolower(str_replace('\\', '', $entityName)), - 'oneToOne' => array( '__extend__parent' => array( 'targetEntity' => $entityName, @@ -130,17 +236,21 @@ protected function generateDynamicYml($entityName, $extendClass) ) ); - if ($fields = $this->configProvider->getConfig($entityName)->getFields()) { - foreach ($fields as $field => $options) { - if ($this->configProvider->getFieldConfig($entityName, $field)->is('is_extend')) { - $yml[$extendClass]['fields'][$field]['code'] = $options->getCode(); - $yml[$extendClass]['fields'][$field]['type'] = $options->getType(); + if ($fieldIds = $this->configProvider->getIds($entityName)) { + foreach ($fieldIds as $fieldId) { + if ($this->configProvider->getConfigById($fieldId)->is('is_extend') + //&& $this->configProvider->getConfigById($fieldId)->get('state') != ExtendManager::STATE_DELETED + ) { + $yml[$extendClass]['fields'][$fieldId->getFieldName()]['code'] = $fieldId->getFieldName(); + $yml[$extendClass]['fields'][$fieldId->getFieldName()]['type'] = $fieldId->getFieldType(); - $fieldConfig = $this->configProvider->getFieldConfig($entityName, $field); + $fieldConfig = $this->configProvider->getConfigById($fieldId); - $yml[$extendClass]['fields'][$field]['length'] = $fieldConfig->get('length'); - $yml[$extendClass]['fields'][$field]['precision'] = $fieldConfig->get('precision'); - $yml[$extendClass]['fields'][$field]['scale'] = $fieldConfig->get('scale'); + $yml[$extendClass]['fields'][$fieldId->getFieldName()]['length'] = $fieldConfig->get('length'); + $yml[$extendClass]['fields'][$fieldId->getFieldName()]['precision'] = $fieldConfig->get( + 'precision' + ); + $yml[$extendClass]['fields'][$fieldId->getFieldName()]['scale'] = $fieldConfig->get('scale'); } } } @@ -166,7 +276,6 @@ protected function generateClassMethod($methodName, $methodBody, $methodArgs = a /** * Prepare Dynamic class - * * @param $entityName * @param $className * @return $this @@ -208,25 +317,28 @@ protected function generateDynamicClass($entityName, $className) ); $toArray = ''; - if ($fields = $this->configProvider->getConfig($entityName)->getFields()) { - foreach ($fields as $field => $options) { - if ($this->configProvider->getFieldConfig($entityName, $field)->is('is_extend')) { + if ($fieldIds = $this->configProvider->getIds($entityName)) { + foreach ($fieldIds as $fieldId) { + if ($this->configProvider->getConfigById($fieldId)->is('is_extend') + //&& $this->configProvider->getConfigById($fieldId)->get('state') != ExtendManager::STATE_DELETED + ) { + $fieldName = $fieldId->getFieldName(); $class - ->setProperty(PhpProperty::create($field)->setVisibility('protected')) + ->setProperty(PhpProperty::create($fieldName)->setVisibility('protected')) ->setMethod( $this->generateClassMethod( - 'get'.ucfirst($field), - 'return $this->'.$field.';' + 'get' . ucfirst($fieldName), + 'return $this->' . $fieldName . ';' ) ) ->setMethod( $this->generateClassMethod( - 'set'.ucfirst($field), - '$this->'.$field.' = $value; return $this;', + 'set' . ucfirst($fieldName), + '$this->' . $fieldName . ' = $value; return $this;', array('value') ) ); - $toArray .= ' \''.$field.'\' => $this->'.$field.','."\n"; + $toArray .= ' \'' . $fieldName . '\' => $this->' . $fieldName . ',' . "\n"; } } } @@ -234,7 +346,7 @@ protected function generateDynamicClass($entityName, $className) $class->setMethod( $this->generateClassMethod( '__toArray', - 'return array('.$toArray."\n".');' + 'return array(' . $toArray . "\n" . ');' ) ); @@ -245,19 +357,22 @@ protected function generateDynamicClass($entityName, $className) /** * Generate Proxy class - * - * @param $entityName - * @param $className - * @return $this + * @param $entityName + * @param $className + * @param bool $extend + * @return string */ - protected function generateProxyClass($entityName, $className) + protected function generateProxyClass($entityName, $className, $extend = true) { $this->writer = new Writer(); - $class = PhpClass::create($this->generateClassName($entityName)) - ->setName($className) - ->setParentClassName($entityName) - ->setInterfaceNames(array('Oro\Bundle\EntityExtendBundle\Entity\ExtendProxyInterface')) + $class = PhpClass::create($this->generateClassName($entityName))->setName($className); + + if ($extend) { + $class->setParentClassName($entityName); + } + + $class->setInterfaceNames(array('Oro\Bundle\EntityExtendBundle\Entity\ExtendProxyInterface')) ->setProperty(PhpProperty::create('__proxy__extend')->setVisibility('protected')) ->setMethod( $this->generateClassMethod( @@ -287,27 +402,34 @@ protected function generateProxyClass($entityName, $className) ) ); - $toArray = ''; - if ($fields = $this->configProvider->getConfig($entityName)->getFields()) { - foreach ($fields as $field => $options) { - if ($this->configProvider->getFieldConfig($entityName, $field)->is('is_extend')) { + $toArray = ''; + if ($fieldIds = $this->configProvider->getIds($entityName)) { + foreach ($fieldIds as $fieldId) { + $fieldName = $fieldId->getFieldName(); + + if ($this->configProvider->getConfigById($fieldId)->is('is_extend') + && $this->configProvider->getConfigById($fieldId)->get('state') != ExtendManager::STATE_DELETED + ) { $class->setMethod( $this->generateClassMethod( - 'set'.ucfirst($field), - '$this->__proxy__extend->set'.ucfirst($field).'($'.$field.'); return $this;', - array($field) + 'set' . ucfirst($fieldName), + '$this->__proxy__extend->set' . ucfirst( + $fieldName + ) . '($' . $fieldName . '); return $this;', + array($fieldName) ) ); $class->setMethod( $this->generateClassMethod( - 'get'.ucfirst($field), - 'return $this->__proxy__extend->get'.ucfirst($field).'();' + 'get' . ucfirst($fieldName), + 'return $this->__proxy__extend->get' . ucfirst($fieldName) . '();' ) ); - $toArray .= ' \''.$field.'\' => $this->__proxy__extend->get'.ucfirst($field).'(),'."\n"; + $toArray .= ' \'' . $fieldName . '\' => $this->__proxy__extend->get' . + ucfirst($fieldName) . '(),' . "\n"; } else { - $toArray .= ' \''.$field.'\' => $this->get'.ucfirst($field).'(),'."\n"; + $toArray .= ' \'' . $fieldName . '\' => $this->get' . ucfirst($fieldName) . '(),' . "\n"; } } } @@ -315,7 +437,7 @@ protected function generateProxyClass($entityName, $className) $class->setMethod( $this->generateClassMethod( '__proxy__toArray', - 'return array('.$toArray."\n".');' + 'return array(' . $toArray . "\n" . ');' ) ); diff --git a/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php b/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php index 1f5d6eb1ff4..802f2a6687f 100644 --- a/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php +++ b/src/Oro/Bundle/EntityExtendBundle/Tools/Schema.php @@ -5,8 +5,9 @@ use Doctrine\ORM\EntityManager; use Doctrine\ORM\QueryBuilder; use Doctrine\ORM\AbstractQuery; -use Oro\Bundle\EntityConfigBundle\Config\FieldConfig; -use Oro\Bundle\EntityConfigBundle\Entity\ConfigField; + +use Oro\Bundle\EntityConfigBundle\Config\Id\FieldConfigIdInterface; +use Oro\Bundle\EntityConfigBundle\Entity\FieldConfigModel; use Oro\Bundle\EntityExtendBundle\Extend\ExtendManager; class Schema @@ -28,7 +29,7 @@ class Schema /** * @param EntityManager $em - * @param $backend + * @param $backend * @param ExtendManager $extendManager */ public function __construct(EntityManager $em, $backend, ExtendManager $extendManager) @@ -63,37 +64,37 @@ public function checkIsSynchronized($table) } /** - * @param $field FieldConfig + * @param $fieldId FieldConfigIdInterface * @return bool */ - public function checkFieldIsSystem(FieldConfig $field) + public function checkFieldIsSystem(FieldConfigIdInterface $fieldId) { $isSystem = false; - $metadata = $this->em->getClassMetadata($field->getClassName()); - if (in_array($field->getCode(), $metadata->fieldNames)) { - $isSystem = true; + $metadata = $this->em->getClassMetadata($fieldId->getClassName()); + if (in_array($fieldId->getFieldName(), $metadata->fieldNames)) { + $isSystem = true; } return $isSystem; } /** - * @param $field FieldConfig + * @param $fieldId FieldConfigIdInterface * @return bool */ - public function checkFieldCanDelete(FieldConfig $field) + public function checkFieldCanDelete(FieldConfigIdInterface $fieldId) { $canDelete = false; - if ($field->getClassName() - && $field->getCode() - && !$this->checkFieldIsSystem($field) + if ($fieldId->getClassName() + && $fieldId->getFieldName() + && !$this->checkFieldIsSystem($fieldId) ) { - $extendClass = $this->extendManager->getExtendClass($field->getClassName()); + $extendClass = $this->extendManager->getExtendClass($fieldId->getClassName()); /** @var QueryBuilder $builder */ $builder = $this->em->getRepository($extendClass)->createQueryBuilder('ex'); - $builder->select('MAX(ex.'.$field->getCode(). ')'); + $builder->select('MAX(ex.' . $fieldId->getFieldName() . ')'); if (!$builder->getQuery()->getSingleResult(AbstractQuery::HYDRATE_SINGLE_SCALAR)) { $canDelete = true; diff --git a/src/Oro/Bundle/FilterBundle/Form/Type/Filter/DateRangeFilterType.php b/src/Oro/Bundle/FilterBundle/Form/Type/Filter/DateRangeFilterType.php index 6ca6f4c2e38..079343926a2 100644 --- a/src/Oro/Bundle/FilterBundle/Form/Type/Filter/DateRangeFilterType.php +++ b/src/Oro/Bundle/FilterBundle/Form/Type/Filter/DateRangeFilterType.php @@ -14,6 +14,8 @@ class DateRangeFilterType extends AbstractType { const TYPE_BETWEEN = 1; const TYPE_NOT_BETWEEN = 2; + const TYPE_MORE_THAN = 3; + const TYPE_LESS_THAN = 4; const NAME = 'oro_type_date_range_filter'; /** @@ -55,11 +57,17 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) => $this->translator->trans('label_date_type_between', array(), 'OroFilterBundle'), self::TYPE_NOT_BETWEEN => $this->translator->trans('label_date_type_not_between', array(), 'OroFilterBundle'), + self::TYPE_MORE_THAN + => $this->translator->trans('label_date_type_more_than', array(), 'OroFilterBundle'), + self::TYPE_LESS_THAN + => $this->translator->trans('label_date_type_less_than', array(), 'OroFilterBundle'), ); $typeValues = array( 'between' => self::TYPE_BETWEEN, - 'notBetween' => self::TYPE_NOT_BETWEEN + 'notBetween' => self::TYPE_NOT_BETWEEN, + 'moreThan' => self::TYPE_MORE_THAN, + 'lessThan' => self::TYPE_LESS_THAN ); $resolver->setDefaults( diff --git a/src/Oro/Bundle/FilterBundle/Form/Type/Filter/SelectRowFilterType.php b/src/Oro/Bundle/FilterBundle/Form/Type/Filter/SelectRowFilterType.php new file mode 100644 index 00000000000..38123057be5 --- /dev/null +++ b/src/Oro/Bundle/FilterBundle/Form/Type/Filter/SelectRowFilterType.php @@ -0,0 +1,56 @@ +add('in', 'hidden', array('empty_data' => $emptyData)); + $builder->add('out', 'hidden', array('empty_data' => $emptyData)); + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return self::NAME; + } + + /** + * {@inheritDoc} + */ + public function getParent() + { + return FilterType::NAME; + } + + /** + * {@inheritDoc} + */ + public function setDefaultOptions(OptionsResolverInterface $resolver) + { + $resolver->setDefaults( + array( + 'field_type' => 'choice', + 'field_options' => array('choices' => array()), + ) + ); + } +} diff --git a/src/Oro/Bundle/FilterBundle/Form/Type/Filter/TextFilterType.php b/src/Oro/Bundle/FilterBundle/Form/Type/Filter/TextFilterType.php index bd69f55b064..1b8ec9f5760 100644 --- a/src/Oro/Bundle/FilterBundle/Form/Type/Filter/TextFilterType.php +++ b/src/Oro/Bundle/FilterBundle/Form/Type/Filter/TextFilterType.php @@ -11,6 +11,8 @@ class TextFilterType extends AbstractType const TYPE_CONTAINS = 1; const TYPE_NOT_CONTAINS = 2; const TYPE_EQUAL = 3; + const TYPE_STARTS_WITH = 4; + const TYPE_ENDS_WITH = 5; const NAME = 'oro_type_text_filter'; /** @@ -51,6 +53,8 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) self::TYPE_CONTAINS => $this->translator->trans('label_type_contains', array(), 'OroFilterBundle'), self::TYPE_NOT_CONTAINS => $this->translator->trans('label_type_not_contains', array(), 'OroFilterBundle'), self::TYPE_EQUAL => $this->translator->trans('label_type_equals', array(), 'OroFilterBundle'), + self::TYPE_STARTS_WITH => $this->translator->trans('label_type_start_with', array(), 'OroFilterBundle'), + self::TYPE_ENDS_WITH => $this->translator->trans('label_type_end_with', array(), 'OroFilterBundle'), ); $resolver->setDefaults( diff --git a/src/Oro/Bundle/FilterBundle/Resources/config/assets.yml b/src/Oro/Bundle/FilterBundle/Resources/config/assets.yml index a1438e8715b..e6cc0571823 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/FilterBundle/Resources/config/assets.yml @@ -18,6 +18,7 @@ js: - '@OroFilterBundle/Resources/public/js/app/filter/datefilter.js' - '@OroFilterBundle/Resources/public/js/app/filter/datetimefilter.js' - '@OroFilterBundle/Resources/public/js/app/filter/selectfilter.js' + - '@OroFilterBundle/Resources/public/js/app/filter/selectrowfilter.js' - '@OroFilterBundle/Resources/public/js/app/filter/multiselectfilter.js' - '@OroFilterBundle/Resources/public/js/app/filter/list.js' diff --git a/src/Oro/Bundle/FilterBundle/Resources/config/form_types.yml b/src/Oro/Bundle/FilterBundle/Resources/config/form_types.yml index 2089c4ea321..c20758b5c35 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/config/form_types.yml +++ b/src/Oro/Bundle/FilterBundle/Resources/config/form_types.yml @@ -56,3 +56,9 @@ services: arguments: ["@translator"] tags: - { name: form.type, alias: oro_type_entity_filter } + + oro_filter.form.type.filter.selectrow: + class: Oro\Bundle\FilterBundle\Form\Type\Filter\SelectRowFilterType + arguments: ["@translator"] + tags: + - { name: form.type, alias: oro_type_selectrow_filter } diff --git a/src/Oro/Bundle/FilterBundle/Resources/doc/reference/twig_extensions.md b/src/Oro/Bundle/FilterBundle/Resources/doc/reference/twig_extensions.md index 3b0ee05984a..43a17dc577d 100644 --- a/src/Oro/Bundle/FilterBundle/Resources/doc/reference/twig_extensions.md +++ b/src/Oro/Bundle/FilterBundle/Resources/doc/reference/twig_extensions.md @@ -33,7 +33,7 @@ This equals to next code: ``` +{% endblock %} + +{% block genemu_jqueryselect2_entity_widget %} + {% spaceless %} + {% if expanded %} + {{ block('choice_widget_expanded') }} + {% else %} + {{ block('genemu_jqueryselect2_entity_widget_collapsed') }} + {% endif %} + {% endspaceless %} +{% endblock %} + +{% block genemu_jqueryselect2_entity_widget_collapsed %} + {% spaceless %} + + {% endspaceless %} +{% endblock %} + {% block oro_ticker_symbol_widget %} -{% endblock %} - {% block oro_combobox_dataconfig_autocomplete %} {% set url = '' %} {% if configs.ajax.url is defined and configs.ajax.url%} @@ -153,34 +189,3 @@ } }; {% endblock %} - -{% block genemu_jqueryselect2_entity_widget %} - {% spaceless %} - {% if expanded %} - {{ block('choice_widget_expanded') }} - {% else %} - {{ block('genemu_jqueryselect2_entity_widget_collapsed') }} - {% endif %} - {% endspaceless %} -{% endblock %} - -{% block genemu_jqueryselect2_entity_widget_collapsed %} - {% spaceless %} - - {% endspaceless %} -{% endblock %} diff --git a/src/Oro/Bundle/FormBundle/Resources/views/Js/configResult.html.twig b/src/Oro/Bundle/FormBundle/Resources/views/Js/configResult.html.twig new file mode 100644 index 00000000000..fcc864f8944 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Resources/views/Js/configResult.html.twig @@ -0,0 +1 @@ + <%= highlight(text) %> diff --git a/src/Oro/Bundle/FormBundle/Resources/views/Js/configSelection.html.twig b/src/Oro/Bundle/FormBundle/Resources/views/Js/configSelection.html.twig new file mode 100644 index 00000000000..fcc864f8944 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Resources/views/Js/configSelection.html.twig @@ -0,0 +1 @@ + <%= highlight(text) %> diff --git a/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/OroIconTypeTest.php b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/OroIconTypeTest.php new file mode 100644 index 00000000000..27c9320e764 --- /dev/null +++ b/src/Oro/Bundle/FormBundle/Tests/Unit/Form/Type/OroIconTypeTest.php @@ -0,0 +1,35 @@ +type = new OroIconType(); + } + + /** + * {@inheritDoc} + */ + protected function getTestFormType() + { + return $this->type; + } + + public function testParameters() + { + $this->assertEquals('genemu_jqueryselect2_hidden', $this->type->getParent()); + $this->assertEquals('oro_icon_select', $this->type->getName()); + } +} diff --git a/src/Oro/Bundle/GridBundle/Action/AbstractAction.php b/src/Oro/Bundle/GridBundle/Action/AbstractAction.php index 9f8b2453ba6..3b67554d298 100644 --- a/src/Oro/Bundle/GridBundle/Action/AbstractAction.php +++ b/src/Oro/Bundle/GridBundle/Action/AbstractAction.php @@ -26,11 +26,6 @@ abstract class AbstractAction implements ActionInterface */ protected $options; - /** - * @var ManagerInterface - */ - protected $aclManager; - /** * @var bool */ @@ -41,14 +36,6 @@ abstract class AbstractAction implements ActionInterface */ protected $requiredOptions = array(); - /** - * @param ManagerInterface $aclManager - */ - public function __construct(ManagerInterface $aclManager) - { - $this->aclManager = $aclManager; - } - /** * Filter name * @@ -133,18 +120,4 @@ protected function assertHasRequiredOption($optionName) ); } } - - /** - * Check whether action allowed for current user - * - * @return bool - */ - public function isGranted() - { - if ($this->aclResource) { - return $this->aclManager->isResourceGranted($this->aclResource); - } - - return true; - } } diff --git a/src/Oro/Bundle/GridBundle/Action/ActionInterface.php b/src/Oro/Bundle/GridBundle/Action/ActionInterface.php index a423466df15..302d820f1b1 100644 --- a/src/Oro/Bundle/GridBundle/Action/ActionInterface.php +++ b/src/Oro/Bundle/GridBundle/Action/ActionInterface.php @@ -7,6 +7,7 @@ interface ActionInterface /** * Action types */ + const TYPE_AJAX = 'oro_grid_action_ajax'; const TYPE_REDIRECT = 'oro_grid_action_redirect'; const TYPE_DELETE = 'oro_grid_action_delete'; @@ -52,11 +53,4 @@ public function setAclResource($aclResource); * @param array $options */ public function setOptions(array $options); - - /** - * Check whether action allowed for current user - * - * @return mixed - */ - public function isGranted(); } diff --git a/src/Oro/Bundle/GridBundle/Action/AjaxAction.php b/src/Oro/Bundle/GridBundle/Action/AjaxAction.php new file mode 100644 index 00000000000..3e49db7cebc --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Action/AjaxAction.php @@ -0,0 +1,28 @@ +options = $options; + $this->assertRequiredOptions(array('name')); + } + + /** + * {@inheritDoc} + */ + public function getOptions() + { + return $this->options; + } + + /** + * {@inheritDoc} + */ + public function getOption($name) + { + return isset($this->options[$name]) ? $this->options[$name] : null; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return $this->getOption('name'); + } + + /** + * {@inheritDoc} + */ + public function getAclResource() + { + return $this->getOption('acl_resource'); + } + + /** + * {@inheritDoc} + */ + public function getLabel() + { + return $this->getOption('label'); + } + + /** + * @param array $requiredOptions + * @throws \InvalidArgumentException + */ + protected function assertRequiredOptions(array $requiredOptions) + { + foreach ($requiredOptions as $optionName) { + if (!isset($this->options[$optionName])) { + $actionName = $this->getName(); + if ($actionName) { + throw new \InvalidArgumentException( + sprintf('Option "%s" is required for mass action "%s"', $optionName, $this->getName()) + ); + } else { + throw new \InvalidArgumentException( + sprintf('Option "%s" is required for mass action class %s', $optionName, get_called_class()) + ); + } + } + } + } +} diff --git a/src/Oro/Bundle/GridBundle/Action/MassAction/Ajax/AjaxMassAction.php b/src/Oro/Bundle/GridBundle/Action/MassAction/Ajax/AjaxMassAction.php new file mode 100644 index 00000000000..d138203ba3e --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Action/MassAction/Ajax/AjaxMassAction.php @@ -0,0 +1,34 @@ +assertRequiredOptions(array('handler')); + } +} diff --git a/src/Oro/Bundle/GridBundle/Action/MassAction/Ajax/DeleteMassAction.php b/src/Oro/Bundle/GridBundle/Action/MassAction/Ajax/DeleteMassAction.php new file mode 100644 index 00000000000..d831c5d10cf --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Action/MassAction/Ajax/DeleteMassAction.php @@ -0,0 +1,20 @@ +entityManager = $entityManager; + $this->translator = $translator; + } + + /** + * {@inheritDoc} + */ + public function handle(MassActionMediatorInterface $mediator) + { + $iteration = 0; + $entityName = null; + $entityIdentifiedField = null; + + $results = $mediator->getResults(); + + // batch remove should be processed in transaction + $this->entityManager->beginTransaction(); + try { + foreach ($results as $result) { + $entity = $result->getRootEntity(); + if (!$entity) { + // no entity in result record, it should be extracted from DB + if (!$entityName) { + $entityName = $this->getEntityName($mediator); + } + if (!$entityIdentifiedField) { + $entityIdentifiedField = $this->getEntityIdentifierField($mediator); + } + $entity = $this->getEntity($entityName, $result->getValue($entityIdentifiedField)); + } + + if ($entity) { + $this->entityManager->remove($entity); + + $iteration++; + if ($iteration % self::FLUSH_BATCH_SIZE == 0) { + $this->entityManager->flush(); + } + } + } + + if ($iteration % self::FLUSH_BATCH_SIZE > 0) { + $this->entityManager->flush(); + } + + $this->entityManager->commit(); + } catch (\Exception $e) { + $this->entityManager->rollback(); + throw $e; + } + + return $this->getResponse($mediator, $iteration); + } + + /** + * @param MassActionMediatorInterface $mediator + * @param int $entitiesCount + * @return MassActionResponse + */ + protected function getResponse(MassActionMediatorInterface $mediator, $entitiesCount = 0) + { + $massAction = $mediator->getMassAction(); + $messages = $massAction->getOption('messages'); + $responseMessage = !empty($messages) && !empty($messages['success']) + ? $messages['success'] + : $this->responseMessage; + + $successful = $entitiesCount > 0; + $options = array('count' => $entitiesCount); + + return new MassActionResponse( + $successful, + $this->translator->transChoice( + $responseMessage, + $entitiesCount, + array('%count%' => $entitiesCount) + ), + $options + ); + } + + /** + * @param MassActionMediatorInterface $mediator + * @return string + * @throws \LogicException + */ + protected function getEntityName(MassActionMediatorInterface $mediator) + { + $entityName = $mediator->getDatagrid()->getEntityName(); + if (!$entityName) { + $massAction = $mediator->getMassAction(); + $entityName = $massAction->getOption('entity_name'); + if (!$entityName) { + throw new \LogicException(sprintf('Mass action "%s" must define entity name', $massAction->getName())); + } + } + + return $entityName; + } + + /** + * @param MassActionMediatorInterface $mediator + * @return string + */ + protected function getEntityIdentifierField(MassActionMediatorInterface $mediator) + { + return $mediator->getDatagrid()->getIdentifierField()->getFieldName(); + } + + /** + * @param string $entityName + * @param mixed $identifierValue + * @return object + */ + protected function getEntity($entityName, $identifierValue) + { + return $this->entityManager->getReference($entityName, $identifierValue); + } +} diff --git a/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionDispatcher.php b/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionDispatcher.php new file mode 100644 index 00000000000..a85a83856b3 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionDispatcher.php @@ -0,0 +1,191 @@ +container = $container; + $this->managerRegistry = $managerRegistry; + } + + /** + * @param string $datagridName + * @param string $actionName + * @param array $parameters + * @param array $data + * @throws \LogicException + * + * @return MassActionResponseInterface + */ + public function dispatch($datagridName, $actionName, array $parameters, array $data = array()) + { + $inset = true; + if (isset($parameters['inset'])) { + $inset = $parameters['inset']; + } + + $values = array(); + if (isset($parameters['values'])) { + $values = $parameters['values']; + } + + $filters = array(); + if (isset($parameters['filters'])) { + $filters = $parameters['filters']; + } + + if ($inset && empty($values)) { + throw new \LogicException(sprintf('There is nothing to do in mass action "%s"', $actionName)); + } + + $datagridManager = $this->managerRegistry->getDatagridManager($datagridName); + + // create datagrid + $datagrid = $datagridManager->getDatagrid(); + $datagrid->getParameters()->set(ParametersInterface::FILTER_PARAMETERS, $filters); + $datagrid->applyFilters(); + + // create mediator + $massAction = $this->getMassActionByName($datagrid, $actionName); + $proxyQuery = $this->getDatagridQuery($datagrid, $inset, $values); + $resultIterator = $this->getResultIterator($proxyQuery); + $mediator = new MassActionMediator($massAction, $datagrid, $resultIterator, $data); + + // perform mass action + $handle = $this->getMassActionHandler($massAction); + $result = $handle->handle($mediator); + + return $result; + } + + /** + * @param DatagridInterface $datagrid + * @param bool $inset + * @param array $values + * @return ProxyQueryInterface + */ + protected function getDatagridQuery(DatagridInterface $datagrid, $inset = true, $values = array()) + { + $identifierFieldExpression = $this->getIdentifierExpression($datagrid); + /** @var QueryBuilder $proxyQuery */ + $proxyQuery = $datagrid->getQuery(); + if ($values) { + $valueWhereCondition = + $inset + ? $proxyQuery->expr()->in($identifierFieldExpression, $values) + : $proxyQuery->expr()->notIn($identifierFieldExpression, $values); + $proxyQuery->andWhere($valueWhereCondition); + } + + return $proxyQuery; + } + + /** + * @param DatagridInterface $datagrid + * @param $massActionName + * @return MassActionInterface + * @throws \LogicException + */ + protected function getMassActionByName(DatagridInterface $datagrid, $massActionName) + { + $massAction = null; + foreach ($datagrid->getMassActions() as $action) { + if ($action->getName() == $massActionName) { + $massAction = $action; + } + } + + if (!$massAction) { + throw new \LogicException(sprintf('Can\'t find mass action "%s"', $massActionName)); + } + + return $massAction; + } + + /** + * @param ProxyQueryInterface $proxyQuery + * @param int|null $bufferSize + * @return IterableResultInterface + */ + protected function getResultIterator(ProxyQueryInterface $proxyQuery, $bufferSize = null) + { + $result = new IterableResult($proxyQuery); + + if ($bufferSize) { + $result->setBufferSize($result); + } + + return $result; + } + + /** + * @param DatagridInterface $datagrid + * @return string + * @throws \LogicException + */ + protected function getIdentifierExpression(DatagridInterface $datagrid) + { + $identifierField = $datagrid->getIdentifierField(); + $fieldMapping = $identifierField->getFieldMapping(); + + return isset($fieldMapping['fieldExpression']) ? + $fieldMapping['fieldExpression'] : + $identifierField->getFieldName(); + } + + + /** + * @param MassActionInterface $massAction + * @return MassActionHandlerInterface + * @throws \LogicException + * @throws UnexpectedTypeException + */ + protected function getMassActionHandler(MassActionInterface $massAction) + { + $handlerServiceId = $massAction->getOption('handler'); + if (!$handlerServiceId) { + throw new \LogicException(sprintf('There is no handler for mass action "%s"', $massAction->getName())); + } + if (!$this->container->has($handlerServiceId)) { + throw new \LogicException(sprintf('Mass action handler service "%s" not exist', $handlerServiceId)); + } + + $handler = $this->container->get($handlerServiceId); + if (!$handler instanceof MassActionHandlerInterface) { + throw new UnexpectedTypeException($handler, 'MassActionHandlerInterface'); + } + + return $handler; + } +} diff --git a/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionHandlerInterface.php b/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionHandlerInterface.php new file mode 100644 index 00000000000..ed66de9b749 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionHandlerInterface.php @@ -0,0 +1,17 @@ +massAction = $massAction; + $this->results = $results; + $this->data = $data; + $this->datagrid = $datagrid; + } + + /** + * {@inheritDoc} + */ + public function getMassAction() + { + return $this->massAction; + } + + /** + * {@inheritDoc} + */ + public function getResults() + { + return $this->results; + } + + /** + * {@inheritDoc} + */ + public function getData() + { + return $this->data; + } + + /** + * {@inheritDoc} + */ + public function getDatagrid() + { + return $this->datagrid; + } +} diff --git a/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionMediatorInterface.php b/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionMediatorInterface.php new file mode 100644 index 00000000000..3fcb716d888 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionMediatorInterface.php @@ -0,0 +1,32 @@ +get('inset', true); + $inset = !empty($inset); + + $values = $request->get('values', ''); + if (!is_array($values)) { + $values = $values !== '' ? explode(',', $values) : array(); + } + + $filters = $request->get('filters', null); + if (is_string($filters)) { + $filters = json_decode($filters, true); + } + if (!$filters) { + $filters = array(); + } + + return array( + 'inset' => $inset, + 'values' => $values, + 'filters' => $filters, + ); + } +} diff --git a/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionResponse.php b/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionResponse.php new file mode 100644 index 00000000000..ee57e49b2e3 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionResponse.php @@ -0,0 +1,69 @@ +successful = $successful; + $this->message = $message; + $this->options = $options; + } + + /** + * {@inheritDoc} + */ + public function getOptions() + { + return $this->options; + } + + /** + * {@inheritDoc} + */ + public function getOption($name) + { + return isset($this->options[$name]) ? $this->options[$name] : null; + } + + /** + * @return boolean + */ + public function isSuccessful() + { + return $this->successful; + } + + /** + * @return string + */ + public function getMessage() + { + return $this->message; + } +} diff --git a/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionResponseInterface.php b/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionResponseInterface.php new file mode 100644 index 00000000000..af0f7c77562 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Action/MassAction/MassActionResponseInterface.php @@ -0,0 +1,27 @@ +assertRequiredOptions(array('route')); + } +} diff --git a/src/Oro/Bundle/GridBundle/Action/MassAction/Widget/WidgetMassAction.php b/src/Oro/Bundle/GridBundle/Action/MassAction/Widget/WidgetMassAction.php new file mode 100644 index 00000000000..6388019ca22 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Action/MassAction/Widget/WidgetMassAction.php @@ -0,0 +1,28 @@ +assertRequiredOptions(array('route', 'frontend_type')); + } +} diff --git a/src/Oro/Bundle/GridBundle/Action/MassAction/Widget/WindowMassAction.php b/src/Oro/Bundle/GridBundle/Action/MassAction/Widget/WindowMassAction.php new file mode 100644 index 00000000000..39464df0c7d --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Action/MassAction/Widget/WindowMassAction.php @@ -0,0 +1,18 @@ +formFactory = $formFactory; $this->eventDispatcher = $eventDispatcher; + $this->aclManager = $aclManager; $this->filterFactory = $filterFactory; $this->sorterFactory = $sorterFactory; $this->actionFactory = $actionFactory; @@ -91,6 +102,21 @@ public function addFilter( $datagrid->addFilter($filter); } + /** + * {@inheritdoc} + */ + public function addSelectedRowFilter(DatagridInterface $datagrid, array $options) + { + $filter = $this->filterFactory->create( + self::SELECTED_ROW_FILTER_NAME, + FilterInterface::TYPE_SELECT_ROW, + $options + ); + $filter->setOption('data_type', FieldDescriptionInterface::TYPE_INTEGER); + // filter must be first in list + $datagrid->addFilter($filter, true); + } + /** * @param DatagridInterface $datagrid * @param FieldDescriptionInterface $field @@ -115,11 +141,24 @@ public function addRowAction(DatagridInterface $datagrid, array $parameters) isset($parameters['options']) ? $parameters['options'] : array() ); - if ($action->isGranted()) { + $aclResource = $action->getAclResource(); + if (!$aclResource || $this->aclManager->isResourceGranted($aclResource)) { $datagrid->addRowAction($action); } } + /** + * @param DatagridInterface $datagrid + * @param MassActionInterface $massAction + */ + public function addMassAction(DatagridInterface $datagrid, MassActionInterface $massAction) + { + $aclResource = $massAction->getAclResource(); + if (!$aclResource || $this->aclManager->isResourceGranted($aclResource)) { + $datagrid->addMassAction($massAction); + } + } + /** * Add property to datagrid * @@ -138,7 +177,6 @@ public function addProperty(DatagridInterface $datagrid, PropertyInterface $prop * @param RouteGeneratorInterface $routeGenerator * @param ParametersInterface $parameters * @param string $name - * @param string $entityHint * * @return DatagridInterface */ @@ -147,8 +185,7 @@ public function getBaseDatagrid( FieldDescriptionCollection $fieldCollection, RouteGeneratorInterface $routeGenerator, ParametersInterface $parameters, - $name, - $entityHint = null + $name ) { $formBuilder = $this->formFactory->createNamedBuilder( $this->getFormName($name), @@ -165,9 +202,7 @@ public function getBaseDatagrid( $formBuilder, $routeGenerator, $parameters, - $this->eventDispatcher, - $name, - $entityHint + $this->eventDispatcher ); } diff --git a/src/Oro/Bundle/GridBundle/Builder/DatagridBuilderInterface.php b/src/Oro/Bundle/GridBundle/Builder/DatagridBuilderInterface.php index c62cb58f948..1f2506e563b 100644 --- a/src/Oro/Bundle/GridBundle/Builder/DatagridBuilderInterface.php +++ b/src/Oro/Bundle/GridBundle/Builder/DatagridBuilderInterface.php @@ -9,9 +9,12 @@ use Oro\Bundle\GridBundle\Datagrid\DatagridInterface; use Oro\Bundle\GridBundle\Datagrid\ParametersInterface; use Oro\Bundle\GridBundle\Route\RouteGeneratorInterface; +use Oro\Bundle\GridBundle\Action\MassAction\MassActionInterface; interface DatagridBuilderInterface { + const SELECTED_ROW_FILTER_NAME = 'selected_row_filter'; + /** * Add property to datagrid * @@ -31,6 +34,13 @@ public function addFilter( FieldDescriptionInterface $fieldDescription = null ); + /** + * @param DatagridInterface $datagrid + * @param array $options + * @return void + */ + public function addSelectedRowFilter(DatagridInterface $datagrid, array $options); + /** * @param DatagridInterface $datagrid * @param FieldDescriptionInterface $field @@ -45,13 +55,18 @@ public function addSorter(DatagridInterface $datagrid, FieldDescriptionInterface */ public function addRowAction(DatagridInterface $datagrid, array $parameters); + /** + * @param DatagridInterface $datagrid + * @param MassActionInterface $massAction + */ + public function addMassAction(DatagridInterface $datagrid, MassActionInterface $massAction); + /** * @param ProxyQueryInterface $query * @param FieldDescriptionCollection $fieldCollection * @param RouteGeneratorInterface $routeGenerator, * @param ParametersInterface $parameters * @param string $name - * @param string $entityHint * * @return DatagridInterface */ @@ -60,7 +75,6 @@ public function getBaseDatagrid( FieldDescriptionCollection $fieldCollection, RouteGeneratorInterface $routeGenerator, ParametersInterface $parameters, - $name, - $entityHint = null + $name ); } diff --git a/src/Oro/Bundle/GridBundle/Controller/MassActionController.php b/src/Oro/Bundle/GridBundle/Controller/MassActionController.php new file mode 100644 index 00000000000..d0557b2af99 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Controller/MassActionController.php @@ -0,0 +1,57 @@ +getRequest(); + + /** @var MassActionParametersParser $massActionParametersParser */ + $parametersParser = $this->get('oro_grid.mass_action.parameters_parser'); + $parameters = $parametersParser->parse($request); + + $requestData = array_merge($request->query->all(), $request->request->all()); + + /** @var MassActionDispatcher $massActionDispatcher */ + $massActionDispatcher = $this->get('oro_grid.mass_action.dispatcher'); + $response = $massActionDispatcher->dispatch($gridName, $actionName, $parameters, $requestData); + + $data = array( + 'successful' => $response->isSuccessful(), + 'message' => $response->getMessage(), + ); + + return new JsonResponse(array_merge($data, $response->getOptions())); + } +} diff --git a/src/Oro/Bundle/GridBundle/Datagrid/Datagrid.php b/src/Oro/Bundle/GridBundle/Datagrid/Datagrid.php index dcf851649d1..9c4a19b36f8 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/Datagrid.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/Datagrid.php @@ -6,8 +6,6 @@ use Symfony\Component\Form\Form; use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Sonata\AdminBundle\Filter\FilterInterface as SonataFilterInterface; - use Oro\Bundle\GridBundle\Filter\FilterInterface; use Oro\Bundle\GridBundle\Field\FieldDescriptionCollection; use Oro\Bundle\GridBundle\Field\FieldDescriptionInterface; @@ -17,7 +15,13 @@ use Oro\Bundle\GridBundle\Route\RouteGeneratorInterface; use Oro\Bundle\GridBundle\Action\ActionInterface; use Oro\Bundle\GridBundle\EventDispatcher\ResultDatagridEvent; +use Oro\Bundle\GridBundle\Action\MassAction\MassActionInterface; +use Oro\Bundle\GridBundle\Datagrid\ParametersInterface; +/** + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * TODO: This class should be refactored (BAP-969). + */ class Datagrid implements DatagridInterface { /** @@ -52,6 +56,13 @@ class Datagrid implements DatagridInterface */ protected $parametersApplied = false; + /** + * Filters applied flag + * + * @var bool + */ + protected $filtersApplied = false; + /** * Pager applied flag * @@ -101,6 +112,11 @@ class Datagrid implements DatagridInterface */ protected $name; + /** + * @var string + */ + protected $entityName; + /** * @var string */ @@ -109,18 +125,32 @@ class Datagrid implements DatagridInterface /** * @var ActionInterface[] */ - protected $rowActions; + protected $rowActions = array (); + + /** + * @var MassActionInterface[] + */ + protected $massActions = array(); + + /** + * @var array + */ + protected $toolbarOptions; + + /** + * @var string|null + */ + protected $identifierFieldName; /** - * @param ProxyQueryInterface $query + * @param ProxyQueryInterface $query * @param FieldDescriptionCollection $columns - * @param PagerInterface $pager - * @param FormBuilderInterface $formBuilder - * @param RouteGeneratorInterface $routeGenerator - * @param ParametersInterface $parameters - * @param EventDispatcherInterface $eventDispatcher - * @param string $name - * @param string $entityHint + * @param PagerInterface $pager + * @param FormBuilderInterface $formBuilder + * @param RouteGeneratorInterface $routeGenerator + * @param ParametersInterface $parameters + * @param EventDispatcherInterface $eventDispatcher + * @param string $name */ public function __construct( ProxyQueryInterface $query, @@ -129,9 +159,7 @@ public function __construct( FormBuilderInterface $formBuilder, RouteGeneratorInterface $routeGenerator, ParametersInterface $parameters, - EventDispatcherInterface $eventDispatcher, - $name, - $entityHint = null + EventDispatcherInterface $eventDispatcher ) { $this->query = $query; $this->columns = $columns; @@ -140,8 +168,6 @@ public function __construct( $this->routeGenerator = $routeGenerator; $this->parameters = $parameters; $this->eventDispatcher = $eventDispatcher; - $this->name = $name; - $this->entityHint = $entityHint; $this->properties = new PropertyCollection(); /** @var $field FieldDescriptionInterface */ @@ -165,7 +191,7 @@ public function addProperty(PropertyInterface $property) /** * Get properties * - * @return PropertyCollection + * @return array */ public function getProperties() { @@ -173,13 +199,16 @@ public function getProperties() } /** - * @param SonataFilterInterface $filter - * @return void + * {@inheritDoc} */ - public function addFilter(SonataFilterInterface $filter) + public function addFilter(FilterInterface $filter, $prepend = false) { $name = $filter->getName(); - $this->filters[$name] = $filter; + if ($prepend) { + $this->filters = array_merge(array($name => $filter), $this->filters); + } else { + $this->filters[$name] = $filter; + } list($formType, $formOptions) = $filter->getRenderSettings(); $this->formBuilder->add($name, $formType, $formOptions); } @@ -195,7 +224,7 @@ public function getFilters() /** * @param string $name * - * @return SonataFilterInterface + * @return FilterInterface */ public function getFilter($name) { @@ -236,7 +265,7 @@ public function hasActiveFilters() } /** - * @param SorterInterface $sorter + * @param SorterInterface $sorter * @return void */ public function addSorter(SorterInterface $sorter) @@ -271,6 +300,7 @@ public function getSorter($name) public function getPager() { $this->applyPager(); + return $this->pager; } @@ -293,8 +323,12 @@ protected function applyParameters() /** * Apply filter data to ProxyQuery */ - protected function applyFilters() + public function applyFilters() { + if ($this->filtersApplied) { + return; + } + $form = $this->getForm(); /** @var $filter FilterInterface */ @@ -306,6 +340,8 @@ protected function applyFilters() $filter->apply($this->query, $data); } } + + $this->filtersApplied = true; } /** @@ -333,7 +369,7 @@ protected function applyPager() $pagerParameters = $this->parameters->get(ParametersInterface::PAGER_PARAMETERS); $this->pager->setPage(isset($pagerParameters['_page']) ? $pagerParameters['_page'] : 1); - $this->pager->setMaxPerPage(!empty($pagerParameters['_per_page']) ? $pagerParameters['_per_page'] : 10); + $this->pager->setMaxPerPage(isset($pagerParameters['_per_page']) ? (int) $pagerParameters['_per_page'] : 10); $this->pager->init(); $this->pagerApplied = true; @@ -383,62 +419,59 @@ public function getResults() } /** - * @deprecated Use applyParameters instead + * @return array */ - public function buildPager() + public function getColumns() { - $this->applyParameters(); + return $this->columns->getElements(); } /** - * @return array + * @return ParametersInterface */ - public function getColumns() + public function getParameters() { - return $this->columns->getElements(); + return $this->parameters; } /** - * @return array + * @return RouteGeneratorInterface */ - public function getParameters() + public function getRouteGenerator() { - return $this->parameters->toArray(); + return $this->routeGenerator; } /** - * @param string $name - * @param string $operator - * @param mixed $value - * @deprecated Grid parameters are read-only + * @return string */ - public function setValue($name, $operator, $value) + public function getName() { + return $this->name; } /** - * @return array - * @deprecated Use getParameters instead + * {@inheritDoc} */ - public function getValues() + public function setName($name) { - return $this->getParameters(); + $this->name = $name; } /** - * @return RouteGeneratorInterface + * {@inheritDoc} */ - public function getRouteGenerator() + public function getEntityName() { - return $this->routeGenerator; + return $this->entityName; } /** - * @return string + * {@inheritDoc} */ - public function getName() + public function setEntityName($entityName) { - return $this->name; + $this->entityName = $entityName; } /** @@ -450,7 +483,15 @@ public function getEntityHint() } /** - * @param ActionInterface $action + * {@inheritDoc} + */ + public function setEntityHint($entityHint) + { + $this->entityHint = $entityHint; + } + + /** + * @param ActionInterface $action * @return void */ public function addRowAction(ActionInterface $action) @@ -458,6 +499,15 @@ public function addRowAction(ActionInterface $action) $this->rowActions[] = $action; } + /** + * @param MassActionInterface $action + * @return void + */ + public function addMassAction(MassActionInterface $action) + { + $this->massActions[] = $action; + } + /** * @return ActionInterface[] */ @@ -466,6 +516,14 @@ public function getRowActions() return $this->rowActions; } + /** + * @return MassActionInterface[] + */ + public function getMassActions() + { + return $this->massActions; + } + /** * @return DatagridView */ @@ -473,4 +531,49 @@ public function createView() { return new DatagridView($this); } + + /** + * @return array + */ + public function getToolbarOptions() + { + return $this->toolbarOptions; + } + + /** + * @param $options + * @return $this + */ + public function setToolbarOptions($options) + { + $this->toolbarOptions = $options; + } + + /** + * {@inheritDoc} + */ + public function getIdentifierFieldName() + { + return $this->identifierFieldName; + } + + /** + * @param string $identifierFieldName + */ + public function setIdentifierFieldName($identifierFieldName) + { + $this->identifierFieldName = $identifierFieldName; + } + + /** + * {@inheritDoc} + */ + public function getIdentifierField() + { + $identifierFieldName = $this->getIdentifierFieldName(); + if ($identifierFieldName && $this->columns->has($identifierFieldName)) { + return $this->columns->get($identifierFieldName); + } + throw new \RuntimeException(sprintf('There is no identifier field in grid "%s"', $this->getName())); + } } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/DatagridInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/DatagridInterface.php index d0d60ecb0d0..eb907e2a9a6 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/DatagridInterface.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/DatagridInterface.php @@ -2,33 +2,105 @@ namespace Oro\Bundle\GridBundle\Datagrid; -use Sonata\AdminBundle\Datagrid\DatagridInterface as BaseDatagridInterface; +use Symfony\Component\Form\Form; use Oro\Bundle\GridBundle\Property\PropertyInterface; use Oro\Bundle\GridBundle\Sorter\SorterInterface; use Oro\Bundle\GridBundle\Route\RouteGeneratorInterface; use Oro\Bundle\GridBundle\Action\ActionInterface; +use Oro\Bundle\GridBundle\Action\MassAction\MassActionInterface; +use Oro\Bundle\GridBundle\Datagrid\PagerInterface; +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; +use Oro\Bundle\GridBundle\Filter\FilterInterface; +use Oro\Bundle\GridBundle\Datagrid\ResultRecordInterface; +use Oro\Bundle\GridBundle\Field\FieldDescriptionInterface; -interface DatagridInterface extends BaseDatagridInterface +interface DatagridInterface { /** - * @param PropertyInterface $property + * @return PagerInterface + */ + public function getPager(); + + /** + * @return ProxyQueryInterface + */ + public function getQuery(); + + /** + * @return ResultRecordInterface[] + */ + public function getResults(); + + /** + * @return FilterInterface[] + */ + public function getFilters(); + + /** + * @return FieldDescriptionInterface[] + */ + public function getColumns(); + + /** + * @return Form + */ + public function getForm(); + + /** + * @param string $name + * + * @return FilterInterface + */ + public function getFilter($name); + + /** + * @param string $name + * + * @return bool + */ + public function hasFilter($name); + + /** + * @param string $name + */ + public function removeFilter($name); + + /** + * @return boolean + */ + public function hasActiveFilters(); + + /** + * @param FilterInterface $filter + * @param boolean $prepend + */ + public function addFilter(FilterInterface $filter, $prepend = false); + + /** + * @param PropertyInterface $property * @return void */ public function addProperty(PropertyInterface $property); /** - * @param SorterInterface $sorter + * @param SorterInterface $sorter * @return void */ public function addSorter(SorterInterface $sorter); /** - * @param ActionInterface $action + * @param ActionInterface $action * @return void */ public function addRowAction(ActionInterface $action); + /** + * @param MassActionInterface $action + * @return void + */ + public function addMassAction(MassActionInterface $action); + /** * @return SorterInterface[] */ @@ -40,7 +112,12 @@ public function getSorters(); public function getRowActions(); /** - * @param string $name + * @return MassActionInterface[] + */ + public function getMassActions(); + + /** + * @param string $name * @return null|SorterInterface */ public function getSorter($name); @@ -55,18 +132,38 @@ public function getRouteGenerator(); */ public function getName(); + /** + * @param string $name + */ + public function setName($name); + + /** + * @return string + */ + public function getEntityName(); + + /** + * @param string $entityName + */ + public function setEntityName($entityName); + /** * @return string */ public function getEntityHint(); + /** + * @param string $entityHint + */ + public function setEntityHint($entityHint); + /** * @return DatagridView */ public function createView(); /** - * @return array + * @return ParametersInterface */ public function getParameters(); @@ -74,4 +171,42 @@ public function getParameters(); * @return array */ public function getProperties(); + + /** + * @return array + */ + public function getToolbarOptions(); + + /** + * @param $options + * @return $this + */ + public function setToolbarOptions($options); + + /** + * Apply filter data to ProxyQuery + */ + public function applyFilters(); + + /** + * Get identifier field name + * + * @return string + */ + public function getIdentifierFieldName(); + + /** + * Set identifier field name + * + * @param string $identifierFieldName + */ + public function setIdentifierFieldName($identifierFieldName); + + /** + * Set identifier field + * + * @return FieldDescriptionInterface + * @throws \RuntimeException If there is no identifier field + */ + public function getIdentifierField(); } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/DatagridManager.php b/src/Oro/Bundle/GridBundle/Datagrid/DatagridManager.php index 60c80471d13..86f5a036c80 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/DatagridManager.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/DatagridManager.php @@ -2,6 +2,8 @@ namespace Oro\Bundle\GridBundle\Datagrid; +use Doctrine\ORM\EntityManager; + use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\ValidatorInterface; use Symfony\Component\Routing\Router; @@ -13,8 +15,15 @@ use Oro\Bundle\GridBundle\Property\PropertyInterface; use Oro\Bundle\GridBundle\Datagrid\ParametersInterface; use Oro\Bundle\GridBundle\Route\RouteGeneratorInterface; +use Oro\Bundle\GridBundle\Filter\FilterInterface; +use Oro\Bundle\GridBundle\Field\FieldDescription; use Oro\Bundle\GridBundle\Sorter\SorterInterface; +use Oro\Bundle\GridBundle\Action\MassAction\MassActionInterface; +/** + * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * TODO: This class should be refactored (BAP-969). + */ abstract class DatagridManager implements DatagridManagerInterface { /** @@ -37,6 +46,11 @@ abstract class DatagridManager implements DatagridManagerInterface */ protected $translator; + /** + * @var EntityManager + */ + protected $entityManager; + /** * @var string */ @@ -62,6 +76,16 @@ abstract class DatagridManager implements DatagridManagerInterface */ protected $name; + /** + * @var string + */ + protected $entityName; + + /** + * @var string + */ + protected $queryEntityAlias; + /** * @var string */ @@ -77,6 +101,16 @@ abstract class DatagridManager implements DatagridManagerInterface */ private $fieldsCollection; + /** + * @var string|null + */ + private $identifierField; + + /** + * @var array + */ + protected $toolbarOptions = array(); + /** * {@inheritDoc} */ @@ -149,6 +183,30 @@ public function setName($name) $this->name = $name; } + /** + * {@inheritDoc} + */ + public function setEntityManager(EntityManager $entityManager) + { + $this->entityManager = $entityManager; + } + + /** + * {@inheritDoc} + */ + public function setEntityName($entityName) + { + $this->entityName = $entityName; + } + + /** + * {@inheritDoc} + */ + public function setQueryEntityAlias($queryEntityAlias) + { + $this->queryEntityAlias = $queryEntityAlias; + } + /** * {@inheritDoc} */ @@ -157,6 +215,27 @@ public function setEntityHint($entityHint) $this->entityHint = $entityHint; } + /** + * {@inheritDoc} + */ + public function setIdentifierField($identifierField) + { + $this->identifierField = $identifierField; + } + + /** + * {@inheritDoc} + */ + public function getIdentifierField() + { + if (null == $this->identifierField && $this->entityName && $this->entityManager) { + $this->identifierField = + current($this->entityManager->getClassMetadata($this->entityName)->getIdentifierFieldNames()); + } + + return $this->identifierField; + } + /** * {@inheritDoc} */ @@ -174,7 +253,7 @@ public function getDatagrid() $listCollection = $this->listBuilder->getBaseList(); /** @var $fieldDescription FieldDescriptionInterface */ - foreach ($this->getListFields() as $fieldDescription) { + foreach ($this->getFieldDescriptionCollection() as $fieldDescription) { $listCollection->add($fieldDescription); } @@ -196,10 +275,21 @@ public function getDatagrid() $listCollection, $this->routeGenerator, $this->parameters, - $this->name, - $this->entityHint + $this->name ); + $this->configureDatagrid($datagrid); + + return $datagrid; + } + + /** + * Process datagrid configuration + * + * @param DatagridInterface $datagrid + */ + protected function configureDatagrid(DatagridInterface $datagrid) + { // add properties foreach ($this->getProperties() as $property) { $this->datagridBuilder->addProperty($datagrid, $property); @@ -222,7 +312,50 @@ public function getDatagrid() $this->datagridBuilder->addRowAction($datagrid, $actionParameters); } - return $datagrid; + $massActions = $this->getMassActions(); + // add mass actions + foreach ($massActions as $massAction) { + $this->datagridBuilder->addMassAction($datagrid, $massAction); + } + + // add "selected rows: filter if mass actions exist and identifier is configured + if (count($massActions) && $this->identifierField) { + $this->datagridBuilder->addSelectedRowFilter( + $datagrid, + $this->getSelectedRowFilterDefaultOptions() + ); + } + + // add toolbar options + $datagrid->setToolbarOptions($this->getToolbarOptions()); + + // set identifier field name + if ($this->getIdentifierField()) { + $datagrid->setIdentifierFieldName($this->getIdentifierField()); + } + + // set identifier field name + $datagrid->setEntityName($this->entityName); + $datagrid->setName($this->name); + $datagrid->setEntityHint($this->entityHint); + } + + /** + * Provide ability to override default "selected rows" filter setting + * e.g label, show/hide filter, field name + * + * @return array + */ + protected function getSelectedRowFilterDefaultOptions() + { + return array( + 'field_mapping' => array( + 'fieldName' => $this->identifierField + ), + 'field_name' => $this->identifierField, + 'show_filter' => true, + 'label' => $this->translate('oro.grid.mass_action.selected_rows') + ); } /** @@ -232,6 +365,7 @@ protected function createQuery() { $query = $this->queryFactory->createQuery(); $this->prepareQuery($query); + return $query; } @@ -269,11 +403,54 @@ protected function getFieldDescriptionCollection() if (!$this->fieldsCollection) { $this->fieldsCollection = new FieldDescriptionCollection(); $this->configureFields($this->fieldsCollection); + $this->configureIdentifierField($this->fieldsCollection); } return $this->fieldsCollection; } + /** + * @param FieldDescriptionCollection $fieldCollection + */ + protected function configureIdentifierField(FieldDescriptionCollection $fieldCollection) + { + $identifierField = $this->createIdentifierField(); + if ($identifierField && !$fieldCollection->has($identifierField->getName())) { + $fieldCollection->add($identifierField); + $this->identifierField = $identifierField->getName(); + } + } + + /** + * @return FieldDescription|null + */ + protected function createIdentifierField() + { + $identifierFieldName = $this->getIdentifierField(); + + if (!$identifierFieldName) { + return null; + } + + $field = new FieldDescription(); + $field->setName($identifierFieldName); + $options = array( + 'field_name' => $identifierFieldName, + 'type' => FieldDescriptionInterface::TYPE_INTEGER, + 'label' => $this->translate($this->identifierField), + 'filter_type' => FilterInterface::TYPE_NUMBER, + 'show_column' => false + ); + + if ($this->queryEntityAlias) { + $options['entity_alias'] = $this->queryEntityAlias; + } + + $field->setOptions($options); + + return $field; + } + /** * Configure collection of field descriptions * @@ -293,20 +470,10 @@ public function getRouteGenerator() return $this->routeGenerator; } - /** - * Get list of datagrid fields - * - * @return FieldDescriptionInterface[] - */ - protected function getListFields() - { - return $this->getFieldDescriptionCollection()->getElements(); - } - /** * Get list of properties * - * @return PropertyInterface + * @return PropertyInterface[] */ protected function getProperties() { @@ -359,6 +526,16 @@ protected function getRowActions() return array(); } + /** + * Get list of mass actions + * + * @return MassActionInterface[] + */ + protected function getMassActions() + { + return array(); + } + /** * Get default parameters * @@ -404,13 +581,59 @@ protected function getDefaultFilters() */ protected function getDefaultPager() { - return array(); + $defaultPager = array(); + $options = $this->getToolbarOptions(); + + $options = array_merge_recursive( + array( + 'hide' => false, + 'pageSize' => array( + 'hide' => false, + 'items' => array() + ), + 'pagination' => array( + 'hide' => false, + ) + ), + $options + ); + + // check all label exists + $zeroItem = array_filter( + $options['pageSize']['items'], + function ($item) { + $item = isset($item['size']) ? $item['size'] : $item; + + return $item == 0; + } + ); + $notExists = count($zeroItem) == 0; + + $hidden = in_array( + true, + array($options['hide'] , $options['pagination']['hide'] , $options['pageSize']['hide']) + ); + + if ($hidden) { + $defaultPager['_per_page'] = 0; + } + + // add 'all' pageSize + if ($notExists && $hidden) { + $options['pageSize']['items'][] = array( + 'size' => 0, + 'label' => $this->translate('oro.grid.datagrid.page_size.all') + ); + $this->toolbarOptions = $options; + } + + return $defaultPager; } /** - * @param string $id - * @param array $parameters - * @param string $domain + * @param string $id + * @param array $parameters + * @param string $domain * @return string */ protected function translate($id, array $parameters = array(), $domain = null) @@ -421,4 +644,14 @@ protected function translate($id, array $parameters = array(), $domain = null) return $this->translator->trans($id, $parameters, $domain); } + + /** + * Define grid toolbar options as assoc array + * + * @return array + */ + public function getToolbarOptions() + { + return $this->toolbarOptions; + } } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerInterface.php index 98a2e17efde..b4ffcbd06c9 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerInterface.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerInterface.php @@ -2,10 +2,11 @@ namespace Oro\Bundle\GridBundle\Datagrid; +use Doctrine\ORM\EntityManager; + use Symfony\Component\Validator\ValidatorInterface; use Symfony\Component\Routing\Router; use Symfony\Component\Translation\TranslatorInterface; -use Symfony\Component\HttpFoundation\Request; use Oro\Bundle\GridBundle\Builder\DatagridBuilderInterface; use Oro\Bundle\GridBundle\Builder\ListBuilderInterface; @@ -17,27 +18,59 @@ interface DatagridManagerInterface /** * Set unique name * - * @param string $name + * @param string $name * @return void */ public function setName($name); + /** + * Set entity manager + * + * @param EntityManager $entityManager + * @return void + */ + public function setEntityManager(EntityManager $entityManager); + + /** + * Set entity name + * + * @param string $entityName + * @return void + */ + public function setEntityName($entityName); + + /** + * Set query entity alias + * + * @param string $queryEntityAlias + * @return void + */ + public function setQueryEntityAlias($queryEntityAlias); + /** * Set entity hint * - * @param string $entityHint + * @param string $entityHint * @return void */ public function setEntityHint($entityHint); /** - * @param DatagridBuilderInterface $datagridBuilder + * Pass identifier field name from configuration + * + * @param string $identifier + * @return void + */ + public function setIdentifierField($identifier); + + /** + * @param DatagridBuilderInterface $datagridBuilder * @return void */ public function setDatagridBuilder(DatagridBuilderInterface $datagridBuilder); /** - * @param ListBuilderInterface $listBuilder + * @param ListBuilderInterface $listBuilder * @return void */ public function setListBuilder(ListBuilderInterface $listBuilder); @@ -48,43 +81,43 @@ public function setListBuilder(ListBuilderInterface $listBuilder); public function getDatagrid(); /** - * @param QueryFactoryInterface $queryManager + * @param QueryFactoryInterface $queryManager * @return void */ public function setQueryFactory(QueryFactoryInterface $queryManager); /** - * @param TranslatorInterface $translator + * @param TranslatorInterface $translator * @return void */ public function setTranslator(TranslatorInterface $translator); /** - * @param string $translationDomain + * @param string $translationDomain * @return void */ public function setTranslationDomain($translationDomain); /** - * @param ValidatorInterface $validator + * @param ValidatorInterface $validator * @return void */ public function setValidator(ValidatorInterface $validator); /** - * @param Router $router + * @param Router $router * @return void */ public function setRouter(Router $router); /** - * @param RouteGeneratorInterface $routeGenerator + * @param RouteGeneratorInterface $routeGenerator * @return void */ public function setRouteGenerator(RouteGeneratorInterface $routeGenerator); /** - * @param ParametersInterface $parameters + * @param ParametersInterface $parameters * @return void */ public function setParameters(ParametersInterface $parameters); diff --git a/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerRegistry.php b/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerRegistry.php new file mode 100644 index 00000000000..73c5a2d7c1a --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Datagrid/DatagridManagerRegistry.php @@ -0,0 +1,70 @@ +container = $container; + } + + /** + * @param string $name + * @param string $serviceId + * @throws \LogicException + */ + public function addDatagridManagerService($name, $serviceId) + { + if (isset($this->services[$name])) { + throw new \LogicException(sprintf('Datagrid manager with name "%s" already exists', $name)); + } + + $this->services[$name] = $serviceId; + } + + /** + * @param string $name + * @return bool + */ + public function hasDatagridManager($name) + { + return !empty($this->services[$name]); + } + + /** + * @param string $name + * @return DatagridManagerInterface + * @throws \LogicException + */ + public function getDatagridManager($name) + { + if (!$this->hasDatagridManager($name)) { + throw new \LogicException(sprintf('Datagrid manager with name "%s" is not exist', $name)); + } + + $serviceId = $this->services[$name]; + if (!$this->container->has($serviceId)) { + throw new \LogicException(sprintf('Datagrid manager with service ID "%s" is not exist', $serviceId)); + } + + return $this->container->get($serviceId); + } +} diff --git a/src/Oro/Bundle/GridBundle/Datagrid/FlexibleDatagridManager.php b/src/Oro/Bundle/GridBundle/Datagrid/FlexibleDatagridManager.php index 7e3816825b0..7475d04448e 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/FlexibleDatagridManager.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/FlexibleDatagridManager.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\GridBundle\Datagrid; -use Symfony\Component\DependencyInjection\ContainerInterface; use Oro\Bundle\FlexibleEntityBundle\Manager\FlexibleManager; use Oro\Bundle\FlexibleEntityBundle\Model\AbstractAttribute; use Oro\Bundle\FlexibleEntityBundle\AttributeType\AbstractAttributeType; @@ -88,7 +87,7 @@ public function setFlexibleManager(FlexibleManager $flexibleManager) * Traverse all flexible attributes and add them as fields to collection * * @param FieldDescriptionCollection $fieldsCollection - * @param array $options + * @param array $options */ protected function configureFlexibleFields( FieldDescriptionCollection $fieldsCollection, @@ -107,8 +106,8 @@ protected function configureFlexibleFields( /** * @param FieldDescriptionCollection $fieldsCollection - * @param string $attributeCode - * @param array $options + * @param string $attributeCode + * @param array $options */ protected function configureFlexibleField( FieldDescriptionCollection $fieldsCollection, @@ -128,8 +127,8 @@ protected function configureFlexibleField( /** * Create field by flexible attribute * - * @param AbstractAttribute $attribute - * @param array $options + * @param AbstractAttribute $attribute + * @param array $options * @return FieldDescriptionInterface */ protected function createFlexibleField(AbstractAttribute $attribute, array $options = array()) @@ -144,8 +143,8 @@ protected function createFlexibleField(AbstractAttribute $attribute, array $opti /** * Get options for flexible field * - * @param AbstractAttribute $attribute - * @param array $options + * @param AbstractAttribute $attribute + * @param array $options * @return array */ protected function getFlexibleFieldOptions(AbstractAttribute $attribute, array $options = array()) @@ -197,7 +196,7 @@ protected function getFlexibleAttributes() } /** - * @param string $code + * @param string $code * @return AbstractAttribute * @throws \LogicException */ @@ -212,7 +211,7 @@ protected function getFlexibleAttribute($code) } /** - * @param string $code + * @param string $code * @return boolean */ protected function hasFlexibleAttribute($code) diff --git a/src/Oro/Bundle/GridBundle/Datagrid/IterableResultInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/IterableResultInterface.php new file mode 100644 index 00000000000..76369daaa76 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Datagrid/IterableResultInterface.php @@ -0,0 +1,16 @@ +setMaxPerPage($maxPerPage); + } + + /** + * Returns an array of results on the given page. + * + * @return array + */ + abstract public function getResults(); + + /** + * Returns the current pager's max link. + * + * @return integer + */ + public function getCurrentMaxLink() + { + return $this->currentMaxLink; + } + + /** + * Returns the current pager's max record limit. + * + * @return integer + */ + public function getMaxRecordLimit() + { + return $this->maxRecordLimit; + } + + /** + * Sets the current pager's max record limit. + * + * @param integer $limit + */ + public function setMaxRecordLimit($limit) + { + $this->maxRecordLimit = $limit; + } + + /** + * Returns an array of page numbers to use in pagination links. + * + * @param integer $nb_links The maximum number of page numbers to return + * + * @return array + */ + public function getLinks($nb_links = null) + { + if ($nb_links == null) { + $nb_links = $this->getMaxPageLinks(); + } + $links = array(); + $tmp = $this->page - floor($nb_links / 2); + $check = $this->lastPage - $nb_links + 1; + + $limit = 1; + if ($check > 0) { + $limit = $check; + } + + $begin = 1; + if ($tmp > 0) { + $begin = $tmp > $limit ? $limit : $tmp; + } + + $i = (int) $begin; + while ($i < $begin + $nb_links && $i <= $this->lastPage) { + $links[] = $i++; + } + + $this->currentMaxLink = 1; + if (count($links)) { + $this->currentMaxLink = $links[count($links) - 1]; + } + + return $links; + } + + /** + * Returns true if the current query requires pagination. + * + * @return boolean + */ + public function haveToPaginate() + { + return $this->getMaxPerPage() && $this->getNbResults() > $this->getMaxPerPage(); + } + + /** + * Returns the current cursor. + * + * @return integer + */ + public function getCursor() + { + return $this->cursor; + } + + /** + * Sets the current cursor. + * + * @param integer $pos + */ + public function setCursor($pos) + { + if ($pos < 1) { + $this->cursor = 1; + } else { + if ($pos > $this->nbResults) { + $this->cursor = $this->nbResults; + } else { + $this->cursor = $pos; + } + } + } + + /** + * Returns an object by cursor position. + * + * @param integer $pos + * + * @return mixed + */ + public function getObjectByCursor($pos) + { + $this->setCursor($pos); + + return $this->getCurrent(); + } + + /** + * Returns the current object. + * + * @return mixed + */ + public function getCurrent() + { + return $this->retrieveObject($this->cursor); + } + + /** + * Returns the next object. + * + * @return mixed|null + */ + public function getNext() + { + if ($this->cursor + 1 > $this->nbResults) { + return null; + } else { + return $this->retrieveObject($this->cursor + 1); + } + } + + /** + * Returns the previous object. + * + * @return mixed|null + */ + public function getPrevious() + { + if ($this->cursor - 1 < 1) { + return null; + } else { + return $this->retrieveObject($this->cursor - 1); + } + } + + /** + * Returns the first index on the current page. + * + * @return integer + */ + public function getFirstIndice() + { + if ($this->page == 0) { + return 1; + } else { + return ($this->page - 1) * $this->maxPerPage + 1; + } + } + + /** + * Returns the last index on the current page. + * + * @return integer + */ + public function getLastIndice() + { + if ($this->page == 0) { + return $this->nbResults; + } else { + if ($this->page * $this->maxPerPage >= $this->nbResults) { + return $this->nbResults; + } else { + return $this->page * $this->maxPerPage; + } + } + } + + /** + * Returns the number of results. + * + * @return integer + */ + public function getNbResults() + { + return $this->nbResults; + } + + /** + * Sets the number of results. + * + * @param integer $nb + */ + protected function setNbResults($nb) + { + $this->nbResults = $nb; + } + + /** + * Returns the first page number. + * + * @return integer + */ + public function getFirstPage() + { + return 1; + } + + /** + * Returns the last page number. + * + * @return integer + */ + public function getLastPage() + { + return $this->lastPage; + } + + /** + * Sets the last page number. + * + * @param integer $page + */ + protected function setLastPage($page) + { + $this->lastPage = $page; + + if ($this->getPage() > $page) { + $this->setPage($page); + } + } + + /** + * Returns the current page. + * + * @return integer + */ + public function getPage() + { + return $this->page; + } + + /** + * Returns the next page. + * + * @return integer + */ + public function getNextPage() + { + return min($this->getPage() + 1, $this->getLastPage()); + } + + /** + * Returns the previous page. + * + * @return integer + */ + public function getPreviousPage() + { + return max($this->getPage() - 1, $this->getFirstPage()); + } + + /** + * Sets the current page. + * + * @param integer $page + */ + public function setPage($page) + { + $this->page = intval($page); + + if ($this->page <= 0) { + // set first page, which depends on a maximum set + $this->page = $this->getMaxPerPage() ? 1 : 0; + } + } + + /** + * Returns the maximum number of results per page. + * + * @return integer + */ + public function getMaxPerPage() + { + return $this->maxPerPage; + } + + /** + * Sets the maximum number of results per page. + * + * @param integer $max + */ + public function setMaxPerPage($max) + { + if ($max > 0) { + $this->maxPerPage = $max; + if ($this->page == 0) { + $this->page = 1; + } + } else { + if ($max == 0) { + $this->maxPerPage = 0; + $this->page = 0; + } else { + $this->maxPerPage = 1; + if ($this->page == 0) { + $this->page = 1; + } + } + } + } + + /** + * Returns the maximum number of page numbers. + * + * @return integer + */ + public function getMaxPageLinks() + { + return $this->maxPageLinks; + } + + /** + * Sets the maximum number of page numbers. + * + * @param integer $maxPageLinks + */ + public function setMaxPageLinks($maxPageLinks) + { + $this->maxPageLinks = $maxPageLinks; + } + + /** + * Returns true if on the first page. + * + * @return boolean + */ + public function isFirstPage() + { + return 1 == $this->page; + } + + /** + * Returns true if on the last page. + * + * @return boolean + */ + public function isLastPage() + { + return $this->page == $this->lastPage; + } + + /** + * Returns the current pager's parameter holder. + * + * @return array + */ + public function getParameters() + { + return $this->parameters; + } + + /** + * Returns a parameter. + * + * @param string $name + * @param mixed $default + * + * @return mixed + */ + public function getParameter($name, $default = null) + { + return isset($this->parameters[$name]) ? $this->parameters[$name] : $default; + } + + /** + * Checks whether a parameter has been set. + * + * @param string $name + * + * @return boolean + */ + public function hasParameter($name) + { + return isset($this->parameters[$name]); + } + + /** + * Sets a parameter. + * + * @param string $name + * @param mixed $value + */ + public function setParameter($name, $value) + { + $this->parameters[$name] = $value; + } + + /** + * Returns true if the properties used for iteration have been initialized. + * + * @return boolean + */ + protected function isIteratorInitialized() + { + return null !== $this->results; + } + + /** + * Loads data into properties used for iteration. + */ + protected function initializeIterator() + { + $this->results = $this->getResults(); + $this->resultsCounter = count($this->results); + } + + /** + * Empties properties used for iteration. + */ + protected function resetIterator() + { + $this->results = null; + $this->resultsCounter = 0; + } + + /** + * {@inheritdoc} + */ + public function current() + { + if (!$this->isIteratorInitialized()) { + $this->initializeIterator(); + } + + return current($this->results); + } + + /** + * {@inheritdoc} + */ + public function key() + { + if (!$this->isIteratorInitialized()) { + $this->initializeIterator(); + } + + return key($this->results); + } + + /** + * {@inheritdoc} + */ + public function next() + { + if (!$this->isIteratorInitialized()) { + $this->initializeIterator(); + } + + --$this->resultsCounter; + + return next($this->results); + } + + /** + * {@inheritdoc} + */ + public function rewind() + { + if (!$this->isIteratorInitialized()) { + $this->initializeIterator(); + } + + $this->resultsCounter = count($this->results); + + return reset($this->results); + } + + /** + * {@inheritdoc} + */ + public function valid() + { + if (!$this->isIteratorInitialized()) { + $this->initializeIterator(); + } + + return $this->resultsCounter > 0; + } + + /** + * {@inheritdoc} + */ + public function count() + { + return $this->getNbResults(); + } + + /** + * {@inheritdoc} + */ + public function serialize() + { + $vars = get_object_vars($this); + unset($vars['query']); + + return serialize($vars); + } + + /** + * {@inheritdoc} + */ + public function unserialize($serialized) + { + $array = unserialize($serialized); + + foreach ($array as $name => $values) { + $this->$name = $values; + } + } + + /** + * @return array + */ + public function getCountColumn() + { + return $this->countColumn; + } + + /** + * @param array $countColumn + * + * @return array + */ + public function setCountColumn(array $countColumn) + { + return $this->countColumn = $countColumn; + } + + /** + * Retrieve the object for a certain offset + * + * @param integer $offset + * + * @return object + */ + protected function retrieveObject($offset) + { + $queryForRetrieve = clone $this->getQuery(); + $queryForRetrieve + ->setFirstResult($offset - 1) + ->setMaxResults(1); + + $results = $queryForRetrieve->execute(); + + return $results[0]; + } + + /** + * @param mixed $query + */ + public function setQuery($query) + { + $this->query = $query; + } + + /** + * @return ProxyQueryInterface + */ + public function getQuery() + { + return $this->query; + } +} diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ORM/CountCalculator.php b/src/Oro/Bundle/GridBundle/Datagrid/ORM/CountCalculator.php index 58d5768e59c..f24dca673d8 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/ORM/CountCalculator.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/ORM/CountCalculator.php @@ -10,7 +10,7 @@ class CountCalculator { /** - * @param Query $query + * @param Query $query * @return int */ public function getCount(Query $query) @@ -27,12 +27,12 @@ public function getCount(Query $query) ); $result = $statement->fetchColumn(); - return $result ? (int)$result : 0; + return $result ? (int) $result : 0; } /** - * @param Query $query - * @param array $paramMappings + * @param Query $query + * @param array $paramMappings * @return array * @throws \Doctrine\ORM\Query\QueryException */ diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ORM/EntityProxyQuery.php b/src/Oro/Bundle/GridBundle/Datagrid/ORM/EntityProxyQuery.php new file mode 100644 index 00000000000..9e88e68aeae --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Datagrid/ORM/EntityProxyQuery.php @@ -0,0 +1,134 @@ +getQueryBuilder(); + + $this->applyWhere($qb); + $this->applyOrderByParameters($qb); + + return $qb; + } + + /** + * Get query builder for result count query + * + * @return QueryBuilder + */ + protected function getCountQueryBuilder() + { + return clone $this->getResultIdsQueryBuilder(); + } + + /** + * Apply where part on query builder + * + * @param QueryBuilder $qb + */ + protected function applyWhere(QueryBuilder $qb) + { + $idx = $this->getResultIds(); + + if (count($idx) > 0) { + $qb->where($qb->expr()->in($this->getIdFieldFQN(), ':ids')) + ->resetDQLPart('having') + ->setMaxResults(null) + ->setFirstResult(null) + ->setParameter('ids', $idx); + + // Since DQL has been changed, some parameters potentially are not used anymore. + $this->fixUnusedParameters($qb); + } + } + + /** + * Fetches ids of objects that query builder targets + * + * @return array + */ + protected function getResultIds() + { + $idx = array(); + $query = $this->getResultIdsQueryBuilder()->getQuery(); + + $this->applyQueryHints($query); + + $results = $query->execute(array(), Query::HYDRATE_ARRAY); + $connection = $this->getQueryBuilder()->getEntityManager()->getConnection(); + + foreach ($results as $id) { + $idx[] = is_int($id[$this->getIdFieldName()]) + ? $id[$this->getIdFieldName()] + : $connection->quote($id[$this->getIdFieldName()]); + } + + return $idx; + } + + /** + * Creates query builder that selects only id's of result objects + * + * @return QueryBuilder + */ + protected function getResultIdsQueryBuilder() + { + $qb = clone $this->getQueryBuilder(); + + // Apply orderBy before change select, because it can contain some expressions from select as aliases + $this->applyOrderByParameters($qb); + + $selectExpressions = array('DISTINCT ' . $this->getIdFieldFQN()); + // We must leave expressions used in having + $selectExpressions = array_merge($selectExpressions, $this->selectWhitelist); + $qb->select($selectExpressions); + + // adding of sort by parameters to select + // TODO move this logic to addOrderBy method after removing of flexible entity + /** @var $orderExpression Query\Expr\OrderBy */ + foreach ($qb->getDQLPart('orderBy') as $orderExpression) { + foreach ($orderExpression->getParts() as $orderString) { + $orderField = trim(str_ireplace(array(' asc', ' desc'), '', $orderString)); + if (!$this->hasSelectItem($qb, $orderField)) { + $qb->addSelect($orderField); + } + } + } + + // Since DQL has been changed, some parameters potentially are not used anymore. + $this->fixUnusedParameters($qb); + + return $qb; + } + + /** + * Removes unused parameters from query builder + * + * @param QueryBuilder $qb + */ + protected function fixUnusedParameters(QueryBuilder $qb) + { + $dql = $qb->getDQL(); + $usedParameters = array(); + + /** @var $parameter \Doctrine\ORM\Query\Parameter */ + foreach ($qb->getParameters() as $parameter) { + if ($this->dqlContainsParameter($dql, $parameter->getName())) { + $usedParameters[$parameter->getName()] = $parameter->getValue(); + } + } + + $qb->setParameters($usedParameters); + } +} diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ORM/IterableResult.php b/src/Oro/Bundle/GridBundle/Datagrid/ORM/IterableResult.php new file mode 100644 index 00000000000..90cbf44ec8c --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Datagrid/ORM/IterableResult.php @@ -0,0 +1,210 @@ +query = clone $query; + $this->setBufferSize($pageSize); + } + + /** + * {@inheritDoc} + */ + public function setBufferSize($pageSize) + { + $this->pageSize = (int) $pageSize; + if ($this->pageSize <= 0) { + throw new \InvalidArgumentException('$pageSize must be greater than 0'); + } + if ($this->query->getMaxResults() && $this->query->getMaxResults() < $this->pageSize) { + $this->pageSize = $this->query->getMaxResults(); + } + } + + /** + * {@inheritDoc} + */ + public function current() + { + return $this->current; + } + + /** + * {@inheritDoc} + */ + public function next() + { + $this->offset++; + + if (!isset($this->rows[$this->offset]) && !$this->loadNextPage()) { + return $this->current = null; + } + + $this->current = new ResultRecord($this->rows[$this->offset]); + $this->key = $this->offset + $this->pageSize * $this->page; + + return new ResultRecord($this->rows[$this->offset]); + } + + /** + * Attempts to load next page + * + * @return bool If page loaded successfully + */ + protected function loadNextPage() + { + $totalPages = ceil($this->count() / $this->pageSize); + if (!$totalPages || $totalPages <= $this->page + 1) { + unset($this->rows); + + return false; + } + + $this->page++; + $this->offset = 0; + + $pageQuery = clone $this->query; + $pageQuery->setFirstResult($this->pageSize * $this->page + $this->query->getFirstResult()); + $pageQuery->setMaxResults($this->pageSize); + + $this->rows = $pageQuery->execute(); + + if (!count($this->rows)) { + return false; + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function key() + { + return $this->key; + } + + /** + * {@inheritDoc} + */ + public function valid() + { + return $this->current !== null; + } + + /** + * {@inheritDoc} + */ + public function rewind() + { + if ($this->rewound == true) { + throw new \RuntimeException("Can only iterate a Result once."); + } else { + $this->current = $this->next(); + $this->rewound = true; + } + } + + /** + * {@inheritDoc} + */ + public function count() + { + if (null === $this->totalCount) { + $countCalculator = new CountCalculator(); + $countQuery = $this->query->getQueryBuilder()->getQuery(); + foreach ($this->query->getQueryHints() as $name => $value) { + $countQuery->setHint($name, $value); + } + $this->totalCount = $countCalculator->getCount($countQuery); + } + + return $this->totalCount; + } +} diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ORM/Pager.php b/src/Oro/Bundle/GridBundle/Datagrid/ORM/Pager.php index c7c1ce48b6d..9cd758c93a4 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/ORM/Pager.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/ORM/Pager.php @@ -2,13 +2,19 @@ namespace Oro\Bundle\GridBundle\Datagrid\ORM; -use Sonata\DoctrineORMAdminBundle\Datagrid\Pager as BasePager; +use Doctrine\ORM\QueryBuilder; +use Doctrine\ORM\Query; use Oro\Bundle\GridBundle\Datagrid\PagerInterface; use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; -class Pager extends BasePager implements PagerInterface +class Pager extends AbstractPager implements PagerInterface { + /** + * @var QueryBuilder|null + */ + protected $queryBuilder = null; + /** * {@inheritdoc} */ @@ -30,6 +36,45 @@ public function computeNbResult() */ public function getQuery() { - return parent::getQuery(); + return $this->query; + } + + /** + * {@inheritdoc} + */ + public function getResults($hydrationMode = Query::HYDRATE_OBJECT) + { + return $this->getQuery()->execute(array(), $hydrationMode); + } + + /** + * {@inheritdoc} + */ + public function init() + { + $this->resetIterator(); + + $this->setNbResults($this->computeNbResult()); + + /** @var QueryBuilder $query */ + $query = $this->getQuery(); + + $query->setFirstResult(null); + $query->setMaxResults(null); + + if (count($this->getParameters()) > 0) { + $query->setParameters($this->getParameters()); + } + + if (0 == $this->getPage() || 0 == $this->getMaxPerPage() || 0 == $this->getNbResults()) { + $this->setLastPage(0); + } else { + $offset = ($this->getPage() - 1) * $this->getMaxPerPage(); + + $this->setLastPage(ceil($this->getNbResults() / $this->getMaxPerPage())); + + $query->setFirstResult($offset); + $query->setMaxResults($this->getMaxPerPage()); + } } } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ORM/ProxyQuery.php b/src/Oro/Bundle/GridBundle/Datagrid/ORM/ProxyQuery.php index 2d65c54dd74..f2c54033d63 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/ORM/ProxyQuery.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/ORM/ProxyQuery.php @@ -6,30 +6,48 @@ use Doctrine\ORM\Query; use Doctrine\ORM\AbstractQuery; -use Sonata\DoctrineORMAdminBundle\Datagrid\ProxyQuery as BaseProxyQuery; - use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; /** * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) * TODO: This class should be refactored (BAP-969). */ -class ProxyQuery extends BaseProxyQuery implements ProxyQueryInterface +class ProxyQuery implements ProxyQueryInterface { + /** + * @var QueryBuilder + */ + protected $queryBuilder; + /** * @var string */ - protected $idFieldName; + protected $sortBy; /** * @var string */ - protected $rootAlias; + protected $sortOrder; /** - * @var QueryBuilder + * @var string */ - protected $queryBuilder; + protected $parameterUniqueId; + + /** + * @var string + */ + protected $entityJoinAliases; + + /** + * @var string + */ + protected $idFieldName; + + /** + * @var string + */ + protected $rootAlias; /** * @var array @@ -46,6 +64,16 @@ class ProxyQuery extends BaseProxyQuery implements ProxyQueryInterface */ protected $queryHints = array(); + /** + * @param mixed $queryBuilder + */ + public function __construct($queryBuilder) + { + $this->queryBuilder = $queryBuilder; + $this->uniqueParameterId = 0; + $this->entityJoinAliases = array(); + } + /** * Get query builder * @@ -57,24 +85,23 @@ public function getQueryBuilder() } /** - * Get records total count + * Get the total number of records * * @return int */ public function getTotalCount() { - $qb = clone $this->getResultIdsQueryBuilder(); - $qb->setFirstResult(null); - $qb->setMaxResults(null); - $qb->resetDQLPart('orderBy'); + $query = $this->getCountQueryBuilder() + ->setFirstResult(null) + ->setMaxResults(null) + ->resetDQLPart('orderBy') + ->getQuery(); - $query = $qb->getQuery(); $this->applyQueryHints($query); $countCalculator = new CountCalculator(); - $totalCount = $countCalculator->getCount($query); - return $totalCount; + return $countCalculator->getCount($query); } /** @@ -83,7 +110,9 @@ public function getTotalCount() public function execute(array $params = array(), $hydrationMode = null) { $query = $this->getResultQueryBuilder()->getQuery(); + $this->applyQueryHints($query); + return $query->execute($params, $hydrationMode); } @@ -96,54 +125,26 @@ protected function getResultQueryBuilder() { $qb = clone $this->getQueryBuilder(); - $this->applyWhere($qb); $this->applyOrderByParameters($qb); return $qb; } /** - * Apply where part on query builder + * Get query builder for result count query * - * @param QueryBuilder $qb * @return QueryBuilder */ - protected function applyWhere(QueryBuilder $qb) + protected function getCountQueryBuilder() { - $idx = $this->getResultIds(); - if (count($idx) > 0) { - $qb->where(sprintf('%s IN (%s)', $this->getIdFieldFQN(), implode(',', $idx))); - $qb->resetDQLPart('having'); - $qb->setMaxResults(null); - $qb->setFirstResult(null); - // Since DQL has been changed, some parameters potentially are not used anymore. - $this->fixUnusedParameters($qb); - } - } - - /** - * Removes unused parameters from query builder - * - * @param QueryBuilder $qb - */ - protected function fixUnusedParameters(QueryBuilder $qb) - { - $dql = $qb->getDQL(); - $usedParameters = array(); - /** @var $parameter \Doctrine\ORM\Query\Parameter */ - foreach ($qb->getParameters() as $parameter) { - if ($this->dqlContainsParameter($dql, $parameter->getName())) { - $usedParameters[$parameter->getName()] = $parameter->getValue(); - } - } - $qb->setParameters($usedParameters); + return clone $this->getResultQueryBuilder(); } /** * Returns TRUE if $dql contains usage of parameter with $parameterName * - * @param string $dql - * @param string $parameterName + * @param string $dql + * @param string $parameterName * @return bool */ protected function dqlContainsParameter($dql, $parameterName) @@ -153,13 +154,14 @@ protected function dqlContainsParameter($dql, $parameterName) } else { $pattern = sprintf('/\:%s[^\w]/', preg_quote($parameterName)); } - return (bool)preg_match($pattern, $dql . ' '); + + return (bool) preg_match($pattern, $dql . ' '); } /** * Apply order by part * - * @param QueryBuilder $queryBuilder + * @param QueryBuilder $queryBuilder * @return QueryBuilder */ protected function applyOrderByParameters(QueryBuilder $queryBuilder) @@ -173,7 +175,7 @@ protected function applyOrderByParameters(QueryBuilder $queryBuilder) * Apply sorting * * @param QueryBuilder $queryBuilder - * @param array $sortOrder + * @param array $sortOrder */ protected function applySortOrderParameters(QueryBuilder $queryBuilder, array $sortOrder) { @@ -186,8 +188,8 @@ protected function applySortOrderParameters(QueryBuilder $queryBuilder, array $s /** * Checks if select DQL part already has select expression with name * - * @param QueryBuilder $queryBuilder - * @param string $name + * @param QueryBuilder $queryBuilder + * @param string $name * @return bool */ protected function hasSelectItem(QueryBuilder $queryBuilder, $name) @@ -204,70 +206,15 @@ protected function hasSelectItem(QueryBuilder $queryBuilder, $name) } } } - return false; - } - - /** - * Fetches ids of objects that query builder targets - * - * @return array - */ - protected function getResultIds() - { - $idx = array(); - - $query = $this->getResultIdsQueryBuilder()->getQuery(); - $this->applyQueryHints($query); - $results = $query->execute(array(), Query::HYDRATE_ARRAY); - - $connection = $this->getQueryBuilder()->getEntityManager()->getConnection(); - foreach ($results as $id) { - $idx[] = $connection->quote($id[$this->getIdFieldName()]); - } - - return $idx; - } - - /** - * Creates query builder that selects only id's of result objects - * - * @return QueryBuilder - */ - protected function getResultIdsQueryBuilder() - { - $qb = clone $this->getQueryBuilder(); - // Apply orderBy before change select, because it can contain some expressions from select as aliases - $this->applyOrderByParameters($qb); - - $selectExpressions = array('DISTINCT ' . $this->getIdFieldFQN()); - // We must leave expressions used in having - $selectExpressions = array_merge($selectExpressions, $this->selectWhitelist); - $qb->select($selectExpressions); - - // adding of sort by parameters to select - // TODO move this logic to addOrderBy method after removing of flexible entity - /** @var $orderExpression Query\Expr\OrderBy */ - foreach ($qb->getDQLPart('orderBy') as $orderExpression) { - foreach ($orderExpression->getParts() as $orderString) { - $orderField = trim(str_ireplace(array(' asc', ' desc'), '', $orderString)); - if (!$this->hasSelectItem($qb, $orderField)) { - $qb->addSelect($orderField); - } - } - } - - // Since DQL has been changed, some parameters potentially are not used anymore. - $this->fixUnusedParameters($qb); - - return $qb; + return false; } /** * Check whether provided expression already in select clause * - * @param QueryBuilder $qb - * @param string $selectString + * @param QueryBuilder $qb + * @param string $selectString * @return bool */ protected function isInSelectExpression(QueryBuilder $qb, $selectString) @@ -278,6 +225,7 @@ protected function isInSelectExpression(QueryBuilder $qb, $selectString) return true; } } + return false; } @@ -339,6 +287,7 @@ public function getRootAlias() if (!$this->rootAlias) { $this->rootAlias = current($this->getQueryBuilder()->getRootAliases()); } + return $this->rootAlias; } @@ -379,8 +328,8 @@ protected function getIdFieldFQN() /** * Get fields fully qualified name * - * @param string $fieldName - * @param string|null $parentAlias + * @param string $fieldName + * @param string|null $parentAlias * @return string */ protected function getFieldFQN($fieldName, $parentAlias = null) @@ -388,14 +337,15 @@ protected function getFieldFQN($fieldName, $parentAlias = null) if (strpos($fieldName, '.') === false) { // add the current alias $fieldName = ($parentAlias ? : $this->getRootAlias()) . '.' . $fieldName; } + return $fieldName; } /** * Proxy of QueryBuilder::addSelect with flag that specified whether add select to internal whitelist * - * @param string $select - * @param bool $addToWhitelist + * @param string $select + * @param bool $addToWhitelist * @return ProxyQuery */ public function addSelect($select = null, $addToWhitelist = false) @@ -434,8 +384,8 @@ public function addSelect($select = null, $addToWhitelist = false) /** * Set query parameter * - * @param string $name - * @param mixed $value + * @param string $name + * @param mixed $value * @return ProxyQuery */ public function setParameter($name, $value) @@ -448,8 +398,8 @@ public function setParameter($name, $value) /** * Sets a query hint * - * @param string $name - * @param mixed $value + * @param string $name + * @param mixed $value * @return ProxyQuery */ public function setQueryHint($name, $value) @@ -459,6 +409,16 @@ public function setQueryHint($name, $value) return $this; } + /** + * Get a list of query hints + * + * @return array + */ + public function getQueryHints() + { + return $this->queryHints; + } + /** * @param AbstractQuery $query */ @@ -468,4 +428,120 @@ protected function applyQueryHints(AbstractQuery $query) $query->setHint($name, $value); } } + + /** + * {@inheritdoc} + */ + public function __call($name, $args) + { + return call_user_func_array(array($this->queryBuilder, $name), $args); + } + + /** + * {@inheritdoc} + */ + public function __get($name) + { + return $this->queryBuilder->$name; + } + + /** + * {@inheritdoc} + */ + public function setSortBy($parentAssociationMappings, $fieldMapping) + { + $alias = $this->entityJoin($parentAssociationMappings); + $this->sortBy = $alias . '.' . $fieldMapping['fieldName']; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSortBy() + { + return $this->sortBy; + } + + /** + * {@inheritdoc} + */ + public function setSortOrder($sortOrder) + { + $this->sortOrder = $sortOrder; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSortOrder() + { + return $this->sortOrder; + } + + /** + * {@inheritdoc} + */ + public function getSingleScalarResult() + { + /** @var Query $query */ + $query = $this->queryBuilder->getQuery(); + + return $query->getSingleScalarResult(); + } + + /** + * {@inheritdoc} + */ + public function __clone() + { + $this->queryBuilder = clone $this->queryBuilder; + } + + /** + * {@inheritdoc} + */ + public function setFirstResult($firstResult) + { + $this->queryBuilder->setFirstResult($firstResult); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFirstResult() + { + return $this->queryBuilder->getFirstResult(); + } + + /** + * {@inheritdoc} + */ + public function setMaxResults($maxResults) + { + $this->queryBuilder->setMaxResults($maxResults); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getMaxResults() + { + return $this->queryBuilder->getMaxResults(); + } + + /** + * {@inheritdoc} + */ + public function getUniqueParameterId() + { + return $this->uniqueParameterId++; + } } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/AbstractQueryFactory.php b/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/AbstractQueryFactory.php index 9d1b8d5cd9c..078f370b9a4 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/AbstractQueryFactory.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/AbstractQueryFactory.php @@ -3,9 +3,9 @@ namespace Oro\Bundle\GridBundle\Datagrid\ORM\QueryFactory; use Doctrine\ORM\QueryBuilder; + use Oro\Bundle\GridBundle\Datagrid\QueryFactoryInterface; use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; -use Oro\Bundle\GridBundle\Datagrid\ORM\ProxyQuery; abstract class AbstractQueryFactory implements QueryFactoryInterface { @@ -18,11 +18,5 @@ abstract class AbstractQueryFactory implements QueryFactoryInterface * @return ProxyQueryInterface * @throws \LogicException */ - public function createQuery() - { - if (!$this->queryBuilder) { - throw new \LogicException('Can\'t create datagrid query. Query builder is not configured.'); - } - return new ProxyQuery($this->queryBuilder); - } + abstract public function createQuery(); } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/EntityQueryFactory.php b/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/EntityQueryFactory.php index f15057436f7..1128aedc5de 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/EntityQueryFactory.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/EntityQueryFactory.php @@ -3,10 +3,11 @@ namespace Oro\Bundle\GridBundle\Datagrid\ORM\QueryFactory; use Symfony\Bridge\Doctrine\RegistryInterface; -use Doctrine\ORM\QueryBuilder; + use Doctrine\ORM\EntityRepository; use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; +use Oro\Bundle\GridBundle\Datagrid\ORM\EntityProxyQuery; class EntityQueryFactory extends AbstractQueryFactory { @@ -27,8 +28,8 @@ class EntityQueryFactory extends AbstractQueryFactory /** * @param RegistryInterface $registry - * @param string $className - * @param string $alias + * @param string $className + * @param string $alias */ public function __construct(RegistryInterface $registry, $className, $alias = 'o') { @@ -47,7 +48,7 @@ public function createQuery() $repository = $entityManager->getRepository($this->className); $this->queryBuilder = $repository->createQueryBuilder($this->alias); - return parent::createQuery(); + return new EntityProxyQuery($this->queryBuilder); } /** diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/QueryFactory.php b/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/QueryFactory.php index 867f89f4df5..868ed0f8d8c 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/QueryFactory.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/ORM/QueryFactory/QueryFactory.php @@ -4,6 +4,9 @@ use Doctrine\ORM\QueryBuilder; +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; +use Oro\Bundle\GridBundle\Datagrid\ORM\ProxyQuery; + class QueryFactory extends AbstractQueryFactory { /** @@ -14,6 +17,19 @@ public function __construct(QueryBuilder $queryBuilder = null) $this->queryBuilder = $queryBuilder; } + /** + * @return ProxyQueryInterface + * @throws \LogicException + */ + public function createQuery() + { + if (!$this->queryBuilder) { + throw new \LogicException('Can\'t create datagrid query. Query builder is not configured.'); + } + + return new ProxyQuery($this->queryBuilder); + } + /** * @param QueryBuilder $queryBuilder */ diff --git a/src/Oro/Bundle/GridBundle/Datagrid/PagerInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/PagerInterface.php index 273fdc0fd46..91018732f46 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/PagerInterface.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/PagerInterface.php @@ -2,34 +2,49 @@ namespace Oro\Bundle\GridBundle\Datagrid; -use Sonata\AdminBundle\Datagrid\PagerInterface as BasePagerInterface; - -interface PagerInterface extends BasePagerInterface +interface PagerInterface { /** - * @param ProxyQueryInterface $query + * Initialize the pager. + * + * @return void + */ + public function init(); + + /** + * Set query + * + * @param ProxyQueryInterface $query * @return void */ public function setQuery($query); /** - * @param int $maxPerPage + * Set max records per page + * + * @param int $maxPerPage * @return void */ public function setMaxPerPage($maxPerPage); /** + * Get max records per page + * * @return int */ public function getMaxPerPage(); /** - * @param int $page + * Set current page + * + * @param int $page * @return void */ public function setPage($page); /** + * Get current page + * * @return int */ public function getPage(); @@ -65,7 +80,7 @@ public function getFirstPage(); /** * Returns an array of page numbers to use in pagination links. * - * @param integer $nbLinks The maximum number of page numbers to return + * @param integer $nbLinks The maximum number of page numbers to return * @return array */ public function getLinks($nbLinks = null); diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ParametersInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/ParametersInterface.php index 9c7028fb9db..05ff02579db 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/ParametersInterface.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/ParametersInterface.php @@ -13,15 +13,15 @@ interface ParametersInterface /** * Get parameter value from parameters container * - * @param string $type - * @param mixed $default + * @param string $type + * @param mixed $default * @return array */ public function get($type, $default = null); /** - * @param string $type - * @param mixed $value + * @param string $type + * @param mixed $value * @return void */ public function set($type, $value); diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ProxyQueryInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/ProxyQueryInterface.php index 41dd3f0f5d9..ef67d7481ca 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/ProxyQueryInterface.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/ProxyQueryInterface.php @@ -2,18 +2,34 @@ namespace Oro\Bundle\GridBundle\Datagrid; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface as BaseProxyQueryInterface; - /** * Interface used by the Datagrid to build the query */ -interface ProxyQueryInterface extends BaseProxyQueryInterface +interface ProxyQueryInterface { + /** + * Execute query + * + * @param array $params + * @param null $hydrationMode + * @return mixed + */ + public function execute(array $params = array(), $hydrationMode = null); + + /** + * Proxy for query methods + * + * @param string $name + * @param array $args + * @return mixed + */ + public function __call($name, $args); + /** * Adds sorting order * - * @param array $parentAssociationMappings - * @param array $fieldMapping + * @param array $parentAssociationMappings + * @param array $fieldMapping * @param string $direction */ public function addSortOrder(array $parentAssociationMappings, array $fieldMapping, $direction = null); @@ -28,8 +44,8 @@ public function getTotalCount(); /** * Adds select part to internal whitelist * - * @param string $select - * @param bool $addToWhitelist + * @param string $select + * @param bool $addToWhitelist * @return ProxyQueryInterface */ public function addSelect($select = null, $addToWhitelist = false); @@ -37,8 +53,8 @@ public function addSelect($select = null, $addToWhitelist = false); /** * Set query parameter * - * @param string $name - * @param mixed $value + * @param string $name + * @param mixed $value * @return ProxyQueryInterface */ public function setParameter($name, $value); @@ -53,9 +69,92 @@ public function getRootAlias(); /** * Sets a query hint * - * @param string $name - * @param mixed $value + * @param string $name + * @param mixed $value * @return ProxyQueryInterface */ public function setQueryHint($name, $value); + + /** + * Set sort by field + * + * @param array $parentAssociationMappings + * @param array $fieldMapping + * @return ProxyQueryInterface + */ + public function setSortBy($parentAssociationMappings, $fieldMapping); + + /** + * Get sort by field + * + * @return mixed + */ + public function getSortBy(); + + /** + * Set sort order + * + * @param mixed $sortOrder + * @return ProxyQueryInterface + */ + public function setSortOrder($sortOrder); + + /** + * Get sort order + * + * @return mixed + */ + public function getSortOrder(); + + /** + * Get single scalar result + * + * @return mixed + */ + public function getSingleScalarResult(); + + /** + * Set first result + * + * @param int $firstResult + * @return ProxyQueryInterface + */ + public function setFirstResult($firstResult); + + /** + * Get first result + * + * @return mixed + */ + public function getFirstResult(); + + /** + * Set max records + * + * @param int $maxResults + * @return ProxyQueryInterface + */ + public function setMaxResults($maxResults); + + /** + * Get max records + * + * @return mixed + */ + public function getMaxResults(); + + /** + * Get unique parameter ID + * + * @return mixed + */ + public function getUniqueParameterId(); + + /** + * Join entity + * + * @param array $associationMappings + * @return mixed + */ + public function entityJoin(array $associationMappings); } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php b/src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php new file mode 100644 index 00000000000..d7f531c955b --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Datagrid/QueryConverter/YamlConverter.php @@ -0,0 +1,125 @@ +processConfiguration(new ReportConfiguration(), array('report' => $value)); + $qb = $em->createQueryBuilder(); + + if (!isset($value['from'])) { + throw new \RuntimeException('Missing mandatory "from" section'); + } + + foreach ((array) $value['from'] as $from) { + $qb->from($from['table'], $from['alias']); + } + + if (isset($value['select'])) { + $qb->select($value['select']); + } + + if (isset($value['distinct'])) { + $qb->distinct((bool) $value['distinct']); + } + + if (isset($value['groupBy'])) { + $qb->groupBy($value['groupBy']); + } + + if (isset($value['having'])) { + $qb->having($value['having']); + } + + $this->addJoin($qb, $value); + $this->addWhere($qb, $value); + $this->addOrder($qb, $value); + + return $qb; + } + + /** + * {@inheritdoc} + */ + public function dump(QueryBuilder $input) + { + return ''; + } + + /** + * @param QueryBuilder $qb + * @param array $value + */ + protected function addJoin(QueryBuilder $qb, $value) + { + if (isset($value['join'])) { + if (isset($value['join']['inner'])) { + foreach ((array) $value['join']['inner'] as $join) { + $qb->innerJoin($join['join'], $join['alias']); + } + } + + if (isset($value['join']['left'])) { + foreach ((array) $value['join']['left'] as $join) { + $qb->leftJoin($join['join'], $join['alias']); + } + } + } + } + + /** + * @param QueryBuilder $qb + * @param array $value + */ + protected function addWhere(QueryBuilder $qb, $value) + { + if (isset($value['where'])) { + if (isset($value['where']['and'])) { + foreach ((array) $value['where']['and'] as $where) { + $qb->andWhere($where); + } + } + + if (isset($value['where']['or'])) { + foreach ((array) $value['where']['or'] as $where) { + $qb->orWhere($where); + } + } + } + } + + /** + * @param QueryBuilder $qb + * @param array $value + */ + protected function addOrder(QueryBuilder $qb, $value) + { + if (isset($value['orderBy'])) { + $qb->resetDQLPart('orderBy'); + + foreach ((array) $value['orderBy'] as $order) { + $qb->addOrderBy($order['column'], $order['dir']); + } + } + } +} diff --git a/src/Oro/Bundle/GridBundle/Datagrid/QueryConverterInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/QueryConverterInterface.php new file mode 100644 index 00000000000..f1979acdebd --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Datagrid/QueryConverterInterface.php @@ -0,0 +1,27 @@ +getRootParameterValue(); + return isset($rootParameter[$type]) ? $rootParameter[$type] : $default; } /** - * @param string $type - * @param mixed $value + * @param string $type + * @param mixed $value * @return void */ public function set($type, $value) @@ -90,6 +91,7 @@ protected function getRequest() if (!$this->request) { $this->request = clone $this->container->get('request'); } + return $this->request; } @@ -124,6 +126,7 @@ public function getLocale() public function getScope() { $rootValue = $this->getRootParameterValue(); + return isset($rootValue[self::SCOPE_PARAMETER]) ? $rootValue[self::SCOPE_PARAMETER] : null; } } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ResultRecord.php b/src/Oro/Bundle/GridBundle/Datagrid/ResultRecord.php index 7c408e1b768..0659f0bd74a 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/ResultRecord.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/ResultRecord.php @@ -35,7 +35,7 @@ public function __construct($data) /** * Get value of property by name * - * @param string $name + * @param string $name * @return mixed * @throws \LogicException When cannot get value */ @@ -70,7 +70,7 @@ public function getValue($name) * Camelize a string * * @static - * @param string $property + * @param string $property * @return string */ private static function camelize($property) @@ -81,4 +81,18 @@ private static function camelize($property) $property ); } + + /** + * Gets root entity from result record + * + * @return object|null + */ + public function getRootEntity() + { + if (array_key_exists(0, $this->valueContainers) && is_object($this->valueContainers[0])) { + return $this->valueContainers[0]; + } + + return null; + } } diff --git a/src/Oro/Bundle/GridBundle/Datagrid/ResultRecordInterface.php b/src/Oro/Bundle/GridBundle/Datagrid/ResultRecordInterface.php index 260fe28833a..44203c17278 100644 --- a/src/Oro/Bundle/GridBundle/Datagrid/ResultRecordInterface.php +++ b/src/Oro/Bundle/GridBundle/Datagrid/ResultRecordInterface.php @@ -7,9 +7,16 @@ interface ResultRecordInterface /** * Get value of record property by name * - * @param string $name + * @param string $name * @return mixed * @throws \LogicException When cannot get value */ public function getValue($name); + + /** + * Get root entity of current result record + * + * @return object|null + */ + public function getRootEntity(); } diff --git a/src/Oro/Bundle/GridBundle/DependencyInjection/Compiler/AbstractDatagridManagerCompilerPass.php b/src/Oro/Bundle/GridBundle/DependencyInjection/Compiler/AbstractDatagridManagerCompilerPass.php index 5bf9c97ea31..d72bc710eb7 100644 --- a/src/Oro/Bundle/GridBundle/DependencyInjection/Compiler/AbstractDatagridManagerCompilerPass.php +++ b/src/Oro/Bundle/GridBundle/DependencyInjection/Compiler/AbstractDatagridManagerCompilerPass.php @@ -6,6 +6,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException; +use Symfony\Component\DependencyInjection\Definition; abstract class AbstractDatagridManagerCompilerPass implements CompilerPassInterface { diff --git a/src/Oro/Bundle/GridBundle/DependencyInjection/Compiler/AddDependencyCallsCompilerPass.php b/src/Oro/Bundle/GridBundle/DependencyInjection/Compiler/AddDependencyCallsCompilerPass.php index bee9166b5b6..7f6b702065a 100644 --- a/src/Oro/Bundle/GridBundle/DependencyInjection/Compiler/AddDependencyCallsCompilerPass.php +++ b/src/Oro/Bundle/GridBundle/DependencyInjection/Compiler/AddDependencyCallsCompilerPass.php @@ -14,6 +14,8 @@ class AddDependencyCallsCompilerPass extends AbstractDatagridManagerCompilerPass { + const REGISTRY_SERVICE = 'oro_grid.datagrid_manager.registry'; + const QUERY_FACTORY_ATTRIBUTE = 'query_factory'; const ROUTE_GENERATOR_ATTRIBUTE = 'route_generator'; const DATAGRID_BUILDER_ATTRIBUTE = 'datagrid_builder'; @@ -23,13 +25,29 @@ class AddDependencyCallsCompilerPass extends AbstractDatagridManagerCompilerPass const TRANSLATION_DOMAIN_ATTRIBUTE = 'translation_domain'; const VALIDATOR_ATTRIBUTE = 'validator'; const ROUTER_ATTRIBUTE = 'router'; + const ENTITY_MANAGER_ATTRIBUTE = 'entity_manager'; + const ENTITY_NAME_ATTRIBUTE = 'entity_name'; const ENTITY_HINT_ATTRIBUTE = 'entity_hint'; + const QUERY_ENTITY_ALIAS_ATTRIBUTE = 'query_entity_alias'; + const IDENTIFIER_FIELD = 'identifier_field'; + + /** + * @var Definition + */ + protected $registryDefinition; + + /** + * @var array + */ + protected $datagridNames = array(); /** * {@inheritDoc} */ public function processDatagrid() { + $this->registryDefinition = $this->container->getDefinition(self::REGISTRY_SERVICE); + $this->applyConfigurationFromAttributes(); $this->applyDefaults(); } @@ -39,10 +57,22 @@ public function processDatagrid() */ protected function applyConfigurationFromAttributes() { - $this->definition->addMethodCall('setName', array($this->getMandatoryAttribute('datagrid_name'))); + $datagridName = $this->getMandatoryAttribute('datagrid_name'); + if (in_array($datagridName, $this->datagridNames)) { + throw new InvalidDefinitionException( + sprintf('Datagrid manager with name "%s" already exists', $datagridName) + ); + } + $this->datagridNames[] = $datagridName; + + $this->definition->addMethodCall('setName', array($datagridName)); + + // add service to datagrid manager registry + $this->registryDefinition->addMethodCall('addDatagridManagerService', array($datagridName, $this->serviceId)); // add services $serviceKeys = array( + self::ENTITY_MANAGER_ATTRIBUTE, self::QUERY_FACTORY_ATTRIBUTE, self::ROUTE_GENERATOR_ATTRIBUTE, self::DATAGRID_BUILDER_ATTRIBUTE, @@ -64,8 +94,11 @@ protected function applyConfigurationFromAttributes() // add other attributes $attributeKeys = array( + self::ENTITY_NAME_ATTRIBUTE, + self::QUERY_ENTITY_ALIAS_ATTRIBUTE, self::ENTITY_HINT_ATTRIBUTE, - self::TRANSLATION_DOMAIN_ATTRIBUTE + self::TRANSLATION_DOMAIN_ATTRIBUTE, + self::IDENTIFIER_FIELD ); foreach ($attributeKeys as $key) { @@ -90,6 +123,8 @@ protected function applyDefaults() self::QUERY_FACTORY_ATTRIBUTE => array($this, 'getDefaultQueryFactoryServiceId'), self::ROUTE_GENERATOR_ATTRIBUTE => array($this, 'getDefaultRouteGeneratorServiceId'), self::PARAMETERS_ATTRIBUTE => array($this, 'getDefaultParametersServiceId'), + self::PARAMETERS_ATTRIBUTE => array($this, 'getDefaultParametersServiceId'), + self::ENTITY_MANAGER_ATTRIBUTE => array($this, 'getDefaultEntityManagerServiceId'), self::DATAGRID_BUILDER_ATTRIBUTE => 'oro_grid.builder.datagrid', self::LIST_BUILDER_ATTRIBUTE => 'oro_grid.builder.list', self::TRANSLATOR_ATTRIBUTE => 'translator', @@ -104,7 +139,9 @@ protected function applyDefaults() if (is_callable($serviceId)) { $serviceId = call_user_func($serviceId); } - $this->definition->addMethodCall($method, array(new Reference($serviceId))); + if ($serviceId) { + $this->definition->addMethodCall($method, array(new Reference($serviceId))); + } } } @@ -143,16 +180,16 @@ protected function getDefaultQueryFactoryServiceId() protected function createDefaultQueryFactoryDefinition() { $arguments = array(); - if ($this->hasAttribute('entity_name')) { + if ($this->hasAttribute(self::ENTITY_NAME_ATTRIBUTE)) { $queryFactoryClass = '%oro_grid.orm.query_factory.entity.class%'; $arguments = array( new Reference('doctrine'), - $this->getAttribute('entity_name'), + $this->getAttribute(self::ENTITY_NAME_ATTRIBUTE), ); - if ($this->hasAttribute('query_entity_alias')) { - $arguments[] = $this->getAttribute('query_entity_alias'); + if ($this->hasAttribute(self::QUERY_ENTITY_ALIAS_ATTRIBUTE)) { + $arguments[] = $this->getAttribute(self::QUERY_ENTITY_ALIAS_ATTRIBUTE); } } else { $queryFactoryClass = '%oro_grid.orm.query_factory.query.class%'; @@ -228,4 +265,40 @@ protected function createDefaultParametersDefinition() return $definition; } + + /** + * Get id of default entity manager service + * + * @return string|null + */ + protected function getDefaultEntityManagerServiceId() + { + $entityManagerServiceId = null; + if ($this->hasAttribute(self::ENTITY_NAME_ATTRIBUTE)) { + $entityManagerServiceId = sprintf('%s.entity_manager', $this->serviceId); + $this->container->setDefinition( + $entityManagerServiceId, + $this->createEntityManagerDefinition($this->createEntityManagerDefinition()) + ); + } + + return $entityManagerServiceId; + } + + /** + * Create entity manager definition based on entity_name attribute + * + * @return Definition + * @throws InvalidDefinitionException + */ + protected function createEntityManagerDefinition() + { + $definition = new Definition('%doctrine.orm.entity_manager.class%'); + $definition->setPublic(false); + $definition->setFactoryService('doctrine'); + $definition->setFactoryMethod('getManagerForClass'); + $definition->setArguments(array($this->getAttribute(self::ENTITY_NAME_ATTRIBUTE))); + + return $definition; + } } diff --git a/src/Oro/Bundle/GridBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/GridBundle/DependencyInjection/Configuration.php index e0b0e417db4..df8e44d7ab3 100644 --- a/src/Oro/Bundle/GridBundle/DependencyInjection/Configuration.php +++ b/src/Oro/Bundle/GridBundle/DependencyInjection/Configuration.php @@ -8,7 +8,7 @@ class Configuration implements ConfigurationInterface { const TRANSLATION_DOMAIN_NODE = 'translation_domain'; - const DEFAULT_TRANSLATION_DOMAIN = 'datagrid'; + const DEFAULT_TRANSLATION_DOMAIN = 'messages'; /** * {@inheritDoc} diff --git a/src/Oro/Bundle/GridBundle/DependencyInjection/ReportConfiguration.php b/src/Oro/Bundle/GridBundle/DependencyInjection/ReportConfiguration.php new file mode 100644 index 00000000000..4ab8fc441c6 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/DependencyInjection/ReportConfiguration.php @@ -0,0 +1,115 @@ +root('report') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('name') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->booleanNode('distinct') + ->defaultFalse() + ->end() + ->scalarNode('select') + ->defaultValue('*') + ->end() + ->arrayNode('from') + ->requiresAtLeastOneElement() + ->prototype('array') + ->children() + ->scalarNode('table') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('alias')->end() + ->end() + ->end() + ->end() + ->arrayNode('join') + ->append($this->addJoinNode('left')) + ->append($this->addJoinNode('inner')) + ->end() + ->arrayNode('where') + ->append($this->addWhereNode('and')) + ->append($this->addWhereNode('or')) + ->end() + ->scalarNode('groupBy')->end() + ->scalarNode('having')->end() + ->arrayNode('orderBy') + ->prototype('array') + ->children() + ->scalarNode('column') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('dir') + ->defaultValue('asc') + ->end() + ->end() + ->end() + ->end() + ->end(); + + return $builder; + } + + /** + * + * @param string $name Join type ('left', 'inner') + * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition + */ + protected function addJoinNode($name) + { + if (!in_array($name, array('left', 'inner'))) { + throw new InvalidConfigurationException(sprintf('Invalid join type "%s"', $name)); + } + + $builder = new TreeBuilder(); + + return $builder->root($name) + ->requiresAtLeastOneElement() + ->prototype('array') + ->children() + ->scalarNode('join') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('alias')->end() + ->end() + ->end(); + } + + /** + * + * @param string $name Where type ('and', 'or') + * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition + */ + protected function addWhereNode($name) + { + if (!in_array($name, array('and', 'or'))) { + throw new InvalidConfigurationException(sprintf('Invalid where type "%s"', $name)); + } + + $builder = new TreeBuilder(); + + return $builder->root($name) + ->requiresAtLeastOneElement() + ->prototype('scalar') + ->end(); + } +} diff --git a/src/Oro/Bundle/GridBundle/Field/FieldDescription.php b/src/Oro/Bundle/GridBundle/Field/FieldDescription.php index 5ac826dd3bb..092233b71e5 100644 --- a/src/Oro/Bundle/GridBundle/Field/FieldDescription.php +++ b/src/Oro/Bundle/GridBundle/Field/FieldDescription.php @@ -138,6 +138,8 @@ public function setOptions(array $options) // set the field_name if provided if (isset($options['field_name'])) { $this->setFieldName($options['field_name']); + } else { + $options['field_name'] = $this->getFieldName(); } // remove property value @@ -147,31 +149,43 @@ public function setOptions(array $options) } // set field_mapping - if (isset($options['field_mapping'])) { - $this->setFieldMapping($options['field_mapping']); - } else { - $fieldMapping = array( - 'fieldName' => $this->getFieldName(), - ); - if (isset($options['entity_alias'])) { - $fieldMapping['entityAlias'] = $options['entity_alias']; - } - if (isset($options['expression'])) { - $fieldMapping['fieldExpression'] = $options['expression']; - } - if (isset($options['filter_by_where'])) { - $fieldMapping['filterByWhere'] = $options['filter_by_where']; - } - if (isset($options['filter_by_having'])) { - $fieldMapping['filterByHaving'] = $options['filter_by_having']; - } - $this->setFieldMapping($fieldMapping); - $options['field_mapping'] = $fieldMapping; - } + $options['field_mapping'] = $this->createFieldMapping($options); + $this->setFieldMapping($options['field_mapping']); $this->options = $options; } + /** + * Creates field mapping options + * + * @param array $options + * @return array + */ + protected function createFieldMapping(array $options) + { + $fieldMapping = array( + 'fieldName' => $this->getFieldName(), + ); + if (isset($options['entity_alias'])) { + $fieldMapping['entityAlias'] = $options['entity_alias']; + } + if (isset($options['expression'])) { + $fieldMapping['fieldExpression'] = $options['expression']; + } elseif (isset($options['entity_alias'])) { + $fieldMapping['fieldExpression'] = $options['entity_alias'] . '.' . $this->getFieldName(); + } + if (isset($options['filter_by_where'])) { + $fieldMapping['filterByWhere'] = $options['filter_by_where']; + } + if (isset($options['filter_by_having'])) { + $fieldMapping['filterByHaving'] = $options['filter_by_having']; + } + if (isset($options['field_mapping'])) { + $fieldMapping = array_merge($fieldMapping, $options['field_mapping']); + } + return $fieldMapping; + } + /** * {@inheritdoc} */ diff --git a/src/Oro/Bundle/GridBundle/Field/FieldDescriptionInterface.php b/src/Oro/Bundle/GridBundle/Field/FieldDescriptionInterface.php index 1df25d433b0..dc6c41fb7a8 100644 --- a/src/Oro/Bundle/GridBundle/Field/FieldDescriptionInterface.php +++ b/src/Oro/Bundle/GridBundle/Field/FieldDescriptionInterface.php @@ -9,14 +9,14 @@ interface FieldDescriptionInterface /** * Available field types */ - const TYPE_DATE = 'date'; - const TYPE_DATETIME = 'datetime'; - const TYPE_DECIMAL = 'decimal'; - const TYPE_INTEGER = 'integer'; - const TYPE_OPTIONS = 'options'; - const TYPE_TEXT = 'text'; - const TYPE_HTML = 'html'; - const TYPE_BOOLEAN = 'boolean'; + const TYPE_DATE = 'date'; + const TYPE_DATETIME = 'datetime'; + const TYPE_DECIMAL = 'decimal'; + const TYPE_INTEGER = 'integer'; + const TYPE_OPTIONS = 'options'; + const TYPE_TEXT = 'text'; + const TYPE_HTML = 'html'; + const TYPE_BOOLEAN = 'boolean'; /** * set the field name diff --git a/src/Oro/Bundle/GridBundle/Filter/FilterFactoryInterface.php b/src/Oro/Bundle/GridBundle/Filter/FilterFactoryInterface.php index 02075b8ae6e..5f2fa4b7334 100644 --- a/src/Oro/Bundle/GridBundle/Filter/FilterFactoryInterface.php +++ b/src/Oro/Bundle/GridBundle/Filter/FilterFactoryInterface.php @@ -2,8 +2,13 @@ namespace Oro\Bundle\GridBundle\Filter; -use Sonata\AdminBundle\Filter\FilterFactoryInterface as BaseFilterFactoryInterface; - -interface FilterFactoryInterface extends BaseFilterFactoryInterface +interface FilterFactoryInterface { + /** + * @param string $name + * @param string $type + * @param array $options + * @return FilterInterface + */ + public function create($name, $type, array $options = array()); } diff --git a/src/Oro/Bundle/GridBundle/Filter/FilterInterface.php b/src/Oro/Bundle/GridBundle/Filter/FilterInterface.php index d9eae994fda..587a21ad8b6 100644 --- a/src/Oro/Bundle/GridBundle/Filter/FilterInterface.php +++ b/src/Oro/Bundle/GridBundle/Filter/FilterInterface.php @@ -2,9 +2,9 @@ namespace Oro\Bundle\GridBundle\Filter; -use Sonata\AdminBundle\Filter\FilterInterface as BaseFilterInterface; +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; -interface FilterInterface extends BaseFilterInterface +interface FilterInterface { /** * Allowed filter types @@ -16,6 +16,7 @@ interface FilterInterface extends BaseFilterInterface const TYPE_CHOICE = 'oro_grid_orm_choice'; const TYPE_BOOLEAN = 'oro_grid_orm_boolean'; const TYPE_ENTITY = 'oro_grid_orm_entity'; + const TYPE_SELECT_ROW = 'oro_grid_orm_select_row'; const TYPE_FLEXIBLE_DATE = 'oro_grid_orm_flexible_date_range'; const TYPE_FLEXIBLE_DATETIME = 'oro_grid_orm_flexible_datetime_range'; const TYPE_FLEXIBLE_NUMBER = 'oro_grid_orm_flexible_number'; @@ -33,4 +34,105 @@ public function isActive(); * @return boolean */ public function isNullable(); + + /** + * Apply the filter to the QueryBuilder instance + * + * @param ProxyQueryInterface $queryBuilder + * @param string $alias + * @param string $field + * @param string $value + * + * @return void + */ + public function filter(ProxyQueryInterface $queryBuilder, $alias, $field, $value); + + /** + * @param mixed $query + * @param mixed $value + */ + public function apply($query, $value); + + /** + * Returns the filter name + * + * @return string + */ + public function getName(); + + /** + * Returns the label name + * + * @return string + */ + public function getLabel(); + + /** + * @param string $label + */ + public function setLabel($label); + + /** + * @return array + */ + public function getDefaultOptions(); + + /** + * @param string $name + * @param null $default + * + * @return mixed + */ + public function getOption($name, $default = null); + + /** + * @param string $name + * @param mixed $value + */ + public function setOption($name, $value); + + /** + * @param string $name + * @param array $options + * + * @return void + */ + public function initialize($name, array $options = array()); + + /** + * @return string + */ + public function getFieldName(); + + /** + * @return array of mappings + */ + public function getParentAssociationMappings(); + + /** + * @return array field mapping + */ + public function getFieldMapping(); + + /** + * @return array association mapping + */ + public function getAssociationMapping(); + + /** + * @return array + */ + public function getFieldOptions(); + + /** + * @return string + */ + public function getFieldType(); + + /** + * Returns the main widget used to render the filter + * + * @return array + */ + public function getRenderSettings(); } diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractDateFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractDateFilter.php index 07d2418a7bd..449b89ad344 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractDateFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractDateFilter.php @@ -3,7 +3,8 @@ namespace Oro\Bundle\GridBundle\Filter\ORM; use Doctrine\DBAL\Query\QueryBuilder; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; + +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\FilterBundle\Form\Type\Filter\DateRangeFilterType; abstract class AbstractDateFilter extends AbstractFilter @@ -33,27 +34,16 @@ public function filter(ProxyQueryInterface $queryBuilder, $alias, $field, $data) $startDateParameterName = $this->getNewParameterName($queryBuilder); $endDateParameterName = $this->getNewParameterName($queryBuilder); - if ($type == DateRangeFilterType::TYPE_NOT_BETWEEN) { - $this->applyFilterNotBetween( - $queryBuilder, - $dateStartValue, - $dateEndValue, - $startDateParameterName, - $endDateParameterName, - $alias, - $field - ); - } else { - $this->applyFilterBetween( - $queryBuilder, - $dateStartValue, - $dateEndValue, - $startDateParameterName, - $endDateParameterName, - $alias, - $field - ); - } + $this->applyDependingOnType( + $type, + $queryBuilder, + $dateStartValue, + $dateEndValue, + $startDateParameterName, + $endDateParameterName, + $alias, + $field + ); /** @var $queryBuilder QueryBuilder */ if ($dateStartValue) { @@ -86,16 +76,32 @@ public function parseData($data) $data['value']['end'] = null; } - $data['type'] = isset($data['type']) ? $data['type'] : null; + if (!isset($data['type'])) { + $data['type'] = null; + } - if ($data['type'] != DateRangeFilterType::TYPE_NOT_BETWEEN) { + if (!in_array( + $data['type'], + array( + DateRangeFilterType::TYPE_BETWEEN, + DateRangeFilterType::TYPE_NOT_BETWEEN, + DateRangeFilterType::TYPE_MORE_THAN, + DateRangeFilterType::TYPE_LESS_THAN + ) + )) { $data['type'] = DateRangeFilterType::TYPE_BETWEEN; } + if ($data['type'] == DateRangeFilterType::TYPE_MORE_THAN + || $data['type'] == DateRangeFilterType::TYPE_LESS_THAN + ) { + $data['value']['end'] = null; + } + return array( 'date_start' => $data['value']['start'], - 'date_end' => $data['value']['end'], - 'type' => $data['type'] + 'date_end' => $data['value']['end'], + 'type' => $data['type'] ); } @@ -161,6 +167,32 @@ protected function applyFilterBetween( } } + /** + * Apply expression using one condition (less or more) + * + * @param ProxyQueryInterface $queryBuilder + * @param $dateValue + * @param $dateParameterName + * @param string $alias + * @param string $field + * @param bool $isLess less/more mode, true if 'less than', false if 'more than' + */ + protected function applyFilterLessMore( + $queryBuilder, + $dateValue, + $dateParameterName, + $alias, + $field, + $isLess + ) { + if ($dateValue) { + $this->applyFilterToClause( + $queryBuilder, + $this->createCompareFieldExpression($field, $alias, $isLess ? '<' : '>', $dateParameterName) + ); + } + } + /** * Apply expression using "not between" filtering * @@ -193,4 +225,73 @@ protected function applyFilterNotBetween( $this->applyFilterToClause($queryBuilder, $orExpression); } + + /** + * Applies filter depending on it's type + * + * @param int $type + * @param ProxyQueryInterface $queryBuilder + * @param string $dateStartValue + * @param string $dateEndValue + * @param string $startDateParameterName + * @param string $endDateParameterName + * @param string $alias + * @param string $field + */ + protected function applyDependingOnType( + $type, + $queryBuilder, + $dateStartValue, + $dateEndValue, + $startDateParameterName, + $endDateParameterName, + $alias, + $field + ) { + switch ($type) { + case DateRangeFilterType::TYPE_MORE_THAN: + $this->applyFilterLessMore( + $queryBuilder, + $dateStartValue, + $startDateParameterName, + $alias, + $field, + false + ); + break; + case DateRangeFilterType::TYPE_LESS_THAN: + $this->applyFilterLessMore( + $queryBuilder, + $dateStartValue, + $startDateParameterName, + $alias, + $field, + true + ); + break; + case DateRangeFilterType::TYPE_NOT_BETWEEN: + $this->applyFilterNotBetween( + $queryBuilder, + $dateStartValue, + $dateEndValue, + $startDateParameterName, + $endDateParameterName, + $alias, + $field + ); + break; + default: + case DateRangeFilterType::TYPE_BETWEEN: + $this->applyFilterBetween( + $queryBuilder, + $dateStartValue, + $dateEndValue, + $startDateParameterName, + $endDateParameterName, + $alias, + $field + ); + break; + } + } } diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractDescriptiveFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractDescriptiveFilter.php new file mode 100644 index 00000000000..48ab5e694d5 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractDescriptiveFilter.php @@ -0,0 +1,226 @@ +getOption('field_options', array()); + } + + /** + * {@inheritdoc} + */ + public function isNullable() + { + return $this->getOption('nullable', true); + } + + /** + * {@inheritdoc} + */ + public function isActive() + { + return $this->active; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return $this->name; + } + + /** + * {@inheritdoc} + */ + public function getOption($name, $default = null) + { + if (array_key_exists($name, $this->options)) { + return $this->options[$name]; + } + + return $default; + } + + /** + * {@inheritdoc} + */ + public function setOption($name, $value) + { + $this->options[$name] = $value; + } + + /** + * {@inheritdoc} + */ + public function getFieldType() + { + return $this->getOption('field_type', 'text'); + } + + /** + * {@inheritdoc} + */ + public function getLabel() + { + return $this->getOption('label'); + } + + /** + * {@inheritdoc} + */ + public function setLabel($label) + { + $this->setOption('label', $label); + } + + /** + * {@inheritdoc} + */ + public function getFieldName() + { + $fieldName = $this->getOption('field_name'); + + if (!$fieldName) { + throw new \RunTimeException( + sprintf('The option `field_name` must be set for field : `%s`', $this->getName()) + ); + } + + return $fieldName; + } + + /** + * {@inheritdoc} + */ + public function getParentAssociationMappings() + { + return $this->getOption('parent_association_mappings', array()); + } + + /** + * {@inheritdoc} + */ + public function getFieldMapping() + { + $fieldMapping = $this->getOption('field_mapping'); + + if (!$fieldMapping) { + throw new \RunTimeException( + sprintf('The option `field_mapping` must be set for field : `%s`', $this->getName()) + ); + } + + return $fieldMapping; + } + + /** + * {@inheritdoc} + */ + public function getAssociationMapping() + { + $associationMapping = $this->getOption('association_mapping'); + + if (!$associationMapping) { + throw new \RunTimeException( + sprintf('The option `association_mapping` must be set for field : `%s`', $this->getName()) + ); + } + + return $associationMapping; + } + + /** + * @param array $options + * + * @return void + */ + public function setOptions(array $options) + { + $this->options = array_merge($this->getDefaultOptions(), $options); + } + + /** + * @return array + */ + public function getOptions() + { + return $this->options; + } + + /** + * @param mixed $value + * + * @return void + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * @param string $condition + * + * @return void + */ + public function setCondition($condition) + { + $this->condition = $condition; + } + + /** + * @return string + */ + public function getCondition() + { + return $this->condition; + } +} diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractFilter.php index 8ee35c43abf..d42c9ec11e5 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/AbstractFilter.php @@ -7,13 +7,11 @@ use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\ORM\Query\Expr; -use Sonata\DoctrineORMAdminBundle\Filter\Filter as AbstractORMFilter; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; - use Oro\Bundle\FilterBundle\Form\Type\Filter\FilterType; use Oro\Bundle\GridBundle\Filter\FilterInterface; +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; -abstract class AbstractFilter extends AbstractORMFilter implements FilterInterface +abstract class AbstractFilter extends AbstractDescriptiveFilter implements FilterInterface { /** * @var Expr @@ -33,6 +31,15 @@ public function __construct(TranslatorInterface $translator) $this->translator = $translator; } + /** + * {@inheritdoc} + */ + public function initialize($name, array $options = array()) + { + $this->name = $name; + $this->setOptions($options); + } + /** * {@inheritdoc} */ @@ -47,27 +54,6 @@ protected function association(ProxyQueryInterface $queryBuilder, $value) return array($alias, $this->getFieldName()); } - /** - * Apply filter expression to having or where clause depending on configuration - * - * @param ProxyQueryInterface $queryBuilder - * @param mixed $expression - */ - protected function applyFilterToClause(ProxyQueryInterface $queryBuilder, $expression) - { - if ($this->isApplyFilterToHavingClause()) { - $this->applyHaving( - $queryBuilder, - $expression - ); - } else { - $this->applyWhere( - $queryBuilder, - $expression - ); - } - } - /** * Checks if filter expression should be applied to having clause, if not where clause should be applied * @@ -147,6 +133,36 @@ protected function getExpressionFactory() return $this->expressionFactory; } + /** + * {@inheritdoc} + */ + public function apply($queryBuilder, $value) + { + $this->value = $value; + if (is_array($value) && array_key_exists("value", $value)) { + list($alias, $field) = $this->association($queryBuilder, $value); + + $this->filter($queryBuilder, $alias, $field, $value); + } + } + + /** + * @param ProxyQueryInterface $queryBuilder + * @param mixed $parameter + */ + protected function applyWhere(ProxyQueryInterface $queryBuilder, $parameter) + { + /** @var QueryBuilder $queryBuilder */ + if ($this->getCondition() == self::CONDITION_OR) { + $queryBuilder->orWhere($parameter); + } else { + $queryBuilder->andWhere($parameter); + } + + // filter is active since it's added to the queryBuilder + $this->active = true; + } + /** * Apply expression to having clause * @@ -166,6 +182,21 @@ protected function applyHaving(ProxyQueryInterface $queryBuilder, $parameter) $this->active = true; } + /** + * Apply filter expression to having or where clause depending on configuration + * + * @param ProxyQueryInterface $queryBuilder + * @param mixed $expression + */ + protected function applyFilterToClause(ProxyQueryInterface $queryBuilder, $expression) + { + if ($this->isApplyFilterToHavingClause()) { + $this->applyHaving($queryBuilder, $expression); + } else { + $this->applyWhere($queryBuilder, $expression); + } + } + /** * {@inheritdoc} */ @@ -195,18 +226,14 @@ public function getRenderSettings() } /** - * {@inheritdoc} - */ - public function getFieldOptions() - { - return $this->getOption('field_options', array()); - } - - /** - * {@inheritdoc} + * @param ProxyQueryInterface $proxyQuery + * + * @return string */ - public function isNullable() + protected function getNewParameterName(ProxyQueryInterface $proxyQuery) { - return $this->getOption('nullable', true); + // dots are not accepted in a DQL identifier so replace them + // by underscores. + return str_replace('.', '_', $this->getName()) . '_' . $proxyQuery->getUniqueParameterId(); } } diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/BooleanFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/BooleanFilter.php index ef41990efb3..0aded88474a 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/BooleanFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/BooleanFilter.php @@ -3,7 +3,8 @@ namespace Oro\Bundle\GridBundle\Filter\ORM; use Doctrine\DBAL\Query\QueryBuilder; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; + +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\FilterBundle\Form\Type\Filter\BooleanFilterType; class BooleanFilter extends AbstractFilter diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/ChoiceFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/ChoiceFilter.php index c5e9c11596e..c61d2fa9e93 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/ChoiceFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/ChoiceFilter.php @@ -5,7 +5,7 @@ use Doctrine\DBAL\Query\QueryBuilder; use Doctrine\Common\Collections\Collection; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\FilterBundle\Form\Type\Filter\ChoiceFilterType; class ChoiceFilter extends AbstractFilter diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/AbstractFlexibleDateFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/AbstractFlexibleDateFilter.php index d93afcd806e..6cb4fa80601 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/AbstractFlexibleDateFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/AbstractFlexibleDateFilter.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\GridBundle\Filter\ORM\Flexible; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\GridBundle\Filter\ORM\AbstractDateFilter; use Oro\Bundle\FilterBundle\Form\Type\Filter\DateRangeFilterType; diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/AbstractFlexibleFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/AbstractFlexibleFilter.php index 08eca48269a..3e86d95ce3b 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/AbstractFlexibleFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/AbstractFlexibleFilter.php @@ -155,14 +155,6 @@ public function getName() return $this->parentFilter->getName(); } - /** - * {@inheritdoc} - */ - public function getFormName() - { - return $this->parentFilter->getFormName(); - } - /** * {@inheritdoc} */ diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleBooleanFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleBooleanFilter.php index 761b0fa1461..850e9bbc11f 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleBooleanFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleBooleanFilter.php @@ -1,7 +1,9 @@ parentFilter->parseData($data); if (!$data) { @@ -102,31 +104,6 @@ protected function getClassName() return $valueMetadata->getAssociationTargetClass($this->getOption('backend_type')); } - /** - * {@inheritdoc} - */ - protected function applyFlexibleFilter(ProxyQueryInterface $proxyQuery, $field, $value, $operator) - { - $attribute = $this->getAttribute($field); - /** @var $qb FlexibleQueryBuilder */ - $qb = $proxyQuery->getQueryBuilder(); - - // inner join to value - $joinAlias = 'filter'.$field; - $condition = $qb->prepareAttributeJoinCondition($attribute, $joinAlias); - $rootAlias = $qb->getRootAliases(); - $qb->innerJoin($rootAlias[0] .'.'. $attribute->getBackendStorage(), $joinAlias, 'WITH', $condition); - - // then join to linked entity with filter on id - $joinAliasEntity = 'filterentity'.$field; - $backendField = sprintf('%s.id', $joinAliasEntity); - $condition = $qb->prepareCriteriaCondition($backendField, $operator, $value); - $qb->innerJoin($joinAlias .'.'. $attribute->getBackendType(), $joinAliasEntity, 'WITH', $condition); - - // filter is active since it's applied to the flexible repository - $this->active = true; - } - /** * Extract collection ids * diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleNumberFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleNumberFilter.php index 8c1ae674569..6a9b6db103c 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleNumberFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleNumberFilter.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\GridBundle\Filter\ORM\Flexible; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\GridBundle\Filter\ORM\NumberFilter; class FlexibleNumberFilter extends AbstractFlexibleFilter diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleOptionsFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleOptionsFilter.php index 719d8045e1a..66293a8d0e4 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleOptionsFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleOptionsFilter.php @@ -3,7 +3,8 @@ namespace Oro\Bundle\GridBundle\Filter\ORM\Flexible; use Doctrine\Common\Persistence\ObjectRepository; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; + +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\FilterBundle\Form\Type\Filter\ChoiceFilterType; use Oro\Bundle\FlexibleEntityBundle\Entity\Attribute; use Oro\Bundle\FlexibleEntityBundle\Entity\AttributeOption; diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleStringFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleStringFilter.php index 0be300a887e..4981e5c3422 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleStringFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/Flexible/FlexibleStringFilter.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\GridBundle\Filter\ORM\Flexible; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\GridBundle\Filter\ORM\StringFilter; class FlexibleStringFilter extends AbstractFlexibleFilter diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/NumberFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/NumberFilter.php index 98e33894982..adf6fe54121 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/NumberFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/NumberFilter.php @@ -3,7 +3,8 @@ namespace Oro\Bundle\GridBundle\Filter\ORM; use Doctrine\ORM\QueryBuilder; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; + +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\FilterBundle\Form\Type\Filter\NumberFilterType; use Oro\Bundle\GridBundle\Field\FieldDescriptionInterface; diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/SelectRowFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/SelectRowFilter.php new file mode 100644 index 00000000000..11b209ba288 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/SelectRowFilter.php @@ -0,0 +1,120 @@ +parseData($data); + + if ($data['value'] === null) { + return; + } + + $expression = false; + switch (true) { + case $data['in'] === null && $data['out'] !== null && empty($data['out']): + $expression = $this->getExpressionFactory()->eq(1, 1); + break; + case $data['out'] === null && $data['in'] !== null && empty($data['in']): + $expression = $this->getExpressionFactory()->eq(0, 1); + break; + case !empty($data['in']): + $expression = $this->getExpressionFactory()->in( + $this->createFieldExpression($field, $alias), + $data['in'] + ); + break; + case !empty($data['out']): + $expression = $this->getExpressionFactory()->notIn( + $this->createFieldExpression($field, $alias), + $data['out'] + ); + break; + } + + if ($expression) { + $this->applyFilterToClause($queryBuilder, $expression); + } + } + + /** + * Transform submitted filter data to correct format + * + * @param array $data + * @return array + */ + protected function parseData($data) + { + if (empty($data['value']) + || !in_array($data['value'], array(self::NOT_SELECTED_VALUE, self::SELECTED_VALUE), true)) { + $data['value'] = null; + } + + if (isset($data['in']) && !is_array($data['in'])) { + $data['in'] = explode(',', $data['in']); + } + if (isset($data['out']) && !is_array($data['out'])) { + $data['out'] = explode(',', $data['out']); + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function getDefaultOptions() + { + return array( + 'form_type' => SelectRowFilterType::NAME + ); + } + + /** + * {@inheritdoc} + */ + public function getRenderSettings() + { + list($formType, $formOptions) = parent::getRenderSettings(); + + $choices = $this->getOption('choices'); + if ($choices) { + $formOptions['field_options']['choices'] = $choices; + } else { + $formOptions['field_options']['choices'] = array( + self::NOT_SELECTED_VALUE => 'Not selected', + self::SELECTED_VALUE => 'Selected' + ); + } + $formOptions['field_options']['multiple'] = false; + $translationDomain = $this->getOption('translation_domain'); + if (null !== $translationDomain) { + $formOptions['translation_domain'] = $translationDomain; + } + + return array($formType, $formOptions); + } + + /** + * @TODO should be refactored to use listeners in collection + * + * @return bool + */ + public function needCollection() + { + return true; + } +} diff --git a/src/Oro/Bundle/GridBundle/Filter/ORM/StringFilter.php b/src/Oro/Bundle/GridBundle/Filter/ORM/StringFilter.php index e2c8abb6b3a..f44afaf1860 100644 --- a/src/Oro/Bundle/GridBundle/Filter/ORM/StringFilter.php +++ b/src/Oro/Bundle/GridBundle/Filter/ORM/StringFilter.php @@ -3,7 +3,8 @@ namespace Oro\Bundle\GridBundle\Filter\ORM; use Doctrine\DBAL\Query\QueryBuilder; -use Sonata\AdminBundle\Datagrid\ProxyQueryInterface; + +use Oro\Bundle\GridBundle\Datagrid\ProxyQueryInterface; use Oro\Bundle\FilterBundle\Form\Type\Filter\TextFilterType; class StringFilter extends AbstractFilter @@ -30,7 +31,7 @@ public function filter(ProxyQueryInterface $queryBuilder, $alias, $field, $data) if ('=' == $operator) { $value = $data['value']; } else { - $value = sprintf($this->getOption('format'), $data['value']); + $value = sprintf($this->getFormatByComparisonType($data['type']), $data['value']); } $queryBuilder->setParameter($parameterName, $value); } @@ -64,6 +65,8 @@ public function getOperator($type) TextFilterType::TYPE_CONTAINS => 'LIKE', TextFilterType::TYPE_NOT_CONTAINS => 'NOT LIKE', TextFilterType::TYPE_EQUAL => '=', + TextFilterType::TYPE_STARTS_WITH => 'LIKE', + TextFilterType::TYPE_ENDS_WITH => 'LIKE', ); return isset($operatorTypes[$type]) ? $operatorTypes[$type] : 'LIKE'; @@ -79,4 +82,27 @@ public function getDefaultOptions() 'form_type' => TextFilterType::NAME ); } + + /** + * Return value format depending on comparison type + * + * @param $comparisonType + * @return string + */ + public function getFormatByComparisonType($comparisonType) + { + // for other than listed comparison types - use default format + switch ($comparisonType) { + case TextFilterType::TYPE_STARTS_WITH: + $format = '%s%%'; + break; + case TextFilterType::TYPE_ENDS_WITH: + $format = '%%%s'; + break; + default: + $format = $this->getOption('format'); + } + + return $format; + } } diff --git a/src/Oro/Bundle/GridBundle/Property/ActionConfigurationProperty.php b/src/Oro/Bundle/GridBundle/Property/ActionConfigurationProperty.php new file mode 100644 index 00000000000..7fbfa915b18 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Property/ActionConfigurationProperty.php @@ -0,0 +1,40 @@ +callback = $callback; + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return self::PROPERTY_NAME; + } + + /** + * {@inheritdoc} + */ + public function getValue(ResultRecordInterface $record) + { + $result = call_user_func($this->callback, $record); + + return is_array($result) ? $result : array(); + } +} diff --git a/src/Oro/Bundle/GridBundle/Property/UrlProperty.php b/src/Oro/Bundle/GridBundle/Property/UrlProperty.php index b4f98cfd10c..6fb56f65681 100644 --- a/src/Oro/Bundle/GridBundle/Property/UrlProperty.php +++ b/src/Oro/Bundle/GridBundle/Property/UrlProperty.php @@ -43,9 +43,16 @@ class UrlProperty extends AbstractProperty * @param string $routeName * @param array $placeholders * @param bool $isAbsolute + * @param null $anchor */ - public function __construct($name, Router $router, $routeName, array $placeholders = array(), $isAbsolute = false, $anchor = null) - { + public function __construct( + $name, + Router $router, + $routeName, + array $placeholders = array(), + $isAbsolute = false, + $anchor = null + ) { $this->name = $name; $this->router = $router; $this->routeName = $routeName; diff --git a/src/Oro/Bundle/GridBundle/Resources/config/action_types.yml b/src/Oro/Bundle/GridBundle/Resources/config/action_types.yml index 0057c09e2b5..de3687d94b8 100644 --- a/src/Oro/Bundle/GridBundle/Resources/config/action_types.yml +++ b/src/Oro/Bundle/GridBundle/Resources/config/action_types.yml @@ -1,16 +1,20 @@ parameters: - oro_grid.action.type.redirect.class: Oro\Bundle\GridBundle\Action\RedirectAction - oro_grid.action.type.delete.class: Oro\Bundle\GridBundle\Action\DeleteAction + oro_grid.action.type.redirect.class: Oro\Bundle\GridBundle\Action\RedirectAction + oro_grid.action.type.ajax.class: Oro\Bundle\GridBundle\Action\AjaxAction + oro_grid.action.type.delete.class: Oro\Bundle\GridBundle\Action\DeleteAction services: oro_grid.action.type.redirect: class: %oro_grid.action.type.redirect.class% - arguments: ["@oro_user.acl_manager"] tags: - { name: oro_grid.action.type, alias: oro_grid_action_redirect } + oro_grid.action.type.ajax: + class: %oro_grid.action.type.ajax.class% + tags: + - { name: oro_grid.action.type, alias: oro_grid_action_ajax } + oro_grid.action.type.delete: class: %oro_grid.action.type.delete.class% - arguments: ["@oro_user.acl_manager"] tags: - { name: oro_grid.action.type, alias: oro_grid_action_delete } diff --git a/src/Oro/Bundle/GridBundle/Resources/config/assets.yml b/src/Oro/Bundle/GridBundle/Resources/config/assets.yml index dce45537148..6b397e58013 100644 --- a/src/Oro/Bundle/GridBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/GridBundle/Resources/config/assets.yml @@ -10,8 +10,10 @@ js: - '@OroGridBundle/Resources/public/js/app/datagrid/action/modelaction.js' - '@OroGridBundle/Resources/public/js/app/datagrid/action/navigateaction.js' - '@OroGridBundle/Resources/public/js/app/datagrid/action/deleteaction.js' + - '@OroGridBundle/Resources/public/js/app/datagrid/action/ajaxaction.js' - '@OroGridBundle/Resources/public/js/app/datagrid/action/refreshcollectionaction.js' - '@OroGridBundle/Resources/public/js/app/datagrid/action/resetcollectionaction.js' + - '@OroGridBundle/Resources/public/js/app/datagrid/action/massaction.js' - '@OroGridBundle/Resources/public/js/app/datagrid/action/cell.js' - '@OroGridBundle/Resources/public/js/app/datagrid/action/column.js' - '@OroGridBundle/Resources/public/js/app/datagrid/actionspanel.js' @@ -26,6 +28,8 @@ js: - '@OroGridBundle/Resources/public/js/app/datagrid/cell/stringcell.js' - '@OroGridBundle/Resources/public/js/app/datagrid/cell/htmlcell.js' - '@OroGridBundle/Resources/public/js/app/datagrid/cell/selectcell.js' + - '@OroGridBundle/Resources/public/js/app/datagrid/cell/selectrowcell.js' + - '@OroGridBundle/Resources/public/js/app/datagrid/cell/selectallheader.js' - '@OroGridBundle/Resources/public/js/app/datagrid/headercell.js' - '@OroGridBundle/Resources/public/js/app/datagrid/header.js' diff --git a/src/Oro/Bundle/GridBundle/Resources/config/orm_filter_types.yml b/src/Oro/Bundle/GridBundle/Resources/config/orm_filter_types.yml index 96a55e73f18..7fc7d83333b 100644 --- a/src/Oro/Bundle/GridBundle/Resources/config/orm_filter_types.yml +++ b/src/Oro/Bundle/GridBundle/Resources/config/orm_filter_types.yml @@ -29,6 +29,12 @@ services: tags: - { name: oro_grid.filter.type, alias: oro_grid_orm_choice } + oro_grid.orm.filter.type.select_row: + class: Oro\Bundle\GridBundle\Filter\ORM\SelectRowFilter + arguments: ["@translator"] + tags: + - { name: oro_grid.filter.type, alias: oro_grid_orm_select_row } + oro_grid.orm.filter.type.boolean: class: Oro\Bundle\GridBundle\Filter\ORM\BooleanFilter arguments: ["@translator"] @@ -82,4 +88,3 @@ services: arguments: ["@oro_flexibleentity.registry", "@oro_grid.orm.filter.type.entity"] tags: - { name: oro_grid.filter.type, alias: oro_grid_orm_flexible_entity } - \ No newline at end of file diff --git a/src/Oro/Bundle/GridBundle/Resources/config/routing.yml b/src/Oro/Bundle/GridBundle/Resources/config/routing.yml new file mode 100644 index 00000000000..339e62102eb --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Resources/config/routing.yml @@ -0,0 +1,4 @@ +oro_grid_bundle: + resource: "@OroGridBundle/Controller" + type: annotation + prefix: /grid diff --git a/src/Oro/Bundle/GridBundle/Resources/config/services.yml b/src/Oro/Bundle/GridBundle/Resources/config/services.yml index 3bda80adfb9..c0ea759b607 100644 --- a/src/Oro/Bundle/GridBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/GridBundle/Resources/config/services.yml @@ -1,21 +1,27 @@ parameters: - oro_grid.datagrid.class: Oro\Bundle\GridBundle\Datagrid\Datagrid - oro_grid.datagrid.parameters.class: Oro\Bundle\GridBundle\Datagrid\RequestParameters + oro_grid.datagrid.class: Oro\Bundle\GridBundle\Datagrid\Datagrid + oro_grid.datagrid.parameters.class: Oro\Bundle\GridBundle\Datagrid\RequestParameters - oro_grid.orm.query_factory.entity.class: Oro\Bundle\GridBundle\Datagrid\ORM\QueryFactory\EntityQueryFactory - oro_grid.orm.query_factory.query.class: Oro\Bundle\GridBundle\Datagrid\ORM\QueryFactory\QueryFactory + oro_grid.orm.query_factory.entity.class: Oro\Bundle\GridBundle\Datagrid\ORM\QueryFactory\EntityQueryFactory + oro_grid.orm.query_factory.query.class: Oro\Bundle\GridBundle\Datagrid\ORM\QueryFactory\QueryFactory - oro_grid.builder.datagrid.class: Oro\Bundle\GridBundle\Builder\ORM\DatagridBuilder - oro_grid.builder.list.class: Oro\Bundle\GridBundle\Builder\ListBuilder + oro_grid.builder.datagrid.class: Oro\Bundle\GridBundle\Builder\ORM\DatagridBuilder + oro_grid.builder.list.class: Oro\Bundle\GridBundle\Builder\ListBuilder - oro_grid.filter.factory.class: Oro\Bundle\GridBundle\Filter\FilterFactory - oro_grid.sorter.factory.class: Oro\Bundle\GridBundle\Sorter\SorterFactory - oro_grid.action.factory.class: Oro\Bundle\GridBundle\Action\ActionFactory + oro_grid.filter.factory.class: Oro\Bundle\GridBundle\Filter\FilterFactory + oro_grid.sorter.factory.class: Oro\Bundle\GridBundle\Sorter\SorterFactory + oro_grid.action.factory.class: Oro\Bundle\GridBundle\Action\ActionFactory - oro_grid.route.default_generator.class: Oro\Bundle\GridBundle\Route\DefaultRouteGenerator + oro_grid.datagrid_manager.registry.class: Oro\Bundle\GridBundle\Datagrid\DatagridManagerRegistry - oro_grid.renderer.class: Oro\Bundle\GridBundle\Renderer\GridRenderer - oro_grid.twig.extension.class: Oro\Bundle\GridBundle\Twig\GridRendererExtension + oro_grid.mass_action.dispatcher.class: Oro\Bundle\GridBundle\Action\MassAction\MassActionDispatcher + oro_grid.mass_action.parameters_parser.class: Oro\Bundle\GridBundle\Action\MassAction\MassActionParametersParser + oro_grid.mass_action.handler.delete.class: Oro\Bundle\GridBundle\Action\MassAction\DeleteMassActionHandler + + oro_grid.route.default_generator.class: Oro\Bundle\GridBundle\Route\DefaultRouteGenerator + + oro_grid.renderer.class: Oro\Bundle\GridBundle\Renderer\GridRenderer + oro_grid.twig.extension.class: Oro\Bundle\GridBundle\Twig\GridRendererExtension services: oro_grid.builder.datagrid: @@ -23,6 +29,7 @@ services: arguments: - @form.factory - @event_dispatcher + - @oro_user.acl_manager - @oro_grid.filter.factory - @oro_grid.sorter.factory - @oro_grid.action.factory @@ -43,6 +50,21 @@ services: class: %oro_grid.action.factory.class% arguments: ["@service_container", ~] + oro_grid.datagrid_manager.registry: + class: %oro_grid.datagrid_manager.registry.class% + arguments: ["@service_container"] + + oro_grid.mass_action.dispatcher: + class: %oro_grid.mass_action.dispatcher.class% + arguments: ["@service_container", "@oro_grid.datagrid_manager.registry"] + + oro_grid.mass_action.parameters_parser: + class: %oro_grid.mass_action.parameters_parser.class% + + oro_grid.mass_action.handler.delete: + class: %oro_grid.mass_action.handler.delete.class% + arguments: ["@doctrine.orm.entity_manager", "@translator"] + oro_grid.renderer: class: %oro_grid.renderer.class% arguments: ["@templating.engine.php", "OroGridBundle:Datagrid:list.json.php"] diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/index.md b/src/Oro/Bundle/GridBundle/Resources/doc/index.md index c11e32a1df0..6ad2ff15387 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/index.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/index.md @@ -23,6 +23,7 @@ OroGridBundle Documentation - [Grid Rendering](./reference/backend/grid-rendering.md) - [Events](./reference/backend/events.md) - [Translations](./reference/backend/translations.md) + - [Reports](./reference/backend/reports.md) - **Frontend Architecture** - [Overview](./reference/frontend/overview.md) - [Backbone Developer Introduction](./reference/frontend/backbone-developer-introduction.md) diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/actions.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/actions.md index 2b7530b923a..64d6826d460 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/actions.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/actions.md @@ -105,3 +105,82 @@ class UserDatagridManager extends FlexibleDatagridManager // other methods } ``` + +#### Control actions on record level +To manage(show/hide) some actions by condition(dependent on row) developer should to add ActionConfigurationProperty to datagrid. +This property needs closure as required param that will return array of actions that have to be shown/hidden. +Key of this should be action name and true/false value (show/hide respectively) + +#### Example + +``` php + protected function getProperties() + { + return array( + //... Some properties ... + new ActionConfigurationProperty( + function (ResultRecordInterface $record) { + if ($record->getValue('someField') == true) { + // do not render delete action if row field someField equals true + return array('delete' => false); + } + } + ) + ); + } +``` + +### Mass actions + +#### Class Description +* ** MassAction / Ajax / AjaxMassAction ** - ajax action implementation (confirmation enabled by default) +* ** MassAction / Ajax / DeleteMassAction ** - ajax delete action implementation +* ** MassAction /Redirect / RedirectMassAction ** - redirects to route with checked rows ids (GET method) +* ** MassAction / Widget / WidgetMassAction ** - basic widget mass action implementation open specifed widget with checked rows ids (GET method) +* ** MassAction / Widget / WindowMassAction ** - open window widget with specific url and with checked rows ids (GET method) + + +#### Examples + +``` php + /** + * {@inheritDoc} + */ + protected function getMassActions() + { + $deleteMassAction = new DeleteMassAction( + array( + 'name' => 'delete', + 'acl_resource' => 'oro_user_user_delete', + 'label' => $this->translate('orocrm.contact.datagrid.delete'), + 'icon' => 'trash', + ) + ); + + $redirectMassAction = new RedirectMassAction( + array( + 'name' => 'redirect', + 'acl_resource' => 'oro_user_user_delete', + 'label' => 'Redirect', + 'route' => 'oro_user_view', + 'route_parameters' => array('id' => 1) + ) + ); + + $windowMassAction = new WindowMassAction( + array( + 'name' => 'window', + 'label' => 'Window', + 'acl_resource' => 'oro_user_user_delete', + 'route' => 'oro_user_view', + 'route_parameters' => array('id' => 1), + ) + ); + + return array($deleteMassAction, $redirectMassAction, $windowMassAction); + } +``` + +** NOTE: ** _All ajax massaction performed via OroGridBundle:MassActionController using specified handlers (ref: DeleteMassAction). + Developer should specify 'handler' options contains service id that should + handle current mass action and implements MassActionHandlerInterface_ diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/datagrid.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/datagrid.md index 7392ca1f03d..007030be196 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/datagrid.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/datagrid.md @@ -5,7 +5,6 @@ Datagrid is a main entity that contains fields, additional entities, DB query an #### Class Description -* **Sonata \ AdminBundle \ Datagrid \ DatagridInterface** - Sonata AdminBundle datagrid interface, that provides basic method signatures to work with fields, filters, pager and result. * **Datagrid \ DatagridInterface** - basic datagrid interface, that provides additional methods to work with sorters, actions, router and names. * **Datagrid \ ResultRecordInterface** - basic interface for Result Record entity; * **Datagrid \ Datagrid** - Datagrid entity implementation of Datagrid interface, implements all methods and has protected methods to apply additional entities parameters to DB request and bind source parameters; diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/filters.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/filters.md index c8223605fd1..a344f04b83e 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/filters.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/filters.md @@ -3,15 +3,10 @@ Filters Filters allows to apply additional conditions to DB request and show in grid only required rows. Filter entities are created by Filter Factory. -Filter functionality based on Sonata AdminBundle filters. - Flexible filters are used to apply filters to flexible attributes in flexible entities. Flexible filters has parent filters and use their basic functionality (operators, settings etc). #### Class Description -* **Sonata \ AdminBundle \ Filter \ FilterInterface** - Sonata AdminBundle standard filter interface; -* **Sonata \ AdminBundle \ Filter \ Filter** - Sonata AdminBundle abstract filter implementation; -* **Sonata \ DoctirneORMAdminBundle \ Filter \ Filter** - Sonata AdminBundle abstract filter implementation for Doctrine ORM; * **Filter \ FilterInterface** - basic interface for Grid Filter entities; * **Filter \ ORM \ AbstractFilter** - abstract implementation of Filter entity; * **Filter \ ORM \ NumberFilter** - ORM filter for number values; @@ -30,7 +25,6 @@ entity repository or query builder as choices data source; * **Filter \ ORM \ Flexible \ AbstractFlexibleDateFilter** - abstract ORM filter to work with date/time flexible attributes; * **Filter \ ORM \ Flexible \ FlexibleDateRangeFilter** - ORM filter for date flexible attribute; * **Filter \ ORM \ Flexible \ FlexibleDateTimeRangeFilter - ORM filter for datetime flexible attribute; -* **Sonata \ AdminBundle \ Filter \ FilterFactoryInterface** - Sonata AdminBundle interface for filter factory; * **Filter \ FilterFactoryInterface** - basic interface for Filter Factory entity; * **Filter \ FilterFactory** - basic implementation of Filter Factory entity to create Filter entities. @@ -82,6 +76,12 @@ services: tags: - { name: oro_grid.filter.type, alias: oro_grid_orm_choice } + oro_grid.orm.filter.type.select: + class: Oro\Bundle\GridBundle\Filter\ORM\SelectFilter + arguments: ["@translator"] + tags: + - { name: oro_grid.filter.type, alias: oro_grid_orm_select } + oro_grid.orm.filter.type.boolean: class: Oro\Bundle\GridBundle\Filter\ORM\BooleanFilter arguments: ["@translator"] diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/overview.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/overview.md index 3e352de661f..9d5e0d18aa5 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/overview.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/overview.md @@ -1,9 +1,10 @@ Overview -------- -Datagird backend consists of several entities, which are used to perform specific actions. Every entity implements interface, so every part can be easy extended and replaced with external component. +Datagird backend consists of several entities, which are used to perform specific actions. +Every entity implements interface, so every part can be easy extended and replaced with external component. -Datagrid entities use standard Symfony interfaces to perform translation and validation. Also some interfaces and entities are extended from Sonata AdminBundle classes, so basic Sonata classes can be injected into datagrid entities. +Datagrid entities use standard Symfony interfaces to perform translation and validation. #### Used External Interfaces @@ -11,11 +12,3 @@ Datagrid entities use standard Symfony interfaces to perform translation and val * Translator - Symfony\Component\Translation\TranslatorInterface; * Validator - Symfony\Component\Validator\ValidatorInterface; - -**Sonata AdminBundle** - -* Datagrid - Sonata\AdminBundle\Datagrid\DatagridInterface; -* Filter - Sonata\AdminBundle\Filter\FilterInterface; -* Filter Factory - Sonata\AdminBundle\Filter\FilterFactoryInterface; -* Pager - Sonata\AdminBundle\Datagrid\PagerInterface; -* Proxy Query - Sonata\AdminBundle\Datagrid\ProxyQueryInterface. diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/pager.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/pager.md index 591998c53a0..a0961997e9b 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/pager.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/pager.md @@ -5,8 +5,6 @@ Pager is an entity that provides information about pagination parameters on grid #### Class Description -* **Sonata \ AdminBundle \ Datagrid \ PagerInterface** - Sonata AdminBundle pager interface; -* **Sonata \ AdminBundle \ Datagrid \ Pager** - abstract implementation of Sonata pager interface; -* **Sonata \ DoctrineORMAdminBundle \ Datagrid \ Pager** - Sonata implementation of pager for Doctrine ORM extended from abstract pager; -* **Datagrid \ PagerInterface** - basic interface for Pager entity, provides getters and setters for pagination parameters, applies it and returns values of pagination parameters; +* **Datagrid \ PagerInterface** - basic interface for Pager entity, provides getters and setters for pagination +parameters, applies it and returns values of pagination parameters; * **Datagrid \ ORM \ Pager** - Pager implementation of basic interface with all required methods. diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/proxy-query.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/proxy-query.md index 6124632a1e9..cf7e946d96c 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/proxy-query.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/proxy-query.md @@ -5,10 +5,8 @@ Proxy Query is an objects that encapsulates interaction with DB and provides get #### Class Description -* **Sonata \ AdminBundle \ Datagrid \ ProxyQueryInterface** - Sonata AdminBundle interface that provides methods to get and set query parameters and execute DB query; -* **Sonata \ DoctrintORMBundle \ Datagrid \ ProxyQuery** - implementation of Sonata proxy query interface; -* **Datagrid \ ProxyQueryInterface** - basic interface for Proxy Query fully extended from Sonata interface; -* **Datagrid \ ORM \ ProxyQuery** - implementation of Proxy Query entity extended from Sonata proxy query entity, provides getter for Query Builder; +* **Datagrid \ ProxyQueryInterface** - basic interface for Proxy Query; +* **Datagrid \ ORM \ ProxyQuery** - implementation of Proxy Query entity, provides getter for Query Builder; * **Datagrid \ QueryFactoryInterface** - interface for Query Factory entity, provide method to create query entity; * **Datagrid \ ORM \ QueryFactory \ AbstractQueryFactory** - abstract implementation of Query Factory interface, has protected method to create Proxy Query entity; * **Datagrid \ ORM \ QueryFactory \ QueryFactory** - extended from abstract Query Factory, receives Query Builder as source parameter and creates Proxy Query based on it; diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md new file mode 100644 index 00000000000..70f93d95b28 --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/backend/reports.md @@ -0,0 +1,172 @@ +Reports +------- + +Datagrid bundle provides basic functionality to build reports based on defined structure. PHP array or YAML string +supported as a storage. + +#### Structure definition + +- *name* - report name, reserved for future use +- *distinct* - boolean, indicates that query should use DISTINCT keyword (false by default) +- *select* - string representing column list to select +- *from* - array of table/alias declarations. Each element is an array with the following keys: + - *table* - entity declaration, for example "OroUserBundle:User" + - *alias* - [optional] alias for the entity/table name +- *join* - array of join declarations. May contain two children: + - *inner* - adds inner join. Format: + - *join* - entity relation name, for example "u.articles" + - *alias* - [optional] alias for a relation + - *left* - adds left join. Format: + - *join* - entity relation name, for example "u.articles" + - *alias* - [optional] alias for a relation +- *where* - array of where declarations. May contain two children: + - *and* - adds "AND" where clause. You can use multiple declarations as array elements. + - *or* - adds "OR" where clause. You can use multiple declarations as array elements. +- *groupBy* - group definition as a string +- *having* - "HAVING" clause definition as a string +- *orderBy* - array of order declarations. Format: + - *column* - order column + - *dir* - [optional] sort direction, "asc" by default + + +#### Example of usage + +**YAML definition** +``` yaml +reports: + - + name: DemoReport + distinct: false + select: "u.username, u.firstName, u.loginCount, a.apiKey, COUNT(u.firstName) AS cnt" + from: + - { table: OroUserBundle:User, alias: u } + join: + left: + - { join: u.api, alias: a } + - { join: u.statuses, alias: s } + where: + and: + - "u.loginCount >= 0" + or: + - "u.enabled = 0" + groupBy: "u.firstName" + having: "COUNT(u.firstName) > 1" + orderBy: + - { column: "u.firstName", dir: "asc" } +``` + +**Configuration** + +services.yml +``` yaml +parameters: + acme_demo_grid.report_grid.manager.class: Acme\Bundle\DemoGridBundle\Datagrid\ReportDatagridManager + +services: +acme_demo_grid.report_grid.manager: + class: %acme_demo_grid.report_grid.manager.class% + tags: + - name: oro_grid.datagrid.manager + datagrid_name: report + entity_hint: reports + route_name: acme_demo_gridbundle_report_list +``` + +**Datagrid declaration** +``` php +queryFactory->setQueryBuilder( + $converter->parse($input['reports'][0], $this->entityManager) + ); + + return $this->queryFactory->createQuery(); + } + + /** + * @param EntityManager $entityManager + */ + public function setEntityManager(EntityManager $entityManager) + { + $this->entityManager = $entityManager; + } +} +``` + +**Controller** +``` php +get('acme_demo_grid.report_grid.manager'); + + $gridManager->setEntityManager($this->getDoctrine()->getManager()); + + $grid = $gridManager->getDatagrid(); + $gridView = $grid->createView(); + + if ('json' == $this->getRequest()->getRequestFormat()) { + return $this->get('oro_grid.renderer')->renderResultsJsonResponse($gridView); + } + + return array('datagrid' => $gridView); + } +} +``` \ No newline at end of file diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/frontend/basic-classes.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/frontend/basic-classes.md index eebdddadc67..e6b39f62277 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/frontend/basic-classes.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/frontend/basic-classes.md @@ -81,7 +81,8 @@ var grid = new Oro.Datagrid.Grid({ } ], entityHint: "Users", - noDataHint: "No users were found to match your search. Try modifying your search criteria or creating a new ..." + noDataHint: "There are no users yet. Try to creating a new ..." + noResultsHint: "No users were found to match your search. Try modifying your search criteria ..." }); $('#grid').html(grid.render().$el); diff --git a/src/Oro/Bundle/GridBundle/Resources/doc/reference/overview.md b/src/Oro/Bundle/GridBundle/Resources/doc/reference/overview.md index d2ff0934ef1..3d1b555b27f 100644 --- a/src/Oro/Bundle/GridBundle/Resources/doc/reference/overview.md +++ b/src/Oro/Bundle/GridBundle/Resources/doc/reference/overview.md @@ -137,8 +137,6 @@ Dependencies * Oro UIBundle - https://github.com/laboro/UIBundle; * Oro FilterBundle - https://github.com/laboro/FilterBundle; * Oro FlexibleEntityBundle - https://github.com/laboro/FlexibleEntityBundle; -* Sonata AdminBundle 2.1 (Oro fork) - https://github.com/laboro/SonataAdminBundle; -* Sonata DoctrineORM AdminBundle 2.1 - https://github.com/sonata-project/SonataDoctrineORMAdminBundle. #### Frontend Dependencies diff --git a/src/Oro/Bundle/GridBundle/Resources/public/css/oro.grid.css b/src/Oro/Bundle/GridBundle/Resources/public/css/oro.grid.css index 9653d4d2045..5190a272501 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/css/oro.grid.css +++ b/src/Oro/Bundle/GridBundle/Resources/public/css/oro.grid.css @@ -1,4 +1,5 @@ -table.grid thead th { +table.grid thead th > a, +table.grid thead th > span { text-transform: uppercase; } @@ -39,6 +40,7 @@ table.grid tbody tr.row-click-action { font-size: 16px; padding: 10px; border: 1px solid #dddddd; + margin-top: 20px; } .loading-mask .loading-frame .box { diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/abstractaction.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/abstractaction.js index 661cd3d56ef..8440b570847 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/abstractaction.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/abstractaction.js @@ -19,6 +19,44 @@ Oro.Datagrid.Action.AbstractAction = Backbone.View.extend({ /** @property {Object} */ launcherOptions: undefined, + /** @property {String} */ + name: null, + + /** @property {Oro.Datagrid.Grid} */ + datagrid: null, + + /** @property {string} */ + route: null, + + /** @property {Object} */ + route_parameters: null, + + /** @property {Boolean} */ + confirmation: false, + + /** @property {String} */ + frontend_type: null, + + /** @property {Object} */ + frontend_options: null, + + /** @property {string} */ + identifierFieldName: 'id', + + messages: {}, + + dispatched: false, + + /** @property {Object} */ + defaultMessages: { + confirm_title: _.__('Execution Confirmation'), + confirm_content: _.__('Are you sure you want to do this?'), + confirm_ok: _.__('Yes, do it'), + success: _.__('Action was successfully performed.'), + error: _.__('Action was not performed.'), + empty_selection: _.__('Please, select item to perform action.') + }, + /** * Initialize view * @@ -28,6 +66,13 @@ Oro.Datagrid.Action.AbstractAction = Backbone.View.extend({ initialize: function(options) { options = options || {}; + if (!options.datagrid) { + throw new TypeError("'datagrid' is required"); + } + this.datagrid = options.datagrid; + + _.defaults(this.messages, this.defaultMessages); + if (options.launcherOptions) { this.launcherOptions = _.extend({}, this.launcherOptions, options.launcherOptions); } @@ -47,6 +92,9 @@ Oro.Datagrid.Action.AbstractAction = Backbone.View.extend({ */ createLauncher: function(options) { options = options || {}; + if (_.isUndefined(options.icon) && !_.isUndefined(this.icon)) { + options.icon = this.icon; + } _.defaults(options, this.launcherOptions); return new (this.launcherPrototype)(options); }, @@ -69,6 +117,143 @@ Oro.Datagrid.Action.AbstractAction = Backbone.View.extend({ * Execute action */ execute: function() { - throw new Error("Method execute is abstract and must be implemented"); + var eventName = this.getEventName(); + Oro.Events.once(eventName, this.executeConfiguredAction, this); + this._confirmationExecutor( + _.bind( + function() {Oro.Events.trigger(eventName, this);}, + this + ) + ); + }, + + getEventName: function() { + return 'grid_action_execute:' + this.datagrid.name + ':' + this.name; + }, + + executeConfiguredAction: function(action) { + if (action.frontend_type == 'ajax') { + this._handleAjax(action); + } else if (action.frontend_type == 'redirect') { + this._handleRedirect(action); + } else if (Oro.widget.Manager.isSupportedType(action.frontend_type)) { + this._handleWidget(action); + } + }, + + _confirmationExecutor: function(callback) { + if (this.confirmation) { + this.getConfirmDialog(callback).open(); + } else { + callback(); + } + }, + + _handleWidget: function(action) { + if (action.dispatched) { + return; + } + action.frontend_options.url = action.frontend_options.url || this.getLinkWithParameters(); + action.frontend_options.title = action.frontend_options.title || this.label; + Oro.widget.Manager.createWidget(action.frontend_type, action.frontend_options).render(); + }, + + _handleRedirect: function(action) { + if (action.dispatched) { + return; + } + var url = action.getLinkWithParameters(); + if (Oro.hashNavigationEnabled()) { + Oro.hashNavigationInstance.processRedirect({ + fullRedirect: false, + location: url + }); + } else { + location.href = url; + } + }, + + _handleAjax: function(action) { + if (action.dispatched) { + return; + } + action.datagrid.showLoading(); + $.ajax({ + url: action.getLink(), + data: action.getActionParameters(), + context: action, + dataType: 'json', + error: action._onAjaxError, + success: action._onAjaxSuccess + }); + }, + + _onAjaxError: function(jqXHR, textStatus, errorThrown) { + Oro.BackboneError.Dispatch(null, jqXHR); + this.datagrid.hideLoading(); + }, + + _onAjaxSuccess: function(data, textStatus, jqXHR) { + this.datagrid.hideLoading(); + this.datagrid.collection.fetch(); + + var defaultMessage = data.successful ? this.messages.success : this.messages.error; + var message = data.message ? data.message : defaultMessage; + if (message) { + Oro.NotificationFlashMessage( + data.successful ? 'success' : 'error', + message + ); + } + }, + + /** + * Get action url + * + * @return {String} + * @private + */ + getLink: function(parameters) { + if (_.isUndefined(parameters)) { + parameters = {}; + } + return Routing.generate( + this.route, + _.extend( + this.route_parameters, + parameters + ) + ); + }, + + /** + * Get action url with parameters added. + * + * @returns {String} + */ + getLinkWithParameters: function() { + return this.getLink(this.getActionParameters()); + }, + + /** + * Get action parameters + * + * @returns {Object} + */ + getActionParameters: function() { + return {}; + }, + + /** + * Get view for confirm modal + * + * @return {Oro.BootstrapModal} + */ + getConfirmDialog: function(callback) { + return new Oro.BootstrapModal({ + title: this.messages.confirm_title, + content: this.messages.confirm_content, + okText: this.messages.confirm_ok + }).on('ok', callback); } }); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/ajaxaction.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/ajaxaction.js new file mode 100644 index 00000000000..21a5af7032e --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/ajaxaction.js @@ -0,0 +1,11 @@ +var Oro = Oro || {}; +Oro.Datagrid = Oro.Datagrid || {}; +Oro.Datagrid.Action = Oro.Datagrid.Action || {}; + +/** + * Ajax action, triggers REST AJAX request + * + * @class Oro.Datagrid.Action.AjaxAction + * @extends Oro.Datagrid.Action.ModelAction + */ +Oro.Datagrid.Action.AjaxAction = Oro.Datagrid.Action.ModelAction.extend(); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/cell.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/cell.js index 0054fa73f1e..f75f2c6578d 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/cell.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/cell.js @@ -77,9 +77,18 @@ Oro.Datagrid.Action.Cell = Backgrid.Cell.extend({ */ createActions: function() { var result = []; + var actions = this.column.get('actions'); - _.each(actions, function(action) { - result.push(this.createAction(action)); + var actionConfiguration = this.model.get('action_configuration'); + _.each(actions, function(action, name) { + // filter available actions for current row + if ( + _.isUndefined(actionConfiguration) + || _.isUndefined(actionConfiguration[name]) + || actionConfiguration[name] + ) { + result.push(this.createAction(action)); + } }, this); return result; @@ -93,7 +102,8 @@ Oro.Datagrid.Action.Cell = Backgrid.Cell.extend({ */ createAction: function(actionPrototype) { return new actionPrototype({ - model: this.model + model: this.model, + datagrid: this.column.get('datagrid') }); }, @@ -107,11 +117,6 @@ Oro.Datagrid.Action.Cell = Backgrid.Cell.extend({ _.each(this.actions, function(action) { var options = {}; - if (action.icon) { - options = { - icon: action.icon - }; - } var launcher = action.createLauncher(options); result.push(launcher); }, this); @@ -123,6 +128,12 @@ Oro.Datagrid.Action.Cell = Backgrid.Cell.extend({ * Render cell with actions */ render: function () { + // don't render anything if list of launchers is empty + if (_.isEmpty(this.launchers)) { + this.$el.empty(); + + return this; + } this.$el.empty().append(this.template()); var launchers = this.getLaunchersByIcons(); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/deleteaction.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/deleteaction.js index a975c39648b..3dc055a9993 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/deleteaction.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/deleteaction.js @@ -55,8 +55,7 @@ Oro.Datagrid.Action.DeleteAction = Oro.Datagrid.Action.ModelAction.extend({ this.confirmModal = new Oro.BootstrapModal({ title: _.__('Delete Confirmation'), content: _.__('Are you sure you want to delete this item?'), - okText: _.__('Yes, Delete'), - allowCancel: 'false' + okText: _.__('Yes, Delete') }); this.confirmModal.on('ok', _.bind(this.doDelete, this)); } @@ -70,12 +69,12 @@ Oro.Datagrid.Action.DeleteAction = Oro.Datagrid.Action.ModelAction.extend({ */ getErrorDialog: function() { if (!this.errorModal) { - this.confirmModal = new Oro.BootstrapModal({ + this.errorModal = new Oro.BootstrapModal({ title: _.__('Delete Error'), content: _.__('Cannot delete item.'), cancelText: false }); } - return this.confirmModal; + return this.errorModal; } }); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/massaction.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/massaction.js new file mode 100644 index 00000000000..f3866115bcd --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/massaction.js @@ -0,0 +1,79 @@ +var Oro = Oro || {}; +Oro.Datagrid = Oro.Datagrid || {}; +Oro.Datagrid.Action = Oro.Datagrid.Action || {}; + +/** + * Basic mass action class. + * + * @class Oro.Datagrid.MassAction + * @extends Oro.Datagrid.Action.AbstractAction + */ +Oro.Datagrid.Action.MassAction = Oro.Datagrid.Action.AbstractAction.extend({ + /** @property {Object} */ + defaultMessages: { + confirm_title: _.__('Mass Action Confirmation'), + confirm_content: _.__('Are you sure you want to do this?'), + confirm_ok: _.__('Yes, do it'), + success: _.__('Mass action was successfully performed.'), + error: _.__('Mass action was not performed.'), + empty_selection: _.__('Please, select items to perform mass action.') + }, + + initialize: function(options) { + Oro.Datagrid.Action.AbstractAction.prototype.initialize.apply(this, arguments); + this.route_parameters = _.extend(this.route_parameters, {gridName: this.datagrid.name, actionName: this.name}); + }, + + /** + * Ask a confirmation and execute mass action. + */ + execute: function() { + var selectionState = this.datagrid.getSelectionState(); + if (_.isEmpty(selectionState.selectedModels) && selectionState.inset) { + Oro.NotificationFlashMessage('warning', this.messages.empty_selection); + } else { + Oro.Datagrid.Action.AbstractAction.prototype.execute.call(this); + } + }, + + /** + * Get action parameters + * + * @returns {Object} + * @private + */ + getActionParameters: function() { + var selectionState = this.datagrid.getSelectionState(); + var collection = this.datagrid.collection; + var idValues = _.map(selectionState.selectedModels, function(model) { + return model.get(this.identifierFieldName) + }, this); + + var params = { + inset: selectionState.inset ? 1 : 0, + values: idValues.join(',') + }; + + params = collection.processFiltersParams(params, null, 'filters'); + + return params; + }, + + _onAjaxSuccess: function(data, textStatus, jqXHR) { + this.datagrid.resetSelectionState(); + Oro.Datagrid.Action.AbstractAction.prototype._onAjaxSuccess.apply(this, arguments); + }, + + /** + * Get view for confirm modal + * + * @return {Oro.BootstrapModal} + */ + getConfirmDialog: function(callback) { + return new Oro.BootstrapModal({ + title: this.messages.confirm_title, + content: this.messages.confirm_content, + okText: this.messages.confirm_ok + }).on('ok', callback); + } +}); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/refreshcollectionaction.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/refreshcollectionaction.js index b5ef2d18c89..790c4f57109 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/refreshcollectionaction.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/refreshcollectionaction.js @@ -23,10 +23,10 @@ Oro.Datagrid.Action.RefreshCollectionAction = Oro.Datagrid.Action.AbstractAction initialize: function(options) { options = options || {}; - if (!options.collection) { - throw new TypeError("'collection' is required"); + if (!options.datagrid) { + throw new TypeError("'datagrid' is required"); } - this.collection = options.collection; + this.collection = options.datagrid.collection; Oro.Datagrid.Action.AbstractAction.prototype.initialize.apply(this, arguments); }, diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/resetcollectionaction.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/resetcollectionaction.js index a0f786baa08..a17f92f2891 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/resetcollectionaction.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/action/resetcollectionaction.js @@ -23,10 +23,10 @@ Oro.Datagrid.Action.ResetCollectionAction = Oro.Datagrid.Action.AbstractAction.e initialize: function(options) { options = options || {}; - if (!options.collection) { - throw new TypeError("'collection' is required"); + if (!options.datagrid) { + throw new TypeError("'datagrid' is required"); } - this.collection = options.collection; + this.collection = options.datagrid.collection; Oro.Datagrid.Action.AbstractAction.prototype.initialize.apply(this, arguments); }, diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectallheader.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectallheader.js new file mode 100644 index 00000000000..3176afff50f --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectallheader.js @@ -0,0 +1,229 @@ +/* jshint browser:true */ +(function (factory) { + "use strict"; + /* global define, Oro, jQuery, _, Backgrid */ + if (typeof define === 'function' && define.amd) { + define(['Oro', 'jQuery', '_', 'Backgrid', 'OroDatagridCellSelectRowCell'], factory); + } else { + factory(Oro, jQuery, _, Backgrid, Oro.Datagrid.Cell.SelectRowCell); + } +}(function (Oro, $, _, Backgrid, SelectRowCell) { + "use strict"; + Oro.Datagrid = Oro.Datagrid || {}; + Oro.Datagrid.Cell = Oro.Datagrid.Cell || {}; + + /** + * Contains mass-selection logic + * - watches models selection, keeps reference to selected + * - provides mass-selection actions + * - listening to models collection events, + * fills in 'obj' with proper data for + * `backgrid:isSelected` and `backgrid:getSelected` + * + * @class Oro.Datagrid.Cell.SelectAllHeaderCell + * @extends Oro.Datagrid.Cell.SelectRowCell + */ + Oro.Datagrid.Cell.SelectAllHeaderCell = SelectRowCell.extend({ + /** @property */ + className: "select-all-header-cell", + + /** @property */ + tagName: "th", + + events: {}, + + /** + * Initializer. + * Subscribers on events listening + * + * @param {Object} options + * @param {Backgrid.Column} options.column + * @param {Backbone.Collection} options.collection + */ + initialize: function (options) { + //Backgrid.requireOptions(options, ["column", "collection"]); + + this.column = options.column; + if (!(this.column instanceof Backgrid.Column)) { + this.column = new Backgrid.Column(this.column); + } + + this.initialState(); + this.listenTo(this.collection, { + remove: this.removeModel, + updateState: this.initialState, + 'backgrid:selected': this.selectModel, + 'backgrid:selectAll': this.selectAll, + 'backgrid:selectAllVisible': this.selectAllVisible, + 'backgrid:selectNone': this.selectNone, + + 'backgrid:isSelected': _.bind(function (model, obj) { + if ($.isPlainObject(obj)) { + obj.selected = this.isSelectedModel(model); + } + }, this), + 'backgrid:getSelected': _.bind(function (obj) { + if ($.isEmptyObject(obj)) { + obj.selected = _.keys(this.selectedModels); + obj.inset = this.inset; + } + }, this) + }); + }, + + /** + * Resets selection to initial conditions + * - clear selected models set + * - reset set type in-set/not-in-set + */ + initialState: function () { + this.selectedModels = {}; + this.inset = true; + }, + + /** + * Gets selection state + * + * @returns {{selectedModels: *, inset: boolean}} + */ + getSelectionState: function() { + return { + selectedModels: this.selectedModels, + inset: this.inset + } + }, + + /** + * Checks if passed model have to be marked as selected + * + * @param {Backbone.Model} model + * @returns {boolean} + */ + isSelectedModel: function (model) { + return this.inset === _.has(this.selectedModels, model.id || model.cid); + }, + + /** + * Removes model from selected models set + * + * @param {Backbone.Model} model + */ + removeModel: function (model) { + delete this.selectedModels[model.id || model.cid]; + }, + + /** + * Adds/removes model to/from selected models set + * + * @param {Backbone.Model} model + * @param {boolean} selected + */ + selectModel: function (model, selected) { + if (selected === this.inset) { + this.selectedModels[model.id || model.cid] = model; + } else { + this.removeModel(model); + } + }, + + /** + * Performs selection of all possible models: + * - reset to initial state + * - change type of set type as not-inset + * - marks all models in collection as selected + * start to collect models which have to be excluded + */ + selectAll: function () { + this.initialState(); + this.inset = false; + this._selectAll(); + }, + + /** + * Reset selection of all possible models: + * - reset to initial state + * - change type of set type as inset + * - marks all models in collection as not selected + * start to collect models which have to be included + */ + selectNone: function () { + this.initialState(); + this.inset = true; + this._selectNone(); + }, + + /** + * Performs selection of all visible models: + * - if necessary reset to initial state + * - marks all models in collection as selected + */ + selectAllVisible: function () { + if (!this.inset) { + this.initialState(); + } + this._selectAll(); + }, + + /** + * Marks all models in collection as selected + * + * @private + */ + _selectAll: function () { + this.collection.each(function (model) { + model.trigger("backgrid:select", model, true); + }); + }, + + /** + * Marks all models in collection as not selected + * + * @private + */ + _selectNone: function () { + this.collection.each(function (model) { + model.trigger("backgrid:select", model, false); + }); + }, + + /** + * + * + * @returns {Oro.Datagrid.Cell.SelectAllHeaderCell} + */ + render: function () { + /*jshint multistr:true */ + /*jslint es5: true */ + /* temp solution: start */ + // It's not clear for now, how mass selection will be designed, + // thus implementation is done just to check functionality. + // For future render method will depend on options or will be empty + this.$el.empty().append('
      \ + \ + \ + \ +
      '); + this.$el.find('[data-select-all]').on('click', _.bind(function (e) { + this.collection.trigger('backgrid:selectAll'); + e.preventDefault(); + }, this)); + this.$el.find('[data-select-all-visible]').on('click', _.bind(function (e) { + this.collection.trigger('backgrid:selectAllVisible'); + e.preventDefault(); + }, this)); + this.$el.find('[data-select-none]').on('click', _.bind(function (e) { + this.collection.trigger('backgrid:selectNone'); + e.preventDefault(); + }, this)); + /* temp solution: end */ + return this; + } + }); + + return Oro.Datagrid.Cell.SelectAllHeaderCell; +})); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectrowcell.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectrowcell.js new file mode 100644 index 00000000000..5ec821ec15c --- /dev/null +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/cell/selectrowcell.js @@ -0,0 +1,100 @@ +/* jshint browser:true */ +(function (factory) { + "use strict"; + /* global define, Oro, jQuery, _, Backbone, Backgrid */ + if (typeof define === 'function' && define.amd) { + define(['Oro', 'jQuery', '_', 'Backbone', 'Backgrid'], factory); + } else { + factory(Oro, jQuery, _, Backbone, Backgrid); + } +}(function (Oro, $, _, Backbone, Backgrid) { + "use strict"; + Oro.Datagrid = Oro.Datagrid || {}; + Oro.Datagrid.Cell = Oro.Datagrid.Cell || {}; + + /** + * Renders a checkbox for row selection. + * + * @class Oro.Datagrid.Cell.SelectRowCell + * @extends Backbone.View + */ + Oro.Datagrid.Cell.SelectRowCell = Backbone.View.extend({ + + /** @property */ + className: "select-row-cell", + + /** @property */ + tagName: "td", + + /** @property */ + events: { + "change :checkbox": "onChange", + "click": "enterEditMode" + }, + + /** + * Initializer. If the underlying model triggers a `select` event, this cell + * will change its checked value according to the event's `selected` value. + * + * @param {Object} options + * @param {Backgrid.Column} options.column + * @param {Backbone.Model} options.model + */ + initialize: function (options) { + //Backgrid.requireOptions(options, ["model", "column"]); + + this.column = options.column; + if (!(this.column instanceof Backgrid.Column)) { + this.column = new Backgrid.Column(this.column); + } + + this.listenTo(this.model, "backgrid:select", function (model, checked) { + this.$el.find(":checkbox").prop("checked", checked).change(); + }); + }, + + /** + * Focuses the checkbox. + */ + enterEditMode: function (e) { + var $checkbox = this.$el.find(":checkbox").focus(); + if ($checkbox[0] !== e.target) { + $checkbox.prop("checked", !$checkbox.prop("checked")).change(); + } + e.stopPropagation(); + }, + + /** + * Unfocuses the checkbox. + */ + exitEditMode: function () { + this.$el.find(":checkbox").blur(); + }, + + /** + * When the checkbox's value changes, this method will trigger a Backbone + * `backgrid:selected` event with a reference of the model and the + * checkbox's `checked` value. + */ + onChange: function (e) { + this.model.trigger("backgrid:selected", this.model, $(e.target).prop("checked")); + }, + + /** + * Renders a checkbox in a table cell. + */ + render: function () { + // work around with trigger event to get current state of model (selected or not) + var state = {selected: false}; + this.$el.empty().append(''); + this.model.trigger('backgrid:isSelected', this.model, state); + if (state.selected) { + this.$el.find(':checkbox').prop('checked', 'checked'); + } + this.delegateEvents(); + return this; + } + }); + + return Oro.Datagrid.Cell.SelectRowCell; +})); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/filter/list.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/filter/list.js index a93f783e5d0..81f8935c0cd 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/filter/list.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/filter/list.js @@ -95,6 +95,9 @@ Oro.Datagrid.Filter.List = Oro.Filter.List.extend({ * @return {*} */ _applyState: function(state) { + var toEnable = []; + var toDisable = []; + _.each(this.filters, function(filter, name) { var shortName = '__' + name; if (_.has(state, name)) { @@ -104,22 +107,28 @@ Oro.Datagrid.Filter.List = Oro.Filter.List.extend({ value: filterState } } - this.enableFilter(filter.setValue(filterState)); + filter.setValue(filterState); + toEnable.push(filter); } else if (_.has(state, shortName)) { + filter.reset(); if (Number(state[shortName])) { - this.enableFilter(filter.reset()); + toEnable.push(filter); } else { - this.disableFilter(filter.reset()); + toDisable.push(filter); } } else { + filter.reset(); if (filter.defaultEnabled) { - this.enableFilter(filter.reset()); + toEnable.push(filter); } else { - this.disableFilter(filter.reset()); + toDisable.push(filter); } } }, this); + this.enableFilters(toEnable); + this.disableFilters(toDisable); + return this; } }); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js index c24133ec558..19c4bf75df1 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/grid.js @@ -12,13 +12,16 @@ Oro.Datagrid = Oro.Datagrid || {}; * @extends Backgrid.Grid */ Oro.Datagrid.Grid = Backgrid.Grid.extend({ - /** @property */ + /** @property {String} */ + name: 'datagrid', + + /** @property {String} */ tagName: 'div', - /** @property */ + /** @property {int} */ requestsCount: 0, - /** @property */ + /** @property {String} */ className: 'clearfix', /** @property */ @@ -41,7 +44,8 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ grid: '.grid', toolbar: '.toolbar', noDataBlock: '.no-data', - loadingMask: '.loading-mask' + loadingMask: '.loading-mask', + filterBox: '.filter-box' }, /** @property {Oro.Datagrid.Header} */ @@ -64,13 +68,15 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ */ defaults: { noDataHint: 'No data found.', + noResultsHint: 'No items found during search.', rowClickActionClass: 'row-click-action', rowClassName: '', toolbarOptions: {}, addResetAction: true, addRefreshAction: true, rowClickAction: undefined, - rowActions: [] + rowActions: [], + massActions: [] }, /** @@ -86,6 +92,7 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ * @param {Boolean} [options.addResetAction] If TRUE reset action will be added in toolbar * @param {Boolean} [options.addRefreshAction] If TRUE refresh action will be added in toolbar * @param {Oro.Datagrid.Action.AbstractAction[]} [options.rowActions] Array of row actions prototypes + * @param {Oro.Datagrid.Action.AbstractAction[]} [options.massActions] Array of mass actions prototypes * @param {Oro.Datagrid.Action.AbstractAction} [options.rowClickAction] Prototype for action that handles row click * @throws {TypeError} If mandatory options are undefined */ @@ -113,9 +120,10 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ } options.columns.push(this._createActionsColumn()); + options.columns.unshift(this._getMassActionsColumn()); this.loadingMask = this._createLoadingMask(); - this.toolbar = this._createToolbar(this.toolbarOptions); + this.toolbar = this._createToolbar(_.extend(this.toolbarOptions, options.toolbarOptions)); Backgrid.Grid.prototype.initialize.apply(this, arguments); @@ -151,10 +159,51 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ */ _createActionsColumn: function() { return new this.actionsColumn({ - actions: this.rowActions + actions: this.rowActions, + datagrid: this }); }, + /** + * Creates mass actions column + * + * @return {Backgrid.Column} + * @private + */ + _getMassActionsColumn: function() { + if (!this.massActionsColumn) { + this.massActionsColumn = new Backgrid.Column({ + name: "massAction", + label: _.__("Selected Rows"), + renderable: !_.isEmpty(this.massActions), + sortable: false, + editable: false, + cell: Oro.Datagrid.Cell.SelectRowCell, + headerCell: Oro.Datagrid.Cell.SelectAllHeaderCell + }); + } + + return this.massActionsColumn; + }, + + /** + * Gets selection state + * + * @returns {{selectedModels: *, inset: boolean}} + */ + getSelectionState: function() { + var selectAllHeader = this.header.row.cells[0]; + return selectAllHeader.getSelectionState(); + }, + + /** + * Resets selection state + */ + resetSelectionState: function() { + var selectAllHeader = this.header.row.cells[0]; + return selectAllHeader.selectNone(); + }, + /** * Creates loading mask * @@ -175,7 +224,8 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ _createToolbar: function(toolbarOptions) { return new this.toolbar(_.extend({}, toolbarOptions, { collection: this.collection, - actions: this._getToolbarActions() + actions: this._getToolbarActions(), + massActions: this._getToolbarMassActions() })); }, @@ -196,6 +246,36 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ return result; }, + /** + * Get mass actions of toolbar + * + * @return {Array} + * @private + */ + _getToolbarMassActions: function() { + var result = []; + _.each(this.massActions, function(action) { + result.push(this.createMassAction(action)); + }, this); + + return result; + }, + + /** + * Creates action + * + * @param {Function} actionPrototype + * @protected + */ + createMassAction: function(actionPrototype) { + return new actionPrototype({ + datagrid: this, + launcherOptions: { + className: 'btn' + } + }); + }, + /** * Get action that refreshes grid's collection * @@ -204,7 +284,7 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ getRefreshAction: function() { if (!this.refreshAction) { this.refreshAction = new Oro.Datagrid.Action.RefreshCollectionAction({ - collection: this.collection, + datagrid: this, launcherOptions: { label: 'Refresh', className: 'btn', @@ -223,7 +303,7 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ getResetAction: function() { if (!this.resetAction) { this.resetAction = new Oro.Datagrid.Action.ResetCollectionAction({ - collection: this.collection, + datagrid: this, launcherOptions: { label: 'Reset', className: 'btn', @@ -277,9 +357,16 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ _runRowClickAction: function(row) { if (this.rowClickAction) { var action = new this.rowClickAction({ + datagrid: this, model: row.model }); - action.run(); + var actionConfiguration = row.model.get('action_configuration'); + if (_.isUndefined(actionConfiguration) + || _.isUndefined(actionConfiguration[action.name]) + || actionConfiguration[action.name] + ) { + action.run(); + } } }, @@ -339,8 +426,15 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ * Render no data block. */ renderNoDataBlock: function() { - this.$(this.selectors.noDataBlock).append($(this.noDataTemplate({ - hint: this.noDataHint.replace('\n', '
      ') + if (_.isEmpty(this.collection.state.filters)) { + // no filters + var dataHint = this.noDataHint; + } else { + // some filters exists + var dataHint = this.noResultsHint; + } + this.$(this.selectors.noDataBlock).html($(this.noDataTemplate({ + hint: dataHint.replace('\n', '
      ') }))).hide(); this._updateNoDataBlock(); }, @@ -352,8 +446,7 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ */ _beforeRequest: function() { this.requestsCount++; - this.loadingMask.show(); - this.toolbar.disable(); + this.showLoading(); }, /** @@ -364,9 +457,9 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ _afterRequest: function() { this.requestsCount--; if (this.requestsCount == 0) { - this.loadingMask.hide(); - this.toolbar.enable(); - this._updateNoDataBlock(); + this.hideLoading(); + // render block instead of update in order to change message depending on filter state + this.renderNoDataBlock(); /** * Backbone event. Fired when data for grid has been successfully rendered. * @event grid_load:complete @@ -375,6 +468,22 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ } }, + /** + * Show loading mask and disable toolbar + */ + showLoading: function() { + this.loadingMask.show(); + this.toolbar.disable(); + }, + + /** + * Hide loading mask and enable toolbar + */ + hideLoading: function() { + this.loadingMask.hide(); + this.toolbar.enable(); + }, + /** * Update no data block status * @@ -382,10 +491,14 @@ Oro.Datagrid.Grid = Backgrid.Grid.extend({ */ _updateNoDataBlock: function() { if (this.collection.models.length > 0) { + this.$(this.selectors.toolbar).show(); this.$(this.selectors.grid).show(); + this.$(this.selectors.filterBox).show(); this.$(this.selectors.noDataBlock).hide(); } else { this.$(this.selectors.grid).hide(); + this.$(this.selectors.toolbar).hide(); + this.$(this.selectors.filterBox).hide(); this.$(this.selectors.noDataBlock).show(); } }, diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js index 8d051b3718c..db132aa1d3d 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagesize.js @@ -13,11 +13,12 @@ Oro.Datagrid.PageSize = Backbone.View.extend({ '' + '
      ' + '' + '' + '
      ' @@ -34,6 +35,9 @@ Oro.Datagrid.PageSize = Backbone.View.extend({ /** @property */ enabled: true, + /** @property */ + hidden: false, + /** * Initializer. * @@ -56,6 +60,10 @@ Oro.Datagrid.PageSize = Backbone.View.extend({ this.listenTo(this.collection, "add", this.render); this.listenTo(this.collection, "remove", this.render); this.listenTo(this.collection, "reset", this.render); + + this.enabled = options.enable != false; + this.hidden = options.hide == true; + Backbone.View.prototype.initialize.call(this, options); }, @@ -88,22 +96,44 @@ Oro.Datagrid.PageSize = Backbone.View.extend({ */ onChangePageSize: function (e) { e.preventDefault(); - var pageSize = parseInt($(e.target).text()); + var pageSize = parseInt($(e.target).data('size')); if (pageSize !== this.collection.state.pageSize) { - this.collection.state.pageSize = pageSize; - this.collection.fetch(); + this.changePageSize(pageSize); } }, + changePageSize: function(pageSize) { + this.collection.state.pageSize = pageSize; + this.collection.fetch(); + + return this; + }, + render: function() { this.$el.empty(); + var currentSizeLabel = _.filter( + this.items, + _.bind( + function(item) { + return item.size == undefined ? this.collection.state.pageSize == item : this.collection.state.pageSize == item.size; + }, + this + ) + ); + currentSizeLabel = currentSizeLabel[0].label == undefined ? currentSizeLabel[0] : currentSizeLabel[0].label; + this.$el.append($(this.template({ disabled: !this.enabled || !this.collection.state.totalRecords, collectionState: this.collection.state, - items: this.items + items: this.items, + currentSizeLabel: currentSizeLabel }))); + if (this.hidden) { + this.$el.hide(); + } + return this; } }); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagination.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagination.js index 57e1000c9a9..440d24c6fc4 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagination.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/pagination.js @@ -20,6 +20,9 @@ Oro.Datagrid.Pagination = Backbone.View.extend({ /** @property */ enabled: true, + /** @property */ + hidden: false, + /** @property */ template: _.template( '' + @@ -77,6 +80,9 @@ Oro.Datagrid.Pagination = Backbone.View.extend({ this.listenTo(this.collection, "add", this.render); this.listenTo(this.collection, "remove", this.render); this.listenTo(this.collection, "reset", this.render); + + this.hidden = options.hide == true; + Backbone.View.prototype.initialize.call(this, options); }, @@ -204,6 +210,10 @@ Oro.Datagrid.Pagination = Backbone.View.extend({ state: state }))); + if (this.hidden) { + this.$el.hide(); + } + return this; } }); diff --git a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js index b183eb7285e..03e2c7910d1 100644 --- a/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js +++ b/src/Oro/Bundle/GridBundle/Resources/public/js/app/datagrid/toolbar.js @@ -13,6 +13,7 @@ Oro.Datagrid.Toolbar = Backbone.View.extend({ template:_.template( '
      ' + '
      ' + + '
      ' + '
    diff --git a/src/Oro/Bundle/SearchBundle/Resources/views/Search/searchResultItem.html.twig b/src/Oro/Bundle/SearchBundle/Resources/views/Search/searchResultItem.html.twig index d86b8457ac2..2c9c9e49801 100644 --- a/src/Oro/Bundle/SearchBundle/Resources/views/Search/searchResultItem.html.twig +++ b/src/Oro/Bundle/SearchBundle/Resources/views/Search/searchResultItem.html.twig @@ -1,19 +1,23 @@ {% import 'OroUserBundle::macros.html.twig' as macros %}
    - {% if recordUrl is defined %}{% endif %} - {{ title }} - {% if recordUrl is defined %}{% endif %} + {% if recordUrl is defined -%} + + {%- endif -%} + {{ title }} + {%- if recordUrl is defined -%} + + {%- endif %}
    {{ entityType }}
    -
    +

    - {% if recordUrl is defined %}{% endif %} + {% if recordUrl is defined %}{% endif %} {{ title }} {% if recordUrl is defined %}{% endif %}

    diff --git a/src/Oro/Bundle/SearchBundle/Resources/views/Api/results.html.twig b/src/Oro/Bundle/SearchBundle/Resources/views/Search/searchSuggestion.html.twig similarity index 100% rename from src/Oro/Bundle/SearchBundle/Resources/views/Api/results.html.twig rename to src/Oro/Bundle/SearchBundle/Resources/views/Search/searchSuggestion.html.twig diff --git a/src/Oro/Bundle/SearchBundle/Tests/Functional/API/RestAdvancedSearchApiTest.php b/src/Oro/Bundle/SearchBundle/Tests/Functional/API/RestAdvancedSearchApiTest.php index c67431d4d8e..f30907398ea 100644 --- a/src/Oro/Bundle/SearchBundle/Tests/Functional/API/RestAdvancedSearchApiTest.php +++ b/src/Oro/Bundle/SearchBundle/Tests/Functional/API/RestAdvancedSearchApiTest.php @@ -19,11 +19,7 @@ class RestAdvancedSearchApiTest extends WebTestCase public function setUp() { - if (!isset($this->client)) { - $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); - } else { - $this->client->restart(); - } + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); if (!self::$hasLoaded) { $this->client->appendFixtures(__DIR__ . DIRECTORY_SEPARATOR . 'DataFixtures'); } @@ -39,7 +35,11 @@ public function setUp() public function testApi($request, $response) { $requestUrl = $request['query']; - $this->client->request('GET', $this->client->generate('oro_api_get_search_advanced'), array('query' => $requestUrl)); + $this->client->request( + 'GET', + $this->client->generate('oro_api_get_search_advanced'), + array('query' => $requestUrl) + ); $result = $this->client->getResponse(); diff --git a/src/Oro/Bundle/SearchBundle/Tests/Functional/API/RestSearchApiTest.php b/src/Oro/Bundle/SearchBundle/Tests/Functional/API/RestSearchApiTest.php index d2a19e4b185..ee4f19adae1 100644 --- a/src/Oro/Bundle/SearchBundle/Tests/Functional/API/RestSearchApiTest.php +++ b/src/Oro/Bundle/SearchBundle/Tests/Functional/API/RestSearchApiTest.php @@ -19,12 +19,7 @@ class RestSearchApiTest extends WebTestCase public function setUp() { - if (!isset($this->client)) { - $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); - } else { - $this->client->restart(); - } - + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); if (!self::$hasLoaded) { $this->client->appendFixtures(__DIR__ . DIRECTORY_SEPARATOR . 'DataFixtures'); } diff --git a/src/Oro/Bundle/SearchBundle/Tests/Functional/API/SoapAdvancedSearchApiTest.php b/src/Oro/Bundle/SearchBundle/Tests/Functional/API/SoapAdvancedSearchApiTest.php index 67be6d27523..79c6c1c363d 100644 --- a/src/Oro/Bundle/SearchBundle/Tests/Functional/API/SoapAdvancedSearchApiTest.php +++ b/src/Oro/Bundle/SearchBundle/Tests/Functional/API/SoapAdvancedSearchApiTest.php @@ -22,20 +22,14 @@ class SoapAdvancedSearchApiTest extends WebTestCase public function setUp() { - if (!isset($this->client)) { - $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); - - $this->client->soap( - "http://localhost/api/soap", - array( - 'location' => 'http://localhost/api/soap', - 'soap_version' => SOAP_1_2 - ) - ); - - } else { - $this->client->restart(); - } + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); + $this->client->soap( + "http://localhost/api/soap", + array( + 'location' => 'http://localhost/api/soap', + 'soap_version' => SOAP_1_2 + ) + ); if (!self::$hasLoaded) { $this->client->appendFixtures(__DIR__ . DIRECTORY_SEPARATOR . 'DataFixtures'); @@ -48,7 +42,7 @@ public function setUp() */ public function testApi($request, $response) { - $result = $this->client->soapClient->advancedSearch($request['query']); + $result = $this->client->getSoap()->advancedSearch($request['query']); $result = ToolsAPI::classToArray($result); $this->assertEquals($response['count'], $result['count']); } diff --git a/src/Oro/Bundle/SearchBundle/Tests/Functional/API/SoapSearchApiTest.php b/src/Oro/Bundle/SearchBundle/Tests/Functional/API/SoapSearchApiTest.php index eef082e926c..3243edd81e8 100644 --- a/src/Oro/Bundle/SearchBundle/Tests/Functional/API/SoapSearchApiTest.php +++ b/src/Oro/Bundle/SearchBundle/Tests/Functional/API/SoapSearchApiTest.php @@ -23,20 +23,15 @@ class SoapSearchApiTest extends WebTestCase public function setUp() { - if (!isset($this->client)) { - $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); + $this->client = static::createClient(array(), ToolsAPI::generateWsseHeader()); - $this->client->soap( - "http://localhost/api/soap", - array( - 'location' => 'http://localhost/api/soap', - 'soap_version' => SOAP_1_2 - ) - ); - - } else { - $this->client->restart(); - } + $this->client->soap( + "http://localhost/api/soap", + array( + 'location' => 'http://localhost/api/soap', + 'soap_version' => SOAP_1_2 + ) + ); if (!self::$hasLoaded) { $this->client->appendFixtures(__DIR__ . DIRECTORY_SEPARATOR . 'DataFixtures'); @@ -44,11 +39,6 @@ public function setUp() self::$hasLoaded = true; } - public static function tearDownAfterClass() - { - Client::rollbackTransaction(); - } - /** * @param string $request * @param array $response @@ -66,7 +56,7 @@ public function testApi($request, $response) if (is_null($request['max_results'])) { $request['max_results'] = self::DEFAULT_VALUE; } - $result = $this->client->soapClient->search( + $result = $this->client->getSoap()->search( $request['search'], $request['offset'], $request['max_results'] diff --git a/src/Oro/Bundle/SearchBundle/Tests/Unit/Datagrid/SearchDatagridBuilderTest.php b/src/Oro/Bundle/SearchBundle/Tests/Unit/Datagrid/SearchDatagridBuilderTest.php index 8ac5fec8851..c3079c3f9df 100644 --- a/src/Oro/Bundle/SearchBundle/Tests/Unit/Datagrid/SearchDatagridBuilderTest.php +++ b/src/Oro/Bundle/SearchBundle/Tests/Unit/Datagrid/SearchDatagridBuilderTest.php @@ -64,6 +64,12 @@ protected function createDatagridBuilder() '', false ); + $aclManager = $this->getMockForAbstractClass( + 'Oro\Bundle\UserBundle\Acl\ManagerInterface', + array(), + '', + false + ); $filterFactory = $this->getMockForAbstractClass( 'Oro\Bundle\GridBundle\Filter\FilterFactoryInterface', array(), @@ -86,6 +92,7 @@ protected function createDatagridBuilder() return new SearchDatagridBuilder( $formFactory, $eventDispatcher, + $aclManager, $filterFactory, $sorterFactory, $actionFactory, diff --git a/src/Oro/Bundle/SoapBundle/Controller/Api/Rest/RestApiCrudInterface.php b/src/Oro/Bundle/SoapBundle/Controller/Api/Rest/RestApiCrudInterface.php index 5c382100161..3d3d6cc7b88 100644 --- a/src/Oro/Bundle/SoapBundle/Controller/Api/Rest/RestApiCrudInterface.php +++ b/src/Oro/Bundle/SoapBundle/Controller/Api/Rest/RestApiCrudInterface.php @@ -4,7 +4,7 @@ use Symfony\Component\HttpFoundation\Response; -interface RestApiCrudInterface +interface RestApiCrudInterface extends RestApiReadInterface { /** * Create item. @@ -13,23 +13,6 @@ interface RestApiCrudInterface */ public function handleCreateRequest(); - /** - * Get paginated items list. - * - * @param int $page - * @param int $limit - * @return Response - */ - public function handleGetListRequest($page, $limit); - - /** - * Get item by identifier. - * - * @param mixed $id - * @return mixed - */ - public function handleGetRequest($id); - /** * Update item. * diff --git a/src/Oro/Bundle/SoapBundle/Controller/Api/Rest/RestApiReadInterface.php b/src/Oro/Bundle/SoapBundle/Controller/Api/Rest/RestApiReadInterface.php new file mode 100644 index 00000000000..dfeaa1c0b35 --- /dev/null +++ b/src/Oro/Bundle/SoapBundle/Controller/Api/Rest/RestApiReadInterface.php @@ -0,0 +1,25 @@ +getManager(); - $items = $manager->getList($limit, $page); - - $result = array(); - foreach ($items as $item) { - $result[] = $this->getPreparedItem($item); - } - unset($items); - - return new Response(json_encode($result), $result ? Codes::HTTP_OK : Codes::HTTP_NOT_FOUND); - } - - /** - * GET single item - * - * @param mixed $id - * @return Response - */ - public function handleGetRequest($id) - { - $item = $this->getManager()->find($id); - - if ($item) { - $item = $this->getPreparedItem($item); - } - $responseData = $item ? json_encode($item) : ''; - return new Response($responseData, $item ? Codes::HTTP_OK : Codes::HTTP_NOT_FOUND); - } - /** * Edit entity * @@ -122,47 +78,6 @@ public function handleDeleteRequest($id) return $this->handleView($this->view(null, Codes::HTTP_NO_CONTENT)); } - /** - * Prepare entity for serialization - * - * @param mixed $entity - * @return array - */ - protected function getPreparedItem($entity) - { - $result = array(); - /** @var UnitOfWork $uow */ - $uow = $this->getDoctrine()->getManager()->getUnitOfWork(); - foreach ($uow->getOriginalEntityData($entity) as $field => $value) { - $accessors = array('get' . ucfirst($field), 'is' . ucfirst($field), 'has' . ucfirst($field)); - foreach ($accessors as $accessor) { - if (method_exists($entity, $accessor)) { - $value = $entity->$accessor(); - - $this->transformEntityField($field, $value); - $result[$field] = $value; - break; - } - } - } - return $result; - } - - /** - * Prepare entity field for serialization - * - * @param string $field - * @param mixed $value - */ - protected function transformEntityField($field, &$value) - { - if ($value instanceof Proxy && method_exists($value, '__toString')) { - $value = (string)$value; - } elseif ($value instanceof \DateTime) { - $value = $value->format('c'); - } - } - /** * Process form. * diff --git a/src/Oro/Bundle/SoapBundle/Controller/Api/Rest/RestGetController.php b/src/Oro/Bundle/SoapBundle/Controller/Api/Rest/RestGetController.php new file mode 100644 index 00000000000..191099a0a71 --- /dev/null +++ b/src/Oro/Bundle/SoapBundle/Controller/Api/Rest/RestGetController.php @@ -0,0 +1,104 @@ +getManager(); + $items = $manager->getList($limit, $page); + + $result = array(); + foreach ($items as $item) { + $result[] = $this->getPreparedItem($item); + } + unset($items); + + return new Response(json_encode($result), $result ? Codes::HTTP_OK : Codes::HTTP_NOT_FOUND); + } + + /** + * GET single item + * + * @param mixed $id + * @return Response + */ + public function handleGetRequest($id) + { + $item = $this->getManager()->find($id); + + if ($item) { + $item = $this->getPreparedItem($item); + } + $responseData = $item ? json_encode($item) : ''; + + return new Response($responseData, $item ? Codes::HTTP_OK : Codes::HTTP_NOT_FOUND); + } + + /** + * Prepare entity for serialization + * + * @param mixed $entity + * @return array + */ + protected function getPreparedItem($entity) + { + if ($entity instanceof Proxy && !$entity->__isInitialized()) { + $entity->__load(); + } + $result = array(); + if ($entity) { + /** @var UnitOfWork $uow */ + $uow = $this->getDoctrine()->getManager()->getUnitOfWork(); + foreach ($uow->getOriginalEntityData($entity) as $field => $value) { + $accessors = array('get' . ucfirst($field), 'is' . ucfirst($field), 'has' . ucfirst($field)); + foreach ($accessors as $accessor) { + if (method_exists($entity, $accessor)) { + $value = $entity->$accessor(); + + $this->transformEntityField($field, $value); + $result[$field] = $value; + break; + } + } + } + } + + return $result; + } + + /** + * Prepare entity field for serialization + * + * @param string $field + * @param mixed $value + */ + protected function transformEntityField($field, &$value) + { + if ($value instanceof Proxy && method_exists($value, '__toString')) { + $value = (string)$value; + } elseif ($value instanceof \DateTime) { + $value = $value->format('c'); + } + } +} diff --git a/src/Oro/Bundle/SoapBundle/Controller/Api/Soap/SoapApiCrudInterface.php b/src/Oro/Bundle/SoapBundle/Controller/Api/Soap/SoapApiCrudInterface.php index 38e591ff5bc..5d259211891 100644 --- a/src/Oro/Bundle/SoapBundle/Controller/Api/Soap/SoapApiCrudInterface.php +++ b/src/Oro/Bundle/SoapBundle/Controller/Api/Soap/SoapApiCrudInterface.php @@ -2,7 +2,7 @@ namespace Oro\Bundle\SoapBundle\Controller\Api\Soap; -interface SoapApiCrudInterface +interface SoapApiCrudInterface extends SoapApiReadInterface { /** * Create item. @@ -11,23 +11,6 @@ interface SoapApiCrudInterface */ public function handleCreateRequest(); - /** - * Get item by identifier. - * - * @param mixed $id - * @return object - */ - public function handleGetRequest($id); - - /** - * Get paginated items list. - * - * @param int $page - * @param int $limit - * @return \Traversable - */ - public function handleGetListRequest($page, $limit); - /** * Delete item. * diff --git a/src/Oro/Bundle/SoapBundle/Controller/Api/Soap/SoapApiReadInterface.php b/src/Oro/Bundle/SoapBundle/Controller/Api/Soap/SoapApiReadInterface.php new file mode 100644 index 00000000000..5a0c75e4991 --- /dev/null +++ b/src/Oro/Bundle/SoapBundle/Controller/Api/Soap/SoapApiReadInterface.php @@ -0,0 +1,23 @@ +getManager()->getList($limit, $page); - } - - /** - * {@inheritDoc} - */ - public function handleGetRequest($id) - { - return $this->getEntity($id); - } - /** * {@inheritDoc} */ @@ -46,7 +27,10 @@ public function handleUpdateRequest($id) */ public function handleCreateRequest() { - return $this->processForm($this->getManager()->createEntity()); + $entity = $this->getManager()->createEntity(); + $this->processForm($entity); + + return $this->getManager()->getEntityId($entity); } /** @@ -63,24 +47,6 @@ public function handleDeleteRequest($id) return true; } - /** - * Get entity by identifier. - * - * @param mixed $id - * @return object - * @throws \SoapFault - */ - protected function getEntity($id) - { - $entity = $this->getManager()->find($id); - - if (!$entity) { - throw new \SoapFault('NOT_FOUND', sprintf('Record #%u can not be found', $id)); - } - - return $entity; - } - /** * Form processing * @@ -133,22 +99,37 @@ protected function fixRequestAttributes($entity) return; } - $data = array(); - foreach ((array)$entityData as $field => $value) { - // special case for ordered arrays - if ($value instanceof \stdClass && isset($value->item) && is_array($value->item)) { - $value = (array) $value->item; - } + $data = $this->convertValueToArray($entityData); - if ($value instanceof Collection) { - $value = $value->toArray(); - } + $request->request->set($this->getForm()->getName(), $data); + } - if (!is_null($value)) { - $data[preg_replace('/[^\w+]+/i', '', $field)] = $value; + protected function convertValueToArray($value) + { + // special case for ordered arrays + if ($value instanceof \stdClass && isset($value->item) && is_array($value->item)) { + $value = (array) $value->item; + } + + if ($value instanceof Collection) { + $value = $value->toArray(); + } + + if (is_object($value)) { + $value = (array)$value; + } + + if (is_array($value)) { + $convertedValue = array(); + foreach ($value as $key => $item) { + $itemValue = $this->convertValueToArray($item); + if (!is_null($itemValue)) { + $convertedValue[preg_replace('/[^\w+]+/i', '', $key)] = $itemValue; + } } + $value = $convertedValue; } - $request->request->set($this->getForm()->getName(), $data); + return $value; } } diff --git a/src/Oro/Bundle/SoapBundle/Controller/Api/Soap/SoapGetController.php b/src/Oro/Bundle/SoapBundle/Controller/Api/Soap/SoapGetController.php new file mode 100644 index 00000000000..fb6a41a6ba9 --- /dev/null +++ b/src/Oro/Bundle/SoapBundle/Controller/Api/Soap/SoapGetController.php @@ -0,0 +1,43 @@ +getManager()->getList($limit, $page); + } + + /** + * {@inheritDoc} + */ + public function handleGetRequest($id) + { + return $this->getEntity($id); + } + + /** + * Get entity by identifier. + * + * @param mixed $id + * @return object + * @throws \SoapFault + */ + protected function getEntity($id) + { + $entity = $this->getManager()->find($id); + + if (!$entity) { + throw new \SoapFault('NOT_FOUND', sprintf('Record #%u can not be found', $id)); + } + + return $entity; + } +} diff --git a/src/Oro/Bundle/SoapBundle/Entity/Manager/ApiEntityManager.php b/src/Oro/Bundle/SoapBundle/Entity/Manager/ApiEntityManager.php index 2953cb8d183..81aa4e95898 100644 --- a/src/Oro/Bundle/SoapBundle/Entity/Manager/ApiEntityManager.php +++ b/src/Oro/Bundle/SoapBundle/Entity/Manager/ApiEntityManager.php @@ -31,10 +31,9 @@ class ApiEntityManager */ public function __construct($class, ObjectManager $om) { - $this->metadata = $om->getClassMetadata($class); - - $this->class = $this->metadata->getName(); $this->om = $om; + $this->metadata = $this->om->getClassMetadata($class); + $this->class = $this->metadata->getName(); } /** @@ -68,6 +67,25 @@ public function find($id) return $this->getRepository()->find($id); } + /** + * @param object $entity + * @return int + * @throws \InvalidArgumentException + */ + public function getEntityId($entity) + { + $className = $this->class; + if (!$entity instanceof $className) { + throw new \InvalidArgumentException('Expected instance of ' . $this->class); + } + + $idFields = $this->metadata->getIdentifierFieldNames(); + $idField = current($idFields); + $entityIds = $this->metadata->getIdentifierValues($entity); + + return $entityIds[$idField]; + } + /** * Return related repository * diff --git a/src/Oro/Bundle/SoapBundle/Tests/Unit/Entity/Manager/ApiEntityManagerTest.php b/src/Oro/Bundle/SoapBundle/Tests/Unit/Entity/Manager/ApiEntityManagerTest.php new file mode 100644 index 00000000000..fb70783f603 --- /dev/null +++ b/src/Oro/Bundle/SoapBundle/Tests/Unit/Entity/Manager/ApiEntityManagerTest.php @@ -0,0 +1,76 @@ +getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') + ->disableOriginalConstructor() + ->setMethods(array('getName')) + ->getMock(); + } + $metadata->expects($this->any()) + ->method('getName') + ->will($this->returnValue($class)); + + if (!$objectManager) { + $objectManager = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->setMethods(array('getClassMetadata')) + ->getMock(); + } + $objectManager->expects($this->any()) + ->method('getClassMetadata') + ->with($class) + ->will($this->returnValue($metadata)); + + + return new ApiEntityManager($class, $objectManager); + } + + public function testGetEntityId() + { + $className = 'Oro\Bundle\SoapBundle\Tests\Unit\Entity\Manager\Stub\Entity'; + + $entity = new Entity(); + $entity->id = 1; + $entity->name = 'entityName'; + + $metadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') + ->setConstructorArgs(array($className)) + ->setMethods(array('getIdentifierFieldNames', 'getIdentifierValues')) + ->getMock(); + $metadata->expects($this->once()) + ->method('getIdentifierFieldNames') + ->will($this->returnValue(array('id'))); + $metadata->expects($this->once()) + ->method('getIdentifierValues') + ->with($entity) + ->will($this->returnValue(array('id' => $entity->id))); + + $manager = $this->createApiEntityManager($className, $metadata); + $this->assertEquals($entity->id, $manager->getEntityId($entity)); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage xpected instance of \DateTime + */ + public function testGetEntityIdIncorrectInstance() + { + $manager = $this->createApiEntityManager('\DateTime'); + $manager->getEntityId(new Entity()); + } +} diff --git a/src/Oro/Bundle/SoapBundle/Tests/Unit/Entity/Manager/Stub/Entity.php b/src/Oro/Bundle/SoapBundle/Tests/Unit/Entity/Manager/Stub/Entity.php new file mode 100644 index 00000000000..02557f690cf --- /dev/null +++ b/src/Oro/Bundle/SoapBundle/Tests/Unit/Entity/Manager/Stub/Entity.php @@ -0,0 +1,16 @@ +registry->getManagerForClass($this->className); $this->queryBuilder = $em->getRepository($this->className)->createQueryBuilder($this->alias); - if (!$this->queryBuilder) { - throw new \LogicException('Can\'t create datagrid query. Query builder is not configured.'); - } - return new ResultsQuery($this->queryBuilder, $em, $this->mapper); } } diff --git a/src/Oro/Bundle/TagBundle/Datagrid/TagDatagridManager.php b/src/Oro/Bundle/TagBundle/Datagrid/TagDatagridManager.php index 26a308d9bc3..c335be2c3c2 100644 --- a/src/Oro/Bundle/TagBundle/Datagrid/TagDatagridManager.php +++ b/src/Oro/Bundle/TagBundle/Datagrid/TagDatagridManager.php @@ -30,19 +30,6 @@ protected function getProperties() */ protected function configureFields(FieldDescriptionCollection $fieldsCollection) { - $fieldId = new FieldDescription(); - $fieldId->setName('id'); - $fieldId->setOptions( - array( - 'type' => FieldDescriptionInterface::TYPE_INTEGER, - 'label' => $this->translate('ID'), - 'field_name' => 'id', - 'filter_type' => FilterInterface::TYPE_NUMBER, - 'show_column' => false - ) - ); - $fieldsCollection->add($fieldId); - $fieldName = new FieldDescription(); $fieldName->setName('tag'); $fieldName->setOptions( diff --git a/src/Oro/Bundle/TagBundle/DependencyInjection/Configuration.php b/src/Oro/Bundle/TagBundle/DependencyInjection/Configuration.php index 0f7bd79c33f..451c6e96d6e 100644 --- a/src/Oro/Bundle/TagBundle/DependencyInjection/Configuration.php +++ b/src/Oro/Bundle/TagBundle/DependencyInjection/Configuration.php @@ -5,11 +5,6 @@ use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; -/** - * This is the class that validates and merges configuration from your app/config files - * - * To learn more see {@link http://symfony.com/doc/current/cookbook/bundles/extension.html#cookbook-bundles-extension-config-class} - */ class Configuration implements ConfigurationInterface { /** diff --git a/src/Oro/Bundle/TagBundle/Entity/Repository/TagRepository.php b/src/Oro/Bundle/TagBundle/Entity/Repository/TagRepository.php new file mode 100644 index 00000000000..7b4cfc98d7a --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Entity/Repository/TagRepository.php @@ -0,0 +1,66 @@ +createQueryBuilder('t') + ->select('t') + ->innerJoin('t.tagging', 't2', Join::WITH, 't2.recordId = :recordId AND t2.entityName = :entityName') + ->setParameter('recordId', $resource->getTaggableId()) + ->setParameter('entityName', get_class($resource)); + + if (!is_null($createdBy)) { + $qb->where('t2.createdBy ' . ($all ? '!=' : '=') . ' :createdBy') + ->setParameter('createdBy', $createdBy); + } + + return $qb->getQuery()->getResult(); + } + + /** + * Remove tagging related to tags by params + * + * @param array|int $tagIds + * @param string $entityName + * @param int $recordId + * @param null|int $createdBy + * @return array + */ + public function deleteTaggingByParams($tagIds, $entityName, $recordId, $createdBy = null) + { + $builder = $this->_em->createQueryBuilder(); + $builder + ->delete('OroTagBundle:Tagging', 't') + ->where('t.entityName = :entityName') + ->setParameter('entityName', $entityName) + ->andWhere('t.recordId = :recordId') + ->setParameter('recordId', $recordId); + + if (!empty($tagIds)) { + $builder->andWhere($builder->expr()->in('t.tag', $tagIds)); + } + + if (!is_null($createdBy)) { + $builder->andWhere('t.createdBy = :createdBy') + ->setParameter('createdBy', $createdBy); + } + + return $builder->getQuery()->getResult(); + } +} diff --git a/src/Oro/Bundle/TagBundle/Entity/Tag.php b/src/Oro/Bundle/TagBundle/Entity/Tag.php index b63f56eba45..b0aef41ce53 100644 --- a/src/Oro/Bundle/TagBundle/Entity/Tag.php +++ b/src/Oro/Bundle/TagBundle/Entity/Tag.php @@ -4,15 +4,23 @@ use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\PersistentCollection; +use Doctrine\Common\Collections\ArrayCollection; use Oro\Bundle\UserBundle\Entity\User; +use Oro\Bundle\EntityConfigBundle\Metadata\Annotation\Config; /** * Tag * * @ORM\Table(name="oro_tag_tag") * @ORM\HasLifecycleCallbacks - * @ORM\Entity + * @ORM\Entity(repositoryClass="Oro\Bundle\TagBundle\Entity\Repository\TagRepository") + * @Config( + * defaultValues={ + * "entity"={"label"="Tag", "plural_label"="Tags"}, + * "ownership"={"owner_type"="USER"} + * } + * ) */ class Tag implements ContainAuthorInterface, ContainUpdaterInterface { @@ -46,7 +54,7 @@ class Tag implements ContainAuthorInterface, ContainUpdaterInterface protected $updated; /** - * @ORM\OneToMany(targetEntity="Tagging", mappedBy="tag", fetch="EAGER") + * @ORM\OneToMany(targetEntity="Tagging", mappedBy="tag", fetch="LAZY") */ protected $tagging; @@ -64,6 +72,13 @@ class Tag implements ContainAuthorInterface, ContainUpdaterInterface */ protected $updatedBy; + /** + * @var User + * @ORM\ManyToOne(targetEntity="Oro\Bundle\UserBundle\Entity\User") + * @ORM\JoinColumn(name="user_owner_id", referencedColumnName="id", onDelete="SET NULL") + */ + protected $owner; + /** * Constructor * @@ -72,6 +87,7 @@ class Tag implements ContainAuthorInterface, ContainUpdaterInterface public function __construct($name = null) { $this->setName($name); + $this->tagging = new ArrayCollection(); $this->setCreatedAt(new \DateTime('now', new \DateTimeZone('UTC'))); $this->setUpdatedAt(new \DateTime('now', new \DateTimeZone('UTC'))); @@ -220,4 +236,23 @@ public function doUpdate() { $this->updated = new \DateTime('now', new \DateTimeZone('UTC')); } + + /** + * @return User + */ + public function getOwner() + { + return $this->owner; + } + + /** + * @param User $owningUser + * @return Tag + */ + public function setOwner($owningUser) + { + $this->owner = $owningUser; + + return $this; + } } diff --git a/src/Oro/Bundle/TagBundle/Entity/TagManager.php b/src/Oro/Bundle/TagBundle/Entity/TagManager.php index b77abd4d4f9..4f453f8f66a 100644 --- a/src/Oro/Bundle/TagBundle/Entity/TagManager.php +++ b/src/Oro/Bundle/TagBundle/Entity/TagManager.php @@ -2,17 +2,23 @@ namespace Oro\Bundle\TagBundle\Entity; -use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\ORM\EntityManager; -use Doctrine\ORM\Query\Expr\Join; use Doctrine\ORM\Query\Expr; +use Doctrine\ORM\EntityManager; +use Doctrine\Common\Collections\ArrayCollection; -use Oro\Bundle\SearchBundle\Engine\ObjectMapper; -use Symfony\Component\Security\Core\SecurityContext; +use Symfony\Bundle\FrameworkBundle\Routing\Router; use Symfony\Component\Security\Core\SecurityContextInterface; +use Oro\Bundle\UserBundle\Acl\Manager; +use Oro\Bundle\UserBundle\Entity\User; +use Oro\Bundle\SearchBundle\Engine\ObjectMapper; +use Oro\Bundle\TagBundle\Entity\Repository\TagRepository; + class TagManager { + const ACL_RESOURCE_REMOVE_ID_KEY = 'oro_tag_unassign_global'; + const ACL_RESOURCE_CREATE_ID_KEY = 'oro_tag_create'; + const ACL_RESOURCE_ASSIGN_ID_KEY = 'oro_tag_assign_unassign'; /** * @var EntityManager */ @@ -38,25 +44,33 @@ class TagManager */ protected $securityContext; - public function __construct(EntityManager $em, $tagClass, $taggingClass, ObjectMapper $mapper, SecurityContextInterface $securityContext) - { + /** + * @var Manager + */ + protected $aclManager; + + /** + * @var Router + */ + protected $router; + + public function __construct( + EntityManager $em, + $tagClass, + $taggingClass, + ObjectMapper $mapper, + SecurityContextInterface $securityContext, + Manager $aclManager, + Router $router + ) { $this->em = $em; $this->tagClass = $tagClass; $this->taggingClass = $taggingClass; $this->mapper = $mapper; $this->securityContext = $securityContext; - } - - /** - * Adds a tag on the given taggable resource - * - * @param Tag $tag Tag object - * @param Taggable $resource Taggable resource - */ - public function addTag(Tag $tag, Taggable $resource) - { - $resource->getTags()->add($tag); + $this->aclManager = $aclManager; + $this->router = $router; } /** @@ -65,40 +79,15 @@ public function addTag(Tag $tag, Taggable $resource) * @param Tag[] $tags Array of Tag objects * @param Taggable $resource Taggable resource */ - public function addTags(array $tags, Taggable $resource) + public function addTags($tags, Taggable $resource) { foreach ($tags as $tag) { if ($tag instanceof Tag) { - $this->addTag($tag, $resource); + $resource->getTags()->add($tag); } } } - /** - * Removes an existant tag on the given taggable resource - * - * @param Tag $tag Tag object - * @param Taggable $resource Taggable resource - * @return Boolean - */ - public function removeTag(Tag $tag, Taggable $resource) - { - return $resource->getTags()->removeElement($tag); - } - - /** - * Replaces all current tags on the given taggable resource - * - * @param Tag[] $tags Array of Tag objects - * @param Taggable $resource Taggable resource - */ - public function replaceTags(array $tags, Taggable $resource) - { - $resource->setTags(new ArrayCollection()); - - $this->addTags($tags, $resource); - } - /** * Loads or creates multiples tags from a list of tag names * @@ -111,20 +100,17 @@ public function loadOrCreateTags(array $names) return array(); } + array_walk( + $names, + function (&$item) { + $item = trim($item); + } + ); $names = array_unique($names); - - $builder = $this->em->createQueryBuilder(); - - $tags = $builder - ->select('t') - ->from($this->tagClass, 't') - - ->where($builder->expr()->in('t.name', $names)) - - ->getQuery() - ->getResult(); + $tags = $this->em->getRepository($this->tagClass)->findBy(array('name' => $names)); $loadedNames = array(); + /** @var Tag $tag */ foreach ($tags as $tag) { $loadedNames[] = $tag->getName(); } @@ -133,138 +119,208 @@ public function loadOrCreateTags(array $names) if (sizeof($missingNames)) { foreach ($missingNames as $name) { $tag = $this->createTag($name); - $this->em->persist($tag); $tags[] = $tag; } - - $this->em->flush(); } return $tags; } /** - * Saves tags for the given taggable resource + * Prepare array * - * @param Taggable $resource Taggable resource + * @param Taggable $entity + * @param ArrayCollection|null $tags + * @return array */ - public function saveTagging(Taggable $resource) + public function getPreparedArray(Taggable $entity, $tags = null) { - $oldTags = $this->getTagging($resource); - $newTags = $resource->getTags(); - - if (!isset($newTags['all'], $newTags['owner'])) { - return; + if (is_null($tags)) { + $this->loadTagging($entity); + $tags = $entity->getTags(); } + $result = array(); + + /** @var Tag $tag */ + foreach ($tags as $tag) { + $entry = array( + 'name' => $tag->getName() + ); + if (!$tag->getId()) { + $entry = array_merge( + $entry, + array( + 'id' => $tag->getName(), + 'url' => false, + 'owner' => true + ) + ); + } else { + $entry = array_merge( + $entry, + array( + 'id' => $tag->getId(), + 'url' => $this->router->generate('oro_tag_search', array('id' => $tag->getId())), + 'owner' => false + ) + ); + } - $tagsToAdd = new ArrayCollection($newTags['owner']); + $taggingCollection = $tag->getTagging()->filter( + function (Tagging $tagging) use ($entity) { + // only use tagging entities that related to current entity + return $tagging->getEntityName() == get_class($entity) + && $tagging->getRecordId() == $entity->getTaggableId(); + } + ); - if ($oldTags !== null and is_array($oldTags) and !empty($oldTags)) { - $tagsToRemove = array(); + /** @var Tagging $tagging */ + foreach ($taggingCollection as $tagging) { + if ($this->getUser()->getId() == $tagging->getCreatedBy()->getId()) { + $entry['owner'] = true; + } + } + + $entry['moreOwners'] = $taggingCollection->count() > 1; - foreach ($oldTags as $oldTag) { - $callback = function ($index, $newTag) use ($oldTag) { - return $newTag->getName() == $oldTag->getName(); - }; + $result[] = $entry; + } - if ($tagsToAdd->exists($callback)) { - $tagsToAdd->removeElement($oldTag); - } else { - $tagsToRemove[] = $oldTag->getId(); + return $result; + } + + /** + * Saves tags for the given taggable resource + * + * @param Taggable $resource Taggable resource + */ + public function saveTagging(Taggable $resource) + { + $oldTags = $this->getTagging($resource, $this->getUser()->getId()); + $newTags = $resource->getTags(); + if (isset($newTags['all'], $newTags['owner'])) { + $newOwnerTags = new ArrayCollection($newTags['owner']); + $newAllTags = new ArrayCollection($newTags['all']); + + $manager = $this; + $tagsToAdd = $newOwnerTags->filter( + function ($tag) use ($oldTags, $manager) { + return !$oldTags->exists($manager->compareCallback($tag)); } + ); + $tagsToDelete = $oldTags->filter( + function ($tag) use ($newOwnerTags, $manager) { + return !$newOwnerTags->exists($manager->compareCallback($tag)); + } + ); + + if (!$tagsToDelete->isEmpty() + && $this->aclManager->isResourceGranted(self::ACL_RESOURCE_ASSIGN_ID_KEY) + ) { + $this->deleteTaggingByParams( + $tagsToDelete, + get_class($resource), + $resource->getTaggableId(), + $this->getUser()->getId() + ); } - if (sizeof($tagsToRemove)) { - $builder = $this->em->createQueryBuilder(); - $builder - ->delete($this->taggingClass, 't') - ->where($builder->expr()->in('t.tag', $tagsToRemove)) - ->andWhere('t.entityName = :entityName') - ->setParameter('entityName', get_class($resource)) - ->andWhere('t.recordId = :recordId') - ->setParameter('recordId', $resource->getTaggableId()) - ->getQuery() - ->getResult(); + // process if current user allowed to remove other's tag links + if ($this->aclManager->isResourceGranted(self::ACL_RESOURCE_REMOVE_ID_KEY)) { + // get 'not mine' taggings + $oldTags = $this->getTagging($resource, $this->getUser()->getId(), true); + $tagsToDelete = $oldTags->filter( + function ($tag) use ($newAllTags, $manager) { + return !$newAllTags->exists($manager->compareCallback($tag)); + } + ); + if (!$tagsToDelete->isEmpty()) { + $this->deleteTaggingByParams( + $tagsToDelete, + get_class($resource), + $resource->getTaggableId() + ); + } } - } - foreach ($tagsToAdd as $tag) { - $this->em->persist($tag); + foreach ($tagsToAdd as $tag) { + if (!$this->aclManager->isResourceGranted(self::ACL_RESOURCE_ASSIGN_ID_KEY) + || (!$this->aclManager->isResourceGranted(self::ACL_RESOURCE_CREATE_ID_KEY) && !$tag->getId()) + ) { + // skip tags that have not ID because user not granted to create tags + continue; + } - $alias = $this->mapper->getEntityConfig(get_class($resource)); + $this->em->persist($tag); - $tagging = $this->createTagging($tag, $resource) - ->setAlias($alias['alias']); + $alias = $this->mapper->getEntityConfig(get_class($resource)); - $this->em->persist($tagging); - } + $tagging = $this->createTagging($tag, $resource) + ->setAlias($alias['alias']); + + $this->em->persist($tagging); + } - if (count($tagsToAdd)) { - $this->em->flush(); + if (!$tagsToAdd->isEmpty()) { + $this->em->flush(); + } } } /** - * Loads all tags for the given taggable resource - * - * @param Taggable $resource Taggable resource + * @param Tag $tag + * @return callable */ - public function loadTagging(Taggable $resource) + public function compareCallback($tag) { - $tags = $this->getTagging($resource); - $this->replaceTags($tags, $resource); + return function ($index, $item) use ($tag) { + /** @var Tag $item */ + return $item->getName() == $tag->getName(); + }; } /** - * Gets all tags for the given taggable resource + * Loads all tags for the given taggable resource * - * @param Taggable $resource Taggable resource - * @return array + * @param Taggable $resource Taggable resource + * @return $this */ - protected function getTagging(Taggable $resource) + public function loadTagging(Taggable $resource) { - $query = $this->em - ->createQueryBuilder() - - ->select('t') - ->from($this->tagClass, 't') - - ->innerJoin('t.tagging', 't2', Join::WITH, 't2.recordId = :recordId AND t2.entityName = :entityName') - ->setParameter('recordId', $resource->getTaggableId()) - ->setParameter('entityName', get_class($resource)) - ->getQuery(); + $tags = $this->getTagging($resource); + $this->addTags($tags, $resource); - return $query->getResult(); + return $this; } /** - * Deletes all tagging records for the given taggable resource + * Remove tagging related to tags by params * - * @param Taggable $resource - * @return $this + * @param array|ArrayCollection|int $tagIds + * @param string $entityName + * @param int $recordId + * @param null|int $createdBy + * @return array */ - public function deleteTagging(Taggable $resource) + public function deleteTaggingByParams($tagIds, $entityName, $recordId, $createdBy = null) { - $taggingList = $this->em->createQueryBuilder() - ->select('t') - ->from($this->taggingClass, 't') - - ->where('t.entityName = :entityName') - ->setParameter('entityName', get_class($resource)) - - ->andWhere('t.recordId = :id') - ->setParameter('id', $resource->getTaggableId()) - - ->getQuery() - ->getResult(); - - foreach ($taggingList as $tagging) { - $this->em->remove($tagging); - $this->em->flush($tagging); + /** @var TagRepository $repository */ + $repository = $this->em->getRepository($this->tagClass); + + if (!$tagIds) { + $tagIds = array(); + } elseif ($tagIds instanceof ArrayCollection) { + $tagIds = array_map( + function ($item) { + /** @var Tag $item */ + return $item->getId(); + }, + $tagIds->toArray() + ); } - return $this; + return $repository->deleteTaggingByParams($tagIds, $entityName, $recordId, $createdBy); } /** @@ -273,7 +329,7 @@ public function deleteTagging(Taggable $resource) * @param string $name Tag name * @return Tag */ - protected function createTag($name) + private function createTag($name) { return new $this->tagClass($name); } @@ -285,8 +341,34 @@ protected function createTag($name) * @param Taggable $resource Taggable resource object * @return Tagging */ - protected function createTagging(Tag $tag, Taggable $resource) + private function createTagging(Tag $tag, Taggable $resource) { return new $this->taggingClass($tag, $resource); } + + /** + * Gets all tags for the given taggable resource + * + * @param Taggable $resource Taggable resource + * @param null|int $createdBy + * @param bool $all + * @return ArrayCollection + */ + private function getTagging(Taggable $resource, $createdBy = null, $all = false) + { + /** @var TagRepository $repository */ + $repository = $this->em->getRepository($this->tagClass); + + return new ArrayCollection($repository->getTagging($resource, $createdBy, $all)); + } + + /** + * Return current user + * + * @return User + */ + private function getUser() + { + return $this->securityContext->getToken()->getUser(); + } } diff --git a/src/Oro/Bundle/TagBundle/Entity/Tagging.php b/src/Oro/Bundle/TagBundle/Entity/Tagging.php index 488feda7850..4e03677cf09 100644 --- a/src/Oro/Bundle/TagBundle/Entity/Tagging.php +++ b/src/Oro/Bundle/TagBundle/Entity/Tagging.php @@ -9,7 +9,9 @@ /** * @ORM\Table( * name="oro_tag_tagging", - * uniqueConstraints={@ORM\UniqueConstraint(name="tagging_idx", columns={"tag_id", "entity_name", "record_id", "created_by"})} + * uniqueConstraints={ + * @ORM\UniqueConstraint(name="tagging_idx", columns={"tag_id", "entity_name", "record_id", "created_by"}) + * } * ) * @ORM\Entity */ @@ -25,7 +27,7 @@ class Tagging implements ContainAuthorInterface protected $id; /** - * @ORM\ManyToOne(targetEntity="Tag", inversedBy="tagging") + * @ORM\ManyToOne(targetEntity="Tag", inversedBy="tagging", cascade="ALL") * @ORM\JoinColumn(name="tag_id", referencedColumnName="id", onDelete="CASCADE") **/ protected $tag; diff --git a/src/Oro/Bundle/TagBundle/EventListener/TagListener.php b/src/Oro/Bundle/TagBundle/EventListener/TagListener.php index 3c3cd621955..be71da55681 100644 --- a/src/Oro/Bundle/TagBundle/EventListener/TagListener.php +++ b/src/Oro/Bundle/TagBundle/EventListener/TagListener.php @@ -31,7 +31,11 @@ public function preRemove(LifecycleEventArgs $args) } if (($resource = $args->getEntity()) and $resource instanceof Taggable) { - $this->manager->deleteTagging($resource); + $this->manager->deleteTaggingByParams( + null, + get_class($resource), + $resource->getTaggableId() + ); } } diff --git a/src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php b/src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php new file mode 100644 index 00000000000..59ec7eb6309 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Form/EventSubscriber/TagSubscriber.php @@ -0,0 +1,119 @@ +manager = $manager; + $this->transformer = $transformer; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() + { + return array( + FormEvents::PRE_SET_DATA => 'preSet', + FormEvents::PRE_SUBMIT => 'preSubmit' + ); + } + + /** + * Loads tagging and transform it to view data + * + * @param FormEvent $event + */ + public function preSet(FormEvent $event) + { + $entity = $event->getForm()->getParent()->getData(); + + if (!$entity instanceof Taggable) { + // do nothing if new entity or some error + return; + } + + $tags = $this->manager->getPreparedArray($entity); + $ownTags = array_filter( + $tags, + function ($item) { + return isset($item['owner']) && $item['owner']; + } + ); + + // pass entity to transformer + $this->transformer->setEntity($entity); + + $event->setData( + array( + 'autocomplete' => null, + 'all' => json_encode($tags), + 'owner' => json_encode($ownTags) + ) + ); + } + + /** + * Transform submitted data to model data + * + * @param FormEvent $event + */ + public function preSubmit(FormEvent $event) + { + $values = $event->getData(); + $entities = array( + 'all' => array(), + 'owner' => array() + ); + + foreach (array_keys($entities) as $type) { + if (isset($values[$type]) && !empty($values[$type])) { + try { + if (!is_array($values[$type])) { + $values[$type] = json_decode($values[$type]); + } + $names[$type] = array(); + foreach ($values[$type] as $value) { + if (!empty($value->name)) { + // new tag + $names[$type][] = $value->name; + } + } + + $entities[$type] = $this->manager->loadOrCreateTags($names[$type]); + } catch (\Exception $e) { + $entities[$type] = array(); + } + } + } + + $event->setData($entities); + } +} diff --git a/src/Oro/Bundle/TagBundle/Form/TagsTransformer.php b/src/Oro/Bundle/TagBundle/Form/TagsTransformer.php deleted file mode 100644 index 05aeb60e5ce..00000000000 --- a/src/Oro/Bundle/TagBundle/Form/TagsTransformer.php +++ /dev/null @@ -1,106 +0,0 @@ - array(), - 'owner' => array() - ); - - if (!isset($values['all'], $values['owner'])) { - return $values; - } - - foreach (array_keys($entities) as $type) { - if (!is_array($values[$type])) { - $values[$type] = explode(',', $values[$type]); - } - - $newValues[$type] = array_filter( - $values[$type], - function ($item) { - return !intval($item) && !empty($item); - } - ); - - $values[$type] = array_filter( - $values[$type], - function ($item) { - return intval($item); - } - ); - - if ($values[$type]) { - $entities[$type] = $this->loadEntitiesByIds($values[$type]); - } - - if ($newValues[$type]) { - $entities[$type] = array_merge($entities[$type], $this->tagManager->loadOrCreateTags($newValues[$type])); - } - - if (count($entities[$type]) !== count($values[$type]) + count($newValues[$type])) { - throw new TransformationFailedException('Could not find all entities for the given IDs'); - } - } - - return $entities; - } - - /** - * Load entities by array of ids - * - * @param array $ids - * @return array - * @throws UnexpectedTypeException if query builder callback returns invalid type - */ - protected function loadEntitiesByIds(array $ids) - { - $repository = $this->em->getRepository($this->className); - if ($this->queryBuilderCallback) { - /** @var $qb QueryBuilder */ - $qb = call_user_func($this->queryBuilderCallback, $repository, $ids); - if (!$qb instanceof QueryBuilder) { - throw new UnexpectedTypeException($qb, 'Doctrine\ORM\QueryBuilder'); - } - } else { - $qb = $repository->createQueryBuilder('e'); - $qb->where(sprintf('e.%s IN (:ids)', $this->propertyPath)) - ->setParameter('ids', $ids); - } - - return $qb->getQuery()->execute(); - } - - public function setTagManager(TagManager $tagManager) - { - $this->tagManager = $tagManager; - } -} diff --git a/src/Oro/Bundle/TagBundle/Form/Transformer/TagTransformer.php b/src/Oro/Bundle/TagBundle/Form/Transformer/TagTransformer.php new file mode 100644 index 00000000000..d7526234df3 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Form/Transformer/TagTransformer.php @@ -0,0 +1,64 @@ +manager = $manager; + } + + /** + * {@inheritdoc} + */ + public function reverseTransform($value) + { + return $value; + } + + /** + * {@inheritdoc} + */ + public function transform($value) + { + // transform to JSON if we have array of entities + // needed to correct rendering form if validation not passed + if (is_array($value)) { + $result = array(); + if ($this->entity) { + $result = $this->manager->getPreparedArray($this->entity, new ArrayCollection($value)); + } + $value = json_encode($result); + } + + return $value; + } + + /** + * Setter for entity object + * + * @param Taggable $entity + */ + public function setEntity(Taggable $entity) + { + $this->entity = $entity; + } +} diff --git a/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php b/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php new file mode 100644 index 00000000000..9b3f7e41627 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Form/Type/TagAutocompleteType.php @@ -0,0 +1,43 @@ +setDefaults( + array( + 'configs' => array( + 'placeholder' => 'oro.tag.form.choose_or_create_tag', + 'extra_config' => 'multi_autocomplete', + 'multiple' => true + ), + 'autocomplete_alias' => 'tags', + ) + ); + } + + /** + * {@inheritdoc} + */ + public function getName() + { + return 'oro_tag_autocomplete'; + } + + /** + * {@inheritdoc} + */ + public function getParent() + { + return 'oro_jqueryselect2_hidden'; + } +} diff --git a/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php b/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php index 0dbd89fa241..0c5ced90ada 100644 --- a/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php +++ b/src/Oro/Bundle/TagBundle/Form/Type/TagSelectType.php @@ -1,49 +1,39 @@ om = $om; - $this->tagManager = $tagManager; + $this->subscriber = $subscriber; + $this->transformer = $transformer; } + /** + * {@inheritdoc} + */ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults( array( - 'configs' => array( - 'placeholder' => 'oro.tag.form.choose_tag', - 'multiple' => true, - 'tokenSeparators' => array(',', ' '), - 'tags' => true, - 'extra_config' => 'multi_autocomplete', - ), - 'autocomplete_alias' => 'tags', + 'required' => false, ) ); } @@ -53,15 +43,26 @@ public function setDefaultOptions(OptionsResolverInterface $resolver) */ public function buildForm(FormBuilderInterface $builder, array $options) { - $transformer = new TagsTransformer($this->om, 'Oro\Bundle\TagBundle\Entity\Tag'); - $transformer->setTagManager($this->tagManager); + $builder->addEventSubscriber($this->subscriber); - $builder->addModelTransformer($transformer); - } + $builder->add( + 'autocomplete', + 'oro_tag_autocomplete' + ); - public function getParent() - { - return 'oro_jqueryselect2_hidden'; + $builder->add( + $builder->create( + 'all', + 'hidden' + )->addViewTransformer($this->transformer) + ); + + $builder->add( + $builder->create( + 'owner', + 'hidden' + )->addViewTransformer($this->transformer) + ); } /** diff --git a/src/Oro/Bundle/TagBundle/Resources/config/assets.yml b/src/Oro/Bundle/TagBundle/Resources/config/assets.yml index 9f352b28ea9..601a3786e7a 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/assets.yml @@ -6,5 +6,5 @@ js: 'tags': - '@OroTagBundle/Resources/public/js/models/tag.js' - '@OroTagBundle/Resources/public/js/views/tag.js' + - '@OroTagBundle/Resources/public/js/views/tag_update.js' - '@OroTagBundle/Resources/public/js/collections/tag.js' - - '@OroTagBundle/Resources/public/js/views/select2.js' diff --git a/src/Oro/Bundle/TagBundle/Resources/config/datagrid.yml b/src/Oro/Bundle/TagBundle/Resources/config/datagrid.yml index fad33ad2e87..cd1cbb23d2b 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/datagrid.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/datagrid.yml @@ -11,7 +11,7 @@ services: - name: oro_grid.datagrid.manager datagrid_name: tags entity_name: %oro_tag.tag.entity.class% - entity_hint: tags + entity_hint: tag route_name: oro_tag_index oro_tag.datagrid_results.datagrid_manager: @@ -20,7 +20,7 @@ services: tags: - name: oro_grid.datagrid.manager datagrid_name: %oro_tag.datagrid_results.grid_name% - entity_hint: results + entity_hint: result query_factory: oro_tag.datagrid_results.query_factory route_name: oro_tag_search_ajax diff --git a/src/Oro/Bundle/TagBundle/Resources/config/services.yml b/src/Oro/Bundle/TagBundle/Resources/config/services.yml index 4e912b5fa35..27beaa4f7f3 100644 --- a/src/Oro/Bundle/TagBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/TagBundle/Resources/config/services.yml @@ -13,6 +13,9 @@ parameters: oro_tag.autocomplete.tag.search_handler.class: Oro\Bundle\FormBundle\Autocomplete\SearchHandler oro_tag.form.type.tag_select.class: Oro\Bundle\TagBundle\Form\Type\TagSelectType + oro_tag.form.type.tag_autocomplete.class: Oro\Bundle\TagBundle\Form\Type\TagAutocompleteType + oro_tag.form.subscriber.tag_select.class: Oro\Bundle\TagBundle\Form\EventSubscriber\TagSubscriber + oro_tag.form.transformer.tag_select.class: Oro\Bundle\TagBundle\Form\Transformer\TagTransformer oro_tag.provider.search_provider.class: Oro\Bundle\TagBundle\Provider\SearchProvider @@ -27,6 +30,8 @@ services: - %oro_tag.tagging.entity.class% - @oro_search.mapper - @security.context + - @oro_user.acl_manager + - @router oro_tag.docrine.event.listener: class: %oro_tag.tag_listener.class% @@ -105,21 +110,33 @@ services: - { name: oro_form.autocomplete.search_handler, alias: tags, acl_resource: oro_tag_assign_unassign } # Autocomplete form type + oro_tag.form.type.tag_autocomplete: + class: %oro_tag.form.type.tag_autocomplete.class% + tags: + - { name: form.type, alias: oro_tag_autocomplete } + oro_tag.form.type.tag_select: class: %oro_tag.form.type.tag_select.class% arguments: - - @doctrine.orm.entity_manager - - @oro_tag.tag.manager + - @oro_tag.form.subscriber.tag_select + - @oro_tag.form.transformer.tag_select tags: - { name: form.type, alias: oro_tag_select } + oro_tag.form.subscriber.tag_select: + class: %oro_tag.form.subscriber.tag_select.class% + arguments: + - @oro_tag.tag.manager + - @oro_tag.form.transformer.tag_select + + oro_tag.form.transformer.tag_select: + class: %oro_tag.form.transformer.tag_select.class% + arguments: [@oro_tag.tag.manager] + # Twig extension oro_tag.twig.tag.extension: class: %oro_tag.twig.tag.extension.class% arguments: - @oro_tag.tag.manager - - @router - - @security.context - - @oro_user.acl_manager tags: - { name: twig.extension } diff --git a/src/Oro/Bundle/TagBundle/Resources/public/css/tag.css b/src/Oro/Bundle/TagBundle/Resources/public/css/tag.css index f92f77723f3..68cf2ee6cfe 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/css/tag.css +++ b/src/Oro/Bundle/TagBundle/Resources/public/css/tag.css @@ -1,7 +1,8 @@ +#tags-overlay #tag-list, .tag-view #tag-list { margin-bottom: 0; } -.tag-view #tag-sort-actions { +#tag-sort-actions { margin-top: 5px; } #tag-sort-actions a.active:hover, @@ -10,15 +11,20 @@ text-decoration: none; color: #777777; } +#tags-overlay #tag-list span.label.with-button { + position: relative; + padding-right: 17px; +} .select2-search-choice-close { display: block; width: 12px; height: 13px; position: absolute; right: 3px; - top: 4px; + top: 3px; font-size: 1px; outline: none; background: url("/bundles/orotag/img/select2.png") right top no-repeat; } + diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js b/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js index f5f6378e713..77651539e2d 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/collections/tag.js @@ -22,12 +22,45 @@ Oro.Tags.TagCollection = Backbone.Collection.extend({ return new Oro.Tags.TagCollection(filtered); }, - toArray: function() { - var tagArray = []; - _.each(this.models, function(tag) { - tagArray.push(tag.attributes); - }); + /** + * Used for adding item on tag_update view + * + * @param {Object} value + */ + addItem: function(value) { + // check if exists tag + var exist = this.where({name: value.name}); + if (exist.length && exist[0].get('owner') == false) { + // adding to owner + exist[0].set('owner', true); + this.trigger('add'); + + return; + } + + var tag = new this.model({id: value.id, name: value.name, owner: true, notSaved: true}); + + this.add(tag); + }, - return tagArray; + /** + * Remove item from collection, or uncheck "owner" if filter is not in global mdoe + * + * @param {String}|{Number} id + * @param {String} filterState + */ + removeItem: function(id, filterState) { + var model = this.where({'id': id}); + if (model.length) { + model = model[0]; + if (filterState == 'owner' && model.get('owner') === true && model.get('moreOwners') === true) { + model.set('owner', false); + + this.trigger('remove'); + + return; + } + this.remove(model); + } } }); diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/models/tag.js b/src/Oro/Bundle/TagBundle/Resources/public/js/models/tag.js index e57a8411c98..ae7669de5b4 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/models/tag.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/models/tag.js @@ -3,8 +3,10 @@ Oro.Tags = Oro.Tags || {}; Oro.Tags.Tag = Backbone.Model.extend({ defaults: { - owner : false, - url : '', - name : '' + owner : false, + notSaved : false, + moreOwners: false, + url : '', + name : '' } }); diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js b/src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js deleted file mode 100644 index 2e1961d38b1..00000000000 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/views/select2.js +++ /dev/null @@ -1,86 +0,0 @@ -Oro = Oro || {}; -Oro.Tags = Oro.Tags || {}; - -Oro.Tags.Select2View = Oro.Tags.TagView.extend({ - options: { - tagInputId: null, - tags: null - }, - - /** - * Constructor - */ - initialize: function() { - this.collection = new Oro.Tags.TagCollection(); - this.ownCollection = new Oro.Tags.TagCollection(); - - this.listenTo(this.getCollection(), 'reset', this.render); - this.listenTo(this.getCollection('owner'), 'reset', this.render); - this.listenTo(this, 'filter', this.render); - - $('#tag-sort-actions a').click(_.bind(this.filter, this)); - - self = this; - $(this.options.tagInputId).on("change", this.updateHiddenInputs); - - if (this.options.tags != null) { - this.getCollection().reset(this.options.tags); - this.getCollection('owner').reset(this.getCollection().getFilteredCollection('owner').models); - } - }, - - getCollection: function(type) { - if (type == undefined || type == 'all') { - return this.collection; - } else if (type == 'owner') { - return this.ownCollection; - } else { - return {}; - } - }, - - updateHiddenInputs: function(event) { - var owner = self.options.filter == undefined ? 'all' : self.options.filter; - - if (event && event.added) { - event.added.owner = true; - owner = 'owner'; - self.getCollection('owner').add(event.added); - } - else if (event && event.removed) { - self.getCollection(owner).remove(event.removed); - } - - $(self.options.tagInputId + '_owner').val(self.getCollection('owner').pluck('id')); - $(self.options.tagInputId + '_all').val(self.getCollection().pluck('id')); - - $(self.options.tagInputId).select2('data', self.getCollection(owner).toArray()); - }, - - /** - * Render widget - * - * @returns {} - */ - render: function() { - this.updateHiddenInputs(); - - var tagCollection = new Oro.Tags.TagCollection(); - tagCollection.add(this.getCollection().models); - tagCollection.add(this.getCollection('owner').models); - - $('.select2-search-choice div').click(function(){ - var tagName = $(this).attr('title'); - var tag = tagCollection.toArray().filter(function(item){ return item.name == tagName }) - var url = tag[0].url; - - if (Oro.hashNavigationEnabled()) { - Oro.hashNavigationInstance.setLocation(url); - } else { - window.location = url; - } - }); - - return this; - } -}); diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js index d649468df32..4d0c82b6717 100644 --- a/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag.js @@ -6,6 +6,26 @@ Oro.Tags.TagView = Backbone.View.extend({ filter: null }, + /** @property */ + template: _.template( + '' + ), + /** * Constructor */ @@ -14,8 +34,6 @@ Oro.Tags.TagView = Backbone.View.extend({ this.listenTo(this.getCollection(), 'reset', this.render); this.listenTo(this, 'filter', this.render); - this.template = $('#tag-view-template').html(); - // process filter action binding $('#tag-sort-actions a').click(_.bind(this.filter, this)); }, @@ -55,12 +73,7 @@ Oro.Tags.TagView = Backbone.View.extend({ */ render: function() { $('#tags-holder').html( - _.template( - this.template, - { - "collection": this.getCollection().getFilteredCollection(this.options.filter) - } - ) + this.template(this.getCollection().getFilteredCollection(this.options.filter)) ); // process tag click redirect if (Oro.hashNavigationEnabled()) { diff --git a/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js new file mode 100644 index 00000000000..59772f00db1 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Resources/public/js/views/tag_update.js @@ -0,0 +1,172 @@ +Oro = Oro || {}; +Oro.Tags = Oro.Tags || {}; + +Oro.Tags.TagsUpdateView = Oro.Tags.TagView.extend({ + /** @property */ + tagsOverlayTemplate: _.template( + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + ), + + /** @property {Object} */ + options: { + filter: null, + tagsOverlayId: '#tags-overlay', + autocompleteFieldId: null, + fieldId: null, + ownFieldId: null, + unassign: false, + unassignGlobal: false + }, + + /** + * Initialize widget + * + * @param {Object} options + * @param {Backbone.Collection} options.tags + * @param {String} options.autocompleteFieldId DomElement ID of autocomplete widget + * @param {String} options.fieldId DomElement ID of hidden field with all tags + * @param {String} options.ownFieldId DomElement ID of hidden field with own tags + * @throws {TypeError} If mandatory options are undefined + */ + initialize: function(options) { + options = options || {}; + + if (!options.autocompleteFieldId) { + throw new TypeError("'autocompleteFieldId' is required") + } + + if (!options.fieldId) { + throw new TypeError("'fieldId' is required") + } + + if (!options.ownFieldId) { + throw new TypeError("'ownFieldId' is required") + } + + Oro.Tags.TagView.prototype.initialize.apply(this, arguments); + + + this._renderOverlay(); + this._prepareCollections(); + this.listenTo(this.getCollection(), 'add', this.render); + this.listenTo(this.getCollection(), 'add', this._updateHiddenInputs); + this.listenTo(this.getCollection(), 'remove', this.render); + this.listenTo(this.getCollection(), 'remove', this._updateHiddenInputs); + + $(this.options.autocompleteFieldId).on('change', _.bind(this._addItem, this)); + }, + + /** + * Render widget + * + * @returns {} + */ + render: function() { + Oro.Tags.TagView.prototype.render.apply(this, arguments); + + var renderButtons = false; + switch (true) { + case this.options.filter == 'owner' && this.options.unassign: + renderButtons = true; + break; + case this.options.filter != 'owner' && this.options.unassignGlobal: + renderButtons = true; + break; + } + if (renderButtons) { + $(this.options.tagsOverlayId).find('span.label').each(function(i, el) { + var $el = $(el); + + $el.append($('')); + $el.addClass('with-button'); + }); + + $(this.options.tagsOverlayId).find('span.label .select2-search-choice-close').click(_.bind(this._removeItem, this)); + } + }, + + /** + * Add item from autocomplete to internal collection + * + * @param {Object} e select2.change event object + * @private + */ + _addItem: function(e) { + e.preventDefault(); + var tag = e.added; + + if (!_.isUndefined(tag)) { + this.getCollection().addItem(tag); + } + + // clear autocomplete + $(e.target).select2('val', ''); + }, + + /** + * Removes item + * + * @param e + * @returns {*} + * @private + */ + _removeItem: function(e) { + e.preventDefault(); + e.stopImmediatePropagation(); + + var $el = $(e.currentTarget).parents('li'); + + var id = $($el).data('id'); + if (!_.isUndefined(id)) { + this.getCollection().removeItem(id, this.options.filter); + } + return this; + }, + + /** + * Render overlay block + * + * @returns {*} + * @private + */ + _renderOverlay: function() { + $(this.options.tagsOverlayId).append(this.tagsOverlayTemplate()); + + return this; + }, + + /** + * Fill data to collections from hidden inputs + * + * @returns {*} + * @private + */ + _prepareCollections: function() { + try { + var allTags = $.parseJSON($(this.options.fieldId).val()); + if (! _.isArray(allTags)) { + throw new TypeError("tags hidden field data is not array") + } + } catch (e) { + allTags = []; + } + + this.getCollection().reset(allTags); + + return this; + }, + + /** + * Update hidden inputs triggered by collection change + * + * @private + */ + _updateHiddenInputs: function() { + $(this.options.fieldId).val(JSON.stringify(this.getCollection())); + $(this.options.ownFieldId).val(JSON.stringify(this.getCollection().getFilteredCollection('owner'))); + } +}); diff --git a/src/Oro/Bundle/TagBundle/Resources/translations/messages.en.yml b/src/Oro/Bundle/TagBundle/Resources/translations/messages.en.yml index 4c0625609ef..a0ad0340a25 100644 --- a/src/Oro/Bundle/TagBundle/Resources/translations/messages.en.yml +++ b/src/Oro/Bundle/TagBundle/Resources/translations/messages.en.yml @@ -1,4 +1,5 @@ oro: tag: controller.tag.saved.message: "Tag successfully saved" - form.choose_tag: "Choose or create tag" + form.choose_tag: "Choose a tag" + form.choose_or_create_tag: "Choose or create a tag" diff --git a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig index 50df37ba5e9..0ca1ccf947f 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/Form/fields.html.twig @@ -1,19 +1,77 @@ + +{% block oro_tag_select_row %} + {% if resource_granted('oro_tag_view_tag_cloud') %} + {% if resource_granted('oro_tag_assign_unassign') %} +
    + {{ form_label(form, '' , { label_attr: label_attr|merge({ class: 'control-label' })}) }} + + {% import 'OroTagBundle::macros.html.twig' as _tag %} + +
    +
    +
    + {{ _tag.tagSortActions() }} +
    +
    +
    +
    + {{ form_row(form.autocomplete) }} +
    +
    + {% endif %} + + {{ form_row(form.all) }} + {{ form_row(form.owner) }} + +
    + {% if not resource_granted('oro_tag_assign_unassign') %} + {{ form_label(form, '' , { label_attr: label_attr|merge({ class: 'control-label' })}) }} + {% endif %} +
    + +
    + {% else %} + {{ form_row(form.all) }} + {{ form_row(form.owner) }} + {% endif %} +{% endblock %} + {% block oro_combobox_dataconfig_multi_autocomplete %} {{ block('oro_combobox_dataconfig_autocomplete') }} - - select2Config.multiple = true; select2Config.createSearchChoice = function(term, data) { - if ($(data).filter(function() { - return this.name.localeCompare(term) === 0; - }).length === 0) { + if ( + $(data).filter(function() { + return this.name.toLowerCase().localeCompare(term.toLowerCase()) === 0; + }).length === 0 + ) { + {% if not resource_granted('oro_tag_create') %} + return null; + {% else %} + return { + id: term, + name: term + }; + {% endif %} + } + } + {% if not resource_granted('oro_tag_create') %} - return null; - {% else %} - return { - id: term, - name: term - }; + select2Config.placeholder = '{{ 'oro.tag.form.choose_tag'|trans }}'; {% endif %} - } - } -{% endblock %} \ No newline at end of file +{% endblock %} + +{% block oro_tag_autocomplete_row %} + {{ form_widget(form) }} + {{ form_javascript(form) }} +{% endblock %} diff --git a/src/Oro/Bundle/TagBundle/Resources/views/Tag/update.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/Tag/update.html.twig index a4915f86946..b4442835941 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/Tag/update.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/Tag/update.html.twig @@ -1,6 +1,6 @@ {% extends 'OroUIBundle:actions:update.html.twig' %} {% form_theme form with 'OroUIBundle:Form:fields.html.twig' %} -{% set title = form.vars.value.id ? form.vars.value.name|default('N/A') : 'Add Tag'|trans %} +{% set title = form.vars.value.id ? form.vars.value.name|default('N/A') : 'New Tag'|trans %} {% if form.vars.value.id %} {% oro_title_set({params : {"%tag.name%": form.vars.value.name} }) %} {% endif %} @@ -59,7 +59,6 @@ { 'formErrors': form_errors(form)? form_errors(form) : null, 'dataBlocks': dataBlocks, - 'hiddenData': form_rest(form) } %} {{ parent() }} diff --git a/src/Oro/Bundle/TagBundle/Resources/views/javascript.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/javascript.html.twig deleted file mode 100644 index 0f7e787c7d8..00000000000 --- a/src/Oro/Bundle/TagBundle/Resources/views/javascript.html.twig +++ /dev/null @@ -1,15 +0,0 @@ -{% block oro_tag_view_js %} - -{% endblock oro_tag_view_js %} diff --git a/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig b/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig index bb1f6d7fedc..025965473d7 100644 --- a/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig +++ b/src/Oro/Bundle/TagBundle/Resources/views/macros.html.twig @@ -1,11 +1,4 @@ {% macro renderView(entity) %} - {# // check if block was not included yet #} - {% set _block = block('oro_tag_view_js') %} - {% if (_block is empty) %} - {% include 'OroTagBundle::javascript.html.twig' %} - {{ block('oro_tag_view_js')|raw }} - {% endif %} -
    {% import _self as _ %} @@ -24,36 +17,6 @@ {% endmacro %} -{# - Render input for tags -#} -{% macro tagInputField(label, form, defaultClass) %} - -
    -
    -
    - {% import _self as _ %} - {{ _.tagSortActions() }} -
    -
    -
    - - {{ form_row(form.tags) }} - - - - -{% endmacro %} - {# Render sort actions list #} diff --git a/src/Oro/Bundle/TagBundle/Tests/Functional/ControllersTest.php b/src/Oro/Bundle/TagBundle/Tests/Functional/ControllersTest.php index 19ce324fc94..0dfe5951a5e 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Functional/ControllersTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Functional/ControllersTest.php @@ -9,7 +9,6 @@ /** * @outputBuffering enabled * @db_isolation - * @reindex */ class ControllersTest extends WebTestCase { @@ -20,11 +19,7 @@ class ControllersTest extends WebTestCase public function setUp() { - if (!isset($this->client)) { - $this->client = static::createClient(array(), ToolsAPI::generateBasicHeader()); - } else { - $this->client->restart(); - } + $this->client = static::createClient(array(), ToolsAPI::generateBasicHeader()); } public function testIndex() @@ -47,12 +42,14 @@ public function testIndexJson() public function testCreate() { - $this->markTestIncomplete('BAP-1260'); $crawler = $this->client->request('GET', $this->client->generate('oro_tag_create')); $form = $crawler->selectButton('Save')->form(); - $form['oro_tag_tag_form[name]'] = 'testTag1'; - $this->client->submit($form); + $form['oro_tag_tag_form[name]'] = 'tag758'; + $form['oro_tag_tag_form[owner]'] = 1; + $this->client->followRedirects(true); + $crawler = $this->client->submit($form); $result = $this->client->getResponse(); - ToolsAPI::assertJsonResponse($result, 200); + ToolsAPI::assertJsonResponse($result, 200, 'text/html; charset=UTF-8'); + $this->assertContains("Tag successfully saved", $crawler->html()); } } diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Datagrid/ResultsQueryFactoryTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Datagrid/ResultsQueryFactoryTest.php index e82a8761b3c..e9d48bfb9ab 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Unit/Datagrid/ResultsQueryFactoryTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Datagrid/ResultsQueryFactoryTest.php @@ -74,30 +74,4 @@ public function testCreateQuery() $this->assertInstanceOf('Oro\Bundle\TagBundle\Datagrid\ResultsQuery', $this->queryFactory->createQuery()); } - - /** - * @expectedException \LogicException - */ - public function testCreateQueryException() - { - $this->registry->expects($this->once()) - ->method('getManagerForClass') - ->with('testClassName') - ->will($this->returnValue($this->em)); - - $repositoryMock = $this->getMockBuilder('Doctrine\ORM\EntityRepository') - ->disableOriginalConstructor() - ->getMock(); - - $this->em->expects($this->once()) - ->method('getRepository') - ->with('testClassName') - ->will($this->returnValue($repositoryMock)); - - $repositoryMock->expects($this->once()) - ->method('createQueryBuilder') - ->will($this->returnValue(false)); - - $this->queryFactory->createQuery(); - } } diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagManagerTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagManagerTest.php index 6148a39b972..ee51da4192d 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagManagerTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagManagerTest.php @@ -2,42 +2,357 @@ namespace Oro\Bundle\TagBundle\Tests\Unit\Entity; +use Doctrine\Common\Collections\ArrayCollection; +use Oro\Bundle\TagBundle\Entity\Tag; use Oro\Bundle\TagBundle\Entity\TagManager; +use Oro\Bundle\TagBundle\Tests\Unit\Fixtures\Taggable; class TagManagerTest extends \PHPUnit_Framework_TestCase { - /** - * @var TagManager - */ + const TEST_TAG_NAME = 'testName'; + const TEST_NEW_TAG_NAME = 'testAnotherName'; + const TEST_TAG_ID = 3333; + + const TEST_ENTITY_NAME = 'test name'; + const TEST_RECORD_ID = 1; + const TEST_CREATED_ID = 22; + + const TEST_USER_ID = 'someID'; + + /** @var TagManager */ protected $manager; + /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $em; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $mapper; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ protected $securityContext; + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $aclManager; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $router; + + /** @var \PHPUnit_Framework_MockObject_MockObject */ + protected $user; + public function setUp() { $this->em = $this->getMockBuilder('Doctrine\ORM\EntityManager') - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor()->getMock(); $this->mapper = $this->getMockBuilder('Oro\Bundle\SearchBundle\Engine\ObjectMapper') - ->disableOriginalConstructor() - ->getMock(); + ->disableOriginalConstructor()->getMock(); $this->securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); + $this->aclManager = $this->getMockBuilder('Oro\Bundle\UserBundle\Acl\Manager') + ->disableOriginalConstructor()->getMock(); + + $this->router = $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Routing\Router') + ->disableOriginalConstructor()->getMock(); + + $this->user = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + $this->user->expects($this->any()) + ->method('getId') + ->will($this->returnValue(self::TEST_USER_ID)); + + $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); + $token->expects($this->any()) + ->method('getUser') + ->will($this->returnValue($this->user)); + + $this->securityContext->expects($this->any()) + ->method('getToken') + ->will($this->returnValue($token)); + $this->manager = new TagManager( $this->em, 'Oro\Bundle\TagBundle\Entity\Tag', 'Oro\Bundle\TagBundle\Entity\Tagging', $this->mapper, - $this->securityContext + $this->securityContext, + $this->aclManager, + $this->router + ); + } + + public function testAddTags() + { + $testTags = array(new Tag(self::TEST_TAG_NAME)); + + $collection = $this->getMock('Doctrine\Common\Collections\ArrayCollection'); + $collection->expects($this->once())->method('add'); + + $resource = $this->getMockForAbstractClass('Oro\Bundle\TagBundle\Entity\Taggable'); + $resource->expects($this->once())->method('getTags') + ->will($this->returnValue($collection)); + + $this->manager->addTags($testTags, $resource); + } + + /** + * @dataProvider getTagNames + * @param array $names + * @param int|bool $shouldWorkWithDB + * @param int $resultCount + * @param array $tagsFromDB + */ + public function testLoadOrCreateTags($names, $shouldWorkWithDB, $resultCount, array $tagsFromDB) + { + $repo = $this->getMockBuilder('Doctrine\ORM\EntityRepository') + ->disableOriginalConstructor()->getMock(); + $this->em->expects($this->exactly((int) $shouldWorkWithDB))->method('getRepository') + ->will($this->returnValue($repo)); + + $repo->expects($this->exactly((int) $shouldWorkWithDB))->method('findBy') + ->will($this->returnValue($tagsFromDB)); + + $result = $this->manager->loadOrCreateTags($names); + + $this->assertCount($resultCount, $result); + if ($shouldWorkWithDB) { + $this->assertContainsOnlyInstancesOf('Oro\Bundle\TagBundle\Entity\Tag', $result); + } + + } + + /** + * @return array + */ + public function getTagNames() + { + return array( + 'with empty tag name will return empty array' => array( + 'names' => array(), + 'shouldWorkWithDB' => false, + 'resultCount' => 0, + array() + ), + 'with 1 tag from DB and 1 new tag' => array( + 'names' => array(self::TEST_TAG_NAME, self::TEST_NEW_TAG_NAME), + 'shouldWorkWithDB' => true, + 'resultCount' => 2, + array(new Tag(self::TEST_TAG_NAME)) + ) ); } - public function testDeleteTagging() + /** + * @dataProvider tagIdsProvider + */ + public function testDeleteTaggingByParams($tagIds, $entityName, $recordId, $createdBy, $expectedCallArg) { + $repo = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\Repository\TagRepository') + ->disableOriginalConstructor()->getMock(); + $repo->expects($this->once())->method('deleteTaggingByParams') + ->with($expectedCallArg, $entityName, $recordId, $createdBy); + + $this->em->expects($this->once())->method('getRepository') + ->will($this->returnValue($repo)); + + $this->manager->deleteTaggingByParams($tagIds, $entityName, $recordId, $createdBy); + } + + /** + * @return array + */ + public function tagIdsProvider() + { + $tag = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); + $tag->expects($this->once())->method('getId') + ->will($this->returnValue(self::TEST_TAG_ID)); + + return array( + 'null value should pass as array' => array( + 'tagIds' => null, + 'entityName' => self::TEST_ENTITY_NAME, + 'recordId' => self::TEST_RECORD_ID, + 'createdBy' => self::TEST_CREATED_ID, + 'expectedCallArg' => array() + ), + 'some ids data ' => array( + 'tagIds' => array(self::TEST_TAG_ID), + 'entityName' => self::TEST_ENTITY_NAME, + 'recordId' => self::TEST_RECORD_ID, + 'createdBy' => self::TEST_CREATED_ID, + 'expectedCallArg' => array(self::TEST_TAG_ID) + ), + 'some array collection' => array( + 'tagIds' => new ArrayCollection(array($tag)), + 'entityName' => self::TEST_ENTITY_NAME, + 'recordId' => self::TEST_RECORD_ID, + 'createdBy' => self::TEST_CREATED_ID, + 'expectedCallArg' => array(self::TEST_TAG_ID) + + ) + ); + } + + public function testLoadTagging() + { + $collection = $this->getMock('Doctrine\Common\Collections\ArrayCollection'); + $collection->expects($this->once())->method('add'); + + $resource = $this->getMockForAbstractClass('Oro\Bundle\TagBundle\Entity\Taggable'); + $resource->expects($this->once())->method('getTags') + ->will($this->returnValue($collection)); + + $repo = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\Repository\TagRepository') + ->disableOriginalConstructor()->getMock(); + $repo->expects($this->once())->method('getTagging') + ->with($resource, null, false) + ->will( + $this->returnValue( + array( + new Tag(self::TEST_TAG_NAME) + ) + ) + ); + + $this->em->expects($this->once())->method('getRepository')->with('Oro\Bundle\TagBundle\Entity\Tag') + ->will($this->returnValue($repo)); + + $this->manager->loadTagging($resource); + } + + public function testCompareCallback() + { + $tag = new Tag('testName'); + $tagToCompare = new Tag('testName'); + $tagToCompare2 = new Tag('notTheSameName'); + + $callback = $this->manager->compareCallback($tag); + + $this->assertTrue($callback(1, $tagToCompare)); + $this->assertFalse($callback(1, $tagToCompare2)); + } + + public function testGetPreparedArrayFromDb() + { + $resource = new Taggable(array('id' => 1)); + $tagging = $this->getMock('Oro\Bundle\TagBundle\Entity\Tagging'); + + $tag1 = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); + $tag1->expects($this->once())->method('getName') + ->will($this->returnValue('test name 1')); + $tag1->expects($this->any())->method('getId') + ->will($this->returnValue(1)); + $tag1->expects($this->exactly(1))->method('getTagging') + ->will($this->returnValue(new ArrayCollection(array($tagging)))); + + $tag2 = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); + $tag2->expects($this->once())->method('getName') + ->will($this->returnValue('test name 2')); + $tag2->expects($this->any())->method('getId') + ->will($this->returnValue(2)); + $tag2->expects($this->exactly(1))->method('getTagging') + ->will($this->returnValue(new ArrayCollection(array($tagging)))); + + $userMock = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + + $tagging->expects($this->exactly(2)) + ->method('getCreatedBy')->will($this->returnValue($userMock)); + $tagging->expects($this->any()) + ->method('getEntityName')->will($this->returnValue(get_class($resource))); + $tagging->expects($this->any()) + ->method('getRecordId')->will($this->returnValue(1)); + + $userMock->expects($this->at(0)) + ->method('getId')->will($this->returnValue(self::TEST_USER_ID)); + $userMock->expects($this->at(1))->method('getId') + ->will($this->returnValue('uniqueId2')); + + $this->user->expects($this->exactly(2))->method('getId') + ->will($this->returnValue(self::TEST_USER_ID)); + + $this->router->expects($this->exactly(2)) + ->method('generate'); + + $repo = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\Repository\TagRepository') + ->disableOriginalConstructor()->getMock(); + $repo->expects($this->once())->method('getTagging')->with($resource, null, false) + ->will( + $this->returnValue( + array($tag1, $tag2) + ) + ); + + $this->em->expects($this->once())->method('getRepository')->with('Oro\Bundle\TagBundle\Entity\Tag') + ->will($this->returnValue($repo)); + + $result = $this->manager->getPreparedArray($resource); + + $this->assertCount(2, $result); + + $this->assertArrayHasKey('url', $result[0]); + $this->assertArrayHasKey('name', $result[0]); + $this->assertArrayHasKey('id', $result[0]); + $this->assertArrayHasKey('owner', $result[0]); + + $this->assertFalse($result[1]['owner']); + $this->assertTrue($result[0]['owner']); + } + + public function testGetPreparedArrayFromArray() + { + $resource = new Taggable(array('id' => 1)); + + $this->user->expects($this->exactly(2)) + ->method('getId') + ->will($this->returnValue(self::TEST_USER_ID)); + + $this->router->expects($this->once()) + ->method('generate'); + + $repo = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\Repository\TagRepository') + ->disableOriginalConstructor()->getMock(); + $repo->expects($this->never())->method('getTagging'); + + $this->manager->getPreparedArray($resource, $this->tagForPreparing()); + } + + protected function tagForPreparing() + { + $tag1 = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); + $tag2 = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); + $tagging = $this->getMock('Oro\Bundle\TagBundle\Entity\Tagging'); + + $tag1->expects($this->exactly(2)) + ->method('getName') + ->will($this->returnValue('test name 1')); + $tag1->expects($this->any()) + ->method('getId') + ->will($this->returnValue(null)); + $tag1->expects($this->exactly(1)) + ->method('getTagging') + ->will($this->returnValue(new ArrayCollection(array($tagging)))); + + $tag2->expects($this->once())->method('getName') + ->will($this->returnValue('test name 2')); + $tag2->expects($this->any())->method('getId') + ->will($this->returnValue(2)); + $tag2->expects($this->exactly(1))->method('getTagging') + ->will($this->returnValue(new ArrayCollection(array($tagging)))); + + $userMock = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); + + $tagging->expects($this->exactly(2)) + ->method('getCreatedBy')->will($this->returnValue($userMock)); + $tagging->expects($this->any()) + ->method('getEntityName')->will($this->returnValue('Oro\Bundle\TagBundle\Tests\Unit\Fixtures\Taggable')); + $tagging->expects($this->any()) + ->method('getRecordId')->will($this->returnValue(1)); + + $userMock->expects($this->at(0)) + ->method('getId')->will($this->returnValue(self::TEST_USER_ID)); + $userMock->expects($this->at(1)) + ->method('getId')->will($this->returnValue('uniqueId2')); + return new ArrayCollection(array($tag1, $tag2)); } } diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagTest.php index 47c442998fc..5e83aa16e32 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Entity/TagTest.php @@ -2,7 +2,10 @@ namespace Oro\Bundle\TagBundle\Tests\Unit\Entity; +use Doctrine\Common\Collections\ArrayCollection; + use Oro\Bundle\TagBundle\Entity\Tag; +use Oro\Bundle\UserBundle\Entity\User; class TagTest extends \PHPUnit_Framework_TestCase { @@ -14,6 +17,8 @@ class TagTest extends \PHPUnit_Framework_TestCase public function setUp() { $this->tag = new Tag(); + + $this->assertEquals(null, $this->tag->getId()); } public function testSetGetNameMethods() @@ -60,4 +65,21 @@ public function testUpdatedTime() $this->assertInstanceOf('\DateTime', $this->tag->getUpdatedAt()); $this->assertNotEquals($oldUpdatedTime, $this->tag->getUpdatedAt()); } + + public function testGetTagging() + { + $this->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $this->tag->getTagging()); + } + + public function testOwners() + { + $entity = $this->tag; + $user = new User(); + + $this->assertEmpty($entity->getOwner()); + + $entity->setOwner($user); + + $this->assertEquals($user, $entity->getOwner()); + } } diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/EventListener/OwnerListenerTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/EventListener/OwnerListenerTest.php index 81e9d316ccc..b408b735bc7 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Unit/EventListener/OwnerListenerTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/EventListener/OwnerListenerTest.php @@ -12,12 +12,25 @@ class OwnerListenerTest extends \PHPUnit_Framework_TestCase private $listener; /** - * @var \Oro\Bundle\TagBundle\Entity\Taggable + * @var \PHPUnit_Framework_MockObject_MockObject */ private $resource; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ private $securityContext; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ private $user; + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + private $container; + public function setUp() { $this->resource = $this->getMock('Oro\Bundle\TagBundle\Entity\ContainUpdaterInterface'); @@ -39,6 +52,23 @@ public function setUp() ->expects($this->exactly(3)) ->method('getToken') ->will($this->returnValue($token)); + + $this->container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); + $this->container->expects($this->once()) + ->method('get') + ->with($this->equalTo('security.context')) + ->will($this->returnValue($this->securityContext)); + + $this->listener = new OwnerListener(); + $this->listener->setContainer($this->container); + } + + public function tearDown() + { + unset($this->listener); + unset($this->resource); + unset($this->securityContext); + unset($this->user); } /** @@ -69,19 +99,10 @@ public function testPreUpdate() ->with(get_class($this->resource)) ->will($this->returnValue($meta)); - $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); - $container->expects($this->once()) - ->method('get') - ->with($this->equalTo('security.context')) - ->will($this->returnValue($this->securityContext)); - $this->resource->expects($this->once()) ->method('setUpdatedBy') ->with($this->user); - $this->listener = new OwnerListener(); - $this->listener->setContainer($container); - $args = $this->getMockBuilder('Doctrine\ORM\Event\PreUpdateEventArgs') ->disableOriginalConstructor() ->getMock(); @@ -97,6 +118,23 @@ public function testPreUpdate() $this->listener->preUpdate($args); } + public function testSkipNotNeededEntitiesOnPreUpdate() + { + $this->resource = $this->getMock('Oro\Bundle\TagBundle\Tests\Unit\Fixtures\Entity'); + $args = $this->getMockBuilder('Doctrine\ORM\Event\PreUpdateEventArgs') + ->disableOriginalConstructor() + ->getMock(); + + $args->expects($this->once()) + ->method('getEntity') + ->will($this->returnValue($this->resource)); + + $this->resource->expects($this->never()) + ->method('setUpdatedBy'); + + $this->listener->preUpdate($args); + } + public function testPrePersist() { $args = $this->getMockBuilder('Doctrine\ORM\Event\LifecycleEventArgs') @@ -108,18 +146,27 @@ public function testPrePersist() ->method('getEntity') ->will($this->returnValue($resource)); - $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); - $container->expects($this->once()) - ->method('get') - ->with($this->equalTo('security.context')) - ->will($this->returnValue($this->securityContext)); - $resource->expects($this->once()) ->method('setCreatedBy') ->with($this->user); - $this->listener = new OwnerListener(); - $this->listener->setContainer($container); + $this->listener->prePersist($args); + } + + public function testSkipNotNeededEntitiesOnPrePersist() + { + $this->resource = $this->getMock('Oro\Bundle\TagBundle\Tests\Unit\Fixtures\Entity'); + $args = $this->getMockBuilder('Doctrine\ORM\Event\LifecycleEventArgs') + ->disableOriginalConstructor() + ->getMock(); + + $args->expects($this->once()) + ->method('getEntity') + ->will($this->returnValue($this->resource)); + + $this->resource->expects($this->never()) + ->method('setCreatedBy'); + $this->listener->prePersist($args); } } diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/EventListener/TagListenerTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/EventListener/TagListenerTest.php index ca550d382de..74aa8dd9fcb 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Unit/EventListener/TagListenerTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/EventListener/TagListenerTest.php @@ -6,19 +6,29 @@ class TagListenerTest extends \PHPUnit_Framework_TestCase { + const TEST_ID = 1; + /** * @var TagListener */ private $listener; /** - * @var \Oro\Bundle\TagBundle\Entity\Taggable + * @var \PHPUnit_Framework_MockObject_MockObject */ private $resource; public function setUp() { $this->resource = $this->getMock('Oro\Bundle\TagBundle\Entity\Taggable'); + $this->resource->expects($this->once())->method('getTaggableId') + ->will($this->returnValue(self::TEST_ID)); + } + + public function tearDown() + { + unset($this->listener); + unset($this->resource); } /** @@ -30,8 +40,8 @@ public function testPreRemove() ->disableOriginalConstructor() ->getMock(); $manager->expects($this->once()) - ->method('deleteTagging') - ->with($this->resource); + ->method('deleteTaggingByParams') + ->with(null, get_class($this->resource), self::TEST_ID); $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); $container->expects($this->once()) diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Fixtures/Entity.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Fixtures/Entity.php new file mode 100644 index 00000000000..af85aa95136 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Fixtures/Entity.php @@ -0,0 +1,8 @@ +tags = new ArrayCollection(); + + if (!isset($data['id'])) { + $data['id'] = time(); + } + + $this->data = $data; + } + + /** + * {@inheritdoc} + */ + public function getTags() + { + return $this->tags; + } + + /** + * {@inheritdoc} + */ + public function setTags($tags) + { + $this->tags = $tags; + + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTaggableId() + { + return $this->data['id']; + } + + /** + * @param $name + * @return null + */ + public function __get($name) + { + if (array_key_exists($name, $this->data)) { + return $this->data[$name]; + } + + $trace = debug_backtrace(); + trigger_error( + 'Undefined property via __get(): ' . $name . + ' in ' . $trace[0]['file'] . + ' on line ' . $trace[0]['line'], + E_USER_NOTICE + ); + return null; + } +} diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Form/EventSubscriber/TagSubscriberTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/EventSubscriber/TagSubscriberTest.php new file mode 100644 index 00000000000..b8b475e638a --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/EventSubscriber/TagSubscriberTest.php @@ -0,0 +1,147 @@ +transformer = $this->getMockBuilder('Oro\Bundle\TagBundle\Form\Transformer\TagTransformer') + ->disableOriginalConstructor()->getMock(); + $this->manager = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\TagManager') + ->disableOriginalConstructor()->getMock(); + + $this->subscriber = new TagSubscriber($this->manager, $this->transformer); + } + + public function tearDown() + { + unset($this->subscriber); + unset($this->transformer); + unset($this->manager); + } + + public function testSubscribedEvents() + { + $result = TagSubscriber::getSubscribedEvents(); + + $this->assertArrayHasKey(FormEvents::PRE_SET_DATA, $result); + $this->assertArrayHasKey(FormEvents::PRE_SUBMIT, $result); + + $this->assertEquals('preSet', $result[FormEvents::PRE_SET_DATA]); + $this->assertEquals('preSubmit', $result[FormEvents::PRE_SUBMIT]); + } + + /** + * @dataProvider entityProvider + */ + public function testPreSet($entity, $shouldSetData) + { + $eventMock = $this->getMockBuilder('Symfony\Component\Form\FormEvent') + ->disableOriginalConstructor()->getMock(); + + $parentFormMock = $this->getMockForAbstractClass('Symfony\Component\Form\Test\FormInterface'); + $parentFormMock->expects($this->once())->method('getData') + ->will($this->returnValue($entity)); + + $formMock = $this->getMockForAbstractClass('Symfony\Component\Form\Test\FormInterface'); + $formMock->expects($this->once())->method('getParent') + ->will($this->returnValue($parentFormMock)); + + $eventMock->expects($this->once())->method('getForm') + ->will($this->returnValue($formMock)); + + if ($shouldSetData) { + $this->manager->expects($this->once())->method('getPreparedArray') + ->with($entity)->will( + $this->returnValue( + array(array('owner' => false), array('owner' => true)) + ) + ); + + $this->transformer->expects($this->exactly($shouldSetData))->method('setEntity')->with($entity); + $eventMock->expects($this->exactly($shouldSetData))->method('setData'); + } else { + $this->manager->expects($this->never())->method('getPreparedArray'); + $this->transformer->expects($this->never())->method('setEntity'); + $eventMock->expects($this->never())->method('setData'); + } + + + + $this->subscriber->preSet($eventMock); + } + + /** + * @return array + */ + public function entityProvider() + { + return array( + 'instance of taggable' => array($this->getMock('Oro\Bundle\TagBundle\Entity\Taggable'), 1), + 'another entity' => array($this->getMock('Oro\Bundle\TagBundle\Tests\Unit\Fixtures\Entity'), false), + ); + } + + /** + * @dataProvider submittedData + * @param $data + */ + public function testPreSubmit($data) + { + $eventMock = $this->getMockBuilder('Symfony\Component\Form\FormEvent') + ->disableOriginalConstructor()->getMock(); + $eventMock->expects($this->once())->method('getData') + ->will($this->returnValue($data)); + + $this->manager->expects($this->once())->method('loadOrCreateTags') + ->with( + array( + self::TEST_TAG_NAME + ) + )->will($this->returnValue(array(new Tag(self::TEST_TAG_NAME)))); + + $phpUnit = $this; + $eventMock->expects($this->once())->method('setData') + ->will( + $this->returnCallback( + function ($entities) use ($phpUnit) { + $phpUnit->assertArrayHasKey('all', $entities); + $phpUnit->assertArrayHasKey('owner', $entities); + + $phpUnit->assertContainsOnlyInstancesOf('Oro\Bundle\TagBundle\Entity\Tag', $entities['all']); + $phpUnit->assertEmpty($entities['owner']); + } + ) + ); + $this->subscriber->preSubmit($eventMock); + } + + public function submittedData() + { + return array( + 'json submitted data' => array( + array( + 'all' => "[{\"name\":\"" . self::TEST_TAG_NAME . "\"}]", + 'owner' => '[incorrect JSON]' + ) + ) + ); + } +} diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Transformer/TagTrasformerTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Transformer/TagTrasformerTest.php new file mode 100644 index 00000000000..9d0f953b4cf --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Transformer/TagTrasformerTest.php @@ -0,0 +1,77 @@ +manager = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\TagManager') + ->disableOriginalConstructor()->getMock(); + $this->transformer = new TagTransformer($this->manager); + } + + public function tearDown() + { + unset($this->manager); + unset($this->transformer); + } + + /** + * @dataProvider valueTransformProvider + * @param $value + */ + public function testReverseTransform($value) + { + $this->assertEquals($value, $this->transformer->reverseTransform($value)); + } + + /** + * @return array + */ + public function valueTransformProvider() + { + return array( + 'some string' => array('string'), + 'null' => array(null), + 'some array' => array(array('test array')), + ); + } + + public function testTransform() + { + $entity = $this->getMock('Oro\Bundle\TagBundle\Entity\Taggable'); + $this->transformer->setEntity($entity); + + $resultArray = array(array('some key' => 'some value')); + $phpUnit = $this; + + $this->manager->expects($this->once()) + ->method('getPreparedArray') + ->will( + $this->returnCallback( + function ($entityArg, $tagsArg) use ($phpUnit, $entity, $resultArray) { + $phpUnit->assertEquals($entity, $entityArg); + $phpUnit->assertInstanceOf('Doctrine\Common\Collections\ArrayCollection', $tagsArg); + + return $resultArray; + } + ) + ) + ->will($this->returnValue($resultArray)); + + $this->assertEquals($this->transformer->transform(array()), json_encode($resultArray)); + } +} diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagApiTypeTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagApiTypeTest.php index 14b36362f7c..b3f1c44e2a5 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagApiTypeTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagApiTypeTest.php @@ -17,6 +17,11 @@ protected function setUp() $this->type = new TagApiType(); } + public function tearDown() + { + unset($this->type); + } + public function testSetDefaultOptions() { $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagAutocompleteTypeTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagAutocompleteTypeTest.php new file mode 100644 index 00000000000..2d57decd1c9 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagAutocompleteTypeTest.php @@ -0,0 +1,44 @@ +type = new TagAutocompleteType(); + } + + public function tearDown() + { + unset($this->type); + } + + public function testSetDefaultOptions() + { + $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); + $resolver->expects($this->once()) + ->method('setDefaults') + ->with($this->isType('array')); + + $this->type->setDefaultOptions($resolver); + } + + public function testGetName() + { + $this->assertEquals('oro_tag_autocomplete', $this->type->getName()); + } + + public function testGetParent() + { + $this->assertEquals('oro_jqueryselect2_hidden', $this->type->getParent()); + } +} diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagSelectTypeTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagSelectTypeTest.php index dafc682e819..c6ad3945507 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagSelectTypeTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagSelectTypeTest.php @@ -12,18 +12,38 @@ class TagSelectTypeTest extends \PHPUnit_Framework_TestCase */ protected $type; - public function testSetDefaultOptions() + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $transformer; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ + protected $subscriber; + + public function setUp() { - $manager = $this->getMockBuilder('Doctrine\ORM\EntityManager') + $this->transformer = $this->getMockBuilder('Oro\Bundle\TagBundle\Form\Transformer\TagTransformer') ->disableOriginalConstructor() ->getMock(); - $tagManager = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\TagManager') + $this->subscriber = $this->getMockBuilder('Oro\Bundle\TagBundle\Form\EventSubscriber\TagSubscriber') ->disableOriginalConstructor() ->getMock(); - $this->type = new TagSelectType($manager, $tagManager); + $this->type = new TagSelectType($this->subscriber, $this->transformer); + } + + public function tearDown() + { + unset($this->transformer); + unset($this->subscriber); + unset($this->type); + } + public function testSetDefaultOptions() + { $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); $resolver->expects($this->once()) @@ -35,35 +55,37 @@ public function testSetDefaultOptions() public function testBuildForm() { - $meta = $this->getMockBuilder('\Doctrine\ORM\Mapping\ClassMetadata') + $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') ->disableOriginalConstructor() ->getMock(); + $builder->expects($this->once()) + ->method('addEventSubscriber') + ->with($this->subscriber) + ->will($this->returnSelf()); - $meta->expects($this->once()) - ->method('getSingleIdentifierFieldName') - ->will($this->returnValue('id')); - - $manager = $this->getMockBuilder('Doctrine\ORM\EntityManager') - ->disableOriginalConstructor() - ->getMock(); - $manager->expects($this->once()) - ->method('getClassMetadata') - ->with('Oro\Bundle\TagBundle\Entity\Tag') - ->will($this->returnValue($meta)); + $builder->expects($this->at(1)) + ->method('add') + ->with('autocomplete', 'oro_tag_autocomplete') + ->will($this->returnSelf()); - $tagManager = $this->getMockBuilder('Oro\Bundle\TagBundle\Entity\TagManager') - ->disableOriginalConstructor() - ->getMock(); + $builder->expects($this->any()) + ->method('add') + ->will($this->returnSelf()); - $this->type = new TagSelectType($manager, $tagManager); + $builder->expects($this->any()) + ->method('create') + ->will($this->returnSelf()); - $builder = $this->getMockBuilder('Symfony\Component\Form\FormBuilder') - ->disableOriginalConstructor() - ->getMock(); - $builder->expects($this->once()) - ->method('addModelTransformer') + $builder->expects($this->exactly(2)) + ->method('addViewTransformer') + ->with($this->transformer) ->will($this->returnSelf()); $this->type->buildForm($builder, array()); } + + public function testGetName() + { + $this->assertEquals('oro_tag_select', $this->type->getName()); + } } diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagTypeTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagTypeTest.php index f6582528d63..642c77a3e6c 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagTypeTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Form/Type/TagTypeTest.php @@ -17,6 +17,11 @@ protected function setUp() $this->type = new TagType(); } + public function tearDown() + { + unset($this->type); + } + public function testSetDefaultOptions() { $resolver = $this->getMock('Symfony\Component\OptionsResolver\OptionsResolverInterface'); diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Provider/SearchProviderTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Provider/SearchProviderTest.php new file mode 100644 index 00000000000..458e54393a4 --- /dev/null +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Provider/SearchProviderTest.php @@ -0,0 +1,72 @@ +entityManager = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor()->getMock(); + $this->mapper = $this->getMockBuilder('Oro\Bundle\SearchBundle\Engine\ObjectMapper') + ->disableOriginalConstructor()->getMock(); + $this->provider = new SearchProvider($this->entityManager, $this->mapper); + } + + public function tearDown() + { + unset($this->entityManager); + unset($this->mapper); + unset($this->provider); + } + + public function testGetResults() + { + $taggingMock = $this->getMock('Oro\Bundle\TagBundle\Entity\Tagging'); + $taggingMock->expects($this->exactly(2))->method('getEntityName') + ->will($this->returnValue(self::TEST_ENTITY_NAME)); + + $query = $this->getMockBuilder('Doctrine\ORM\AbstractQuery') + ->disableOriginalConstructor() + ->setMethods(array('getResult')) + ->getMockForAbstractClass(); + $query->expects($this->once())->method('getResult') + ->will($this->returnValue(array($taggingMock))); + + $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + ->disableOriginalConstructor()->getMock(); + $qb->expects($this->once())->method('select') + ->will($this->returnSelf()); + $qb->expects($this->once())->method('from') + ->will($this->returnSelf()); + $qb->expects($this->once())->method('where') + ->will($this->returnSelf()); + $qb->expects($this->exactly(2))->method('addGroupBy') + ->will($this->returnSelf()); + $qb->expects($this->once())->method('setParameter') + ->will($this->returnSelf()); + $qb->expects($this->once())->method('getQuery') + ->will($this->returnValue($query)); + + $this->entityManager->expects($this->once())->method('createQueryBuilder') + ->will($this->returnValue($qb)); + + $this->mapper->expects($this->once())->method('getEntityConfig')->with(self::TEST_ENTITY_NAME); + + $this->assertInstanceOf('Oro\Bundle\SearchBundle\Query\Result', $this->provider->getResults(self::TEST_ID)); + } +} diff --git a/src/Oro/Bundle/TagBundle/Tests/Unit/Twig/TagExtensionTest.php b/src/Oro/Bundle/TagBundle/Tests/Unit/Twig/TagExtensionTest.php index 05b7c155afa..65eb2d06da2 100644 --- a/src/Oro/Bundle/TagBundle/Tests/Unit/Twig/TagExtensionTest.php +++ b/src/Oro/Bundle/TagBundle/Tests/Unit/Twig/TagExtensionTest.php @@ -2,7 +2,6 @@ namespace Oro\Bundle\TagBundle\Tests\Unit\Twig; -use Doctrine\Common\Collections\ArrayCollection; use Oro\Bundle\TagBundle\Twig\TagExtension; class TagExtensionTest extends \PHPUnit_Framework_TestCase @@ -17,26 +16,6 @@ class TagExtensionTest extends \PHPUnit_Framework_TestCase */ protected $manager; - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $securityContext; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $router; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $aclManager; - - /** - * @var \PHPUnit_Framework_MockObject_MockObject - */ - protected $user; - /** * Set up test environment */ @@ -46,28 +25,13 @@ public function setUp() ->disableOriginalConstructor() ->getMock(); - $this->router = $this->getMockBuilder('Symfony\Component\Routing\Router') - ->disableOriginalConstructor() - ->getMock(); - $this->securityContext = $this->getMock('Symfony\Component\Security\Core\SecurityContextInterface'); - - $this->user = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); - $this->user->expects($this->any()) - ->method('getId') - ->will($this->returnValue('uniqueId')); - - $this->aclManager = $this->getMockForAbstractClass('Oro\Bundle\UserBundle\Acl\ManagerInterface'); - - $token = $this->getMock('Symfony\Component\Security\Core\Authentication\Token\TokenInterface'); - $token->expects($this->any()) - ->method('getUser') - ->will($this->returnValue($this->user)); - - $this->securityContext->expects($this->any()) - ->method('getToken') - ->will($this->returnValue($token)); + $this->extension = new TagExtension($this->manager); + } - $this->extension = new TagExtension($this->manager, $this->router, $this->securityContext, $this->aclManager); + public function tearDown() + { + unset($this->manager); + unset($this->extension); } public function testName() @@ -83,89 +47,11 @@ public function testGetFunctions() public function testGet() { $entity = $this->getMock('Oro\Bundle\TagBundle\Entity\Taggable'); - $tag1 = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); - $tag2 = $this->getMock('Oro\Bundle\TagBundle\Entity\Tag'); - $tagging = $this->getMock('Oro\Bundle\TagBundle\Entity\Tagging'); - - $tag1->expects($this->once()) - ->method('getName') - ->will($this->returnValue('test name 1')); - $tag1->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $tag1->expects($this->exactly(1)) - ->method('getTagging') - ->will($this->returnValue(new ArrayCollection(array($tagging)))); - - $tag2->expects($this->once()) - ->method('getName') - ->will($this->returnValue('test name 2')); - $tag2->expects($this->any()) - ->method('getId') - ->will($this->returnValue(2)); - $tag2->expects($this->exactly(1)) - ->method('getTagging') - ->will($this->returnValue(new ArrayCollection(array($tagging)))); - - $userMock = $this->getMock('Oro\Bundle\UserBundle\Entity\User'); - - $tagging->expects($this->exactly(2)) - ->method('getCreatedBy') - ->will($this->returnValue($userMock)); - $tagging->expects($this->any()) - ->method('getEntityName') - ->will($this->returnValue(get_class($entity))); - $tagging->expects($this->any()) - ->method('getRecordId') - ->will($this->returnValue(1)); - - $userMock->expects($this->at(0)) - ->method('getId') - ->will($this->returnValue('uniqueId')); - $userMock->expects($this->at(1)) - ->method('getId') - ->will($this->returnValue('uniqueId2')); - - $this->user->expects($this->exactly(2)) - ->method('getId') - ->will($this->returnValue('uniqueId')); - - $this->router->expects($this->exactly(2)) - ->method('generate'); $this->manager->expects($this->once()) - ->method('loadTagging'); - - $tags = array( - $tag1, $tag2 - ); - - $entity->expects($this->once()) - ->method('getTags') - ->will($this->returnValue($tags)); - - $entity->expects($this->exactly(2)) - ->method('getTaggableId') - ->will($this->returnValue(1)); - - $this->aclManager->expects($this->at(0)) - ->method('isResourceGranted') - ->will($this->returnValue(true)); - - $this->aclManager->expects($this->at(1)) - ->method('isResourceGranted') - ->will($this->returnValue(false)); - - $result = $this->extension->get($entity); - - $this->assertCount(2, $result); - - $this->assertArrayHasKey('url', $result[0]); - $this->assertArrayHasKey('name', $result[0]); - $this->assertArrayHasKey('id', $result[0]); - $this->assertArrayHasKey('owner', $result[0]); + ->method('getPreparedArray') + ->with($entity); - $this->assertArrayNotHasKey('owner', $result[1]); - $this->arrayHasKey('locked', $result[1]); + $this->extension->get($entity); } } diff --git a/src/Oro/Bundle/TagBundle/Twig/TagExtension.php b/src/Oro/Bundle/TagBundle/Twig/TagExtension.php index 7163cb88d66..dda222808b9 100644 --- a/src/Oro/Bundle/TagBundle/Twig/TagExtension.php +++ b/src/Oro/Bundle/TagBundle/Twig/TagExtension.php @@ -2,12 +2,8 @@ namespace Oro\Bundle\TagBundle\Twig; -use Oro\Bundle\UserBundle\Acl\ManagerInterface; use Symfony\Component\Routing\Router; -use Symfony\Component\Security\Core\SecurityContextInterface; -use Oro\Bundle\TagBundle\Entity\Tag; -use Oro\Bundle\TagBundle\Entity\Tagging; use Oro\Bundle\TagBundle\Entity\Taggable; use Oro\Bundle\TagBundle\Entity\TagManager; @@ -18,38 +14,9 @@ class TagExtension extends \Twig_Extension */ protected $manager; - /** - * @var Router - */ - protected $router; - - /** - * @var SecurityContextInterface - */ - protected $context; - - /** - * @var \Oro\Bundle\UserBundle\Acl\ManagerInterface - */ - private $aclManager; - - public function __construct(TagManager $manager, Router $router, SecurityContextInterface $securityContext, ManagerInterface $aclManager) + public function __construct(TagManager $manager) { $this->manager = $manager; - $this->router = $router; - $this->aclManager = $aclManager; - - $this->context = $securityContext; - } - - /** - * Return current user - * - * @return mixed - */ - public function getUser() - { - return $this->context->getToken()->getUser(); } /** @@ -70,39 +37,7 @@ public function getFunctions() */ public function get(Taggable $entity) { - $this->manager->loadTagging($entity); - $result = array(); - - /** @var Tag $tag */ - foreach ($entity->getTags() as $tag) { - $entry = array( - 'name' => $tag->getName(), - 'id' => $tag->getId(), - 'url' => $this->router->generate('oro_tag_search', array('id' => $tag->getId())) - ); - - $taggingCollection = $tag->getTagging()->filter( - function (Tagging $tagging) use ($entity) { - // only use tagging entities that related to current entity - return $tagging->getEntityName() == get_class($entity) - && $tagging->getRecordId() == $entity->getTaggableId(); - } - ); - /** @var Tagging $tagging */ - foreach ($taggingCollection as $tagging) { - if ($this->getUser()->getId() == $tagging->getCreatedBy()->getId()) { - $entry['owner'] = true; - } - } - - if (!$this->aclManager->isResourceGranted('oro_tag_unassign_global') && !isset($entry['owner'])) { - $entry['locked'] = true; - } - - $result[] = $entry; - } - - return $result; + return $this->manager->getPreparedArray($entity); } /** diff --git a/src/Oro/Bundle/TagBundle/composer.json b/src/Oro/Bundle/TagBundle/composer.json index 6486b753791..9cff610dc09 100644 --- a/src/Oro/Bundle/TagBundle/composer.json +++ b/src/Oro/Bundle/TagBundle/composer.json @@ -6,7 +6,8 @@ "license": "MIT", "require": { "php": ">=5.3.3", - "symfony/symfony": "2.1.*" + "symfony/symfony": "2.1.*", + "oro/form-bundle": "dev-master" }, "autoload": { "psr-0": { "Oro\\Bundle\\TagBundle": "" } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Account.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Account.php index ced6910fc9a..1b1cee5b1c2 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Account.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Account.php @@ -14,23 +14,19 @@ class Account extends AbstractEntity implements Entity protected $country; protected $state; - public function __construct($testCase, $redirect = true) - { - parent::__construct($testCase, $redirect); - } - public function init() { $this->accountname = $this->byId('orocrm_account_form_name'); - $this->street = $this->byId('orocrm_account_form_values_billing_address_address_street'); - $this->city = $this->byId('orocrm_account_form_values_billing_address_address_city'); - $this->country = $this->byXpath("//div[@id='s2id_orocrm_account_form_values_billing_address_address_country']/a"); - $this->zipcode = $this->byId('orocrm_account_form_values_billing_address_address_postalCode'); - - if ($this->byId('orocrm_account_form_values_billing_address_address_state_text')->displayed()) { - $this->state = $this->byId('orocrm_account_form_values_billing_address_address_state_text'); + $this->street = $this->byId('orocrm_account_form_billingAddress_street'); + $this->city = $this->byId('orocrm_account_form_billingAddress_city'); + $this->country = $this->byXpath("//div[@id='s2id_orocrm_account_form_billingAddress_country']/a"); + $this->zipcode = $this->byId('orocrm_account_form_billingAddress_postalCode'); + $this->owner = $this->byXpath("//div[@id='s2id_orocrm_account_form_owner']/a"); + + if ($this->byId('orocrm_account_form_billingAddress_state_text')->displayed()) { + $this->state = $this->byId('orocrm_account_form_billingAddress_state_text'); } else { - $this->state = $this->byXpath("//div[@id='s2id_orocrm_account_form_values_billing_address_address_state']/a"); + $this->state = $this->byXpath("//div[@id='s2id_orocrm_account_form_billingAddress_state']/a"); } return $this; @@ -43,18 +39,45 @@ public function setAccountName($accountname) return $this; } + public function setOwner($owner) + { + $this->owner->click(); + $this->waitForAjax(); + $this->byXpath("//div[@id='select2-drop']/div/input")->value($owner); + $this->waitForAjax(); + $this->assertElementPresent( + "//div[@id='select2-drop']//div[contains(., '{$owner}')]", + "Owner autocoplete doesn't return search value" + ); + $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$owner}')]")->click(); + + return $this; + + } + + public function getOwner() + { + return; + } + public function verifyTag($tag) { - if ($this->isElementPresent("//div[@id='s2id_orocrm_account_form_tags']")) { - $this->tags = $this->byXpath("//div[@id='s2id_orocrm_account_form_tags']//input"); + if ($this->isElementPresent("//div[@id='s2id_orocrm_account_form_tags_autocomplete']")) { + $this->tags = $this->byXpath("//div[@id='s2id_orocrm_account_form_tags_autocomplete']//input"); $this->tags->click(); $this->tags->value(substr($tag, 0, (strlen($tag)-1))); $this->waitForAjax(); - $this->assertElementPresent("//div[@id='select2-drop']//div[contains(., '{$tag}')]", "Tag's autocoplete doesn't return entity"); + $this->assertElementPresent( + "//div[@id='select2-drop']//div[contains(., '{$tag}')]", + "Tag's autocoplete doesn't return entity" + ); $this->tags->clear(); } else { if ($this->isElementPresent("//div[@id='tags-holder']")) { - $this->assertElementPresent("//div[@id='tags-holder'][contains(., '{$tag}')]", 'Tag is not assigned to entity'); + $this->assertElementPresent( + "//div[@id='tags-holder']//li[contains(., '{$tag}')]", + 'Tag is not assigned to entity' + ); } else { throw new \Exception("Tag field can't be found"); } @@ -62,17 +85,28 @@ public function verifyTag($tag) return $this; } + /** + * @param $tag + * @return $this + * @throws \Exception + */ public function setTag($tag) { - $this->isElementPresent("//div[@id='s2id_orocrm_account_form_tags']"); - $this->tags = $this->byXpath("//div[@id='s2id_orocrm_account_form_tags']//input"); - $this->tags->click(); - $this->tags->value($tag); - $this->waitForAjax(); - $this->assertElementPresent("//div[@id='select2-drop']//div[contains(., '{$tag}')]", "Tag's autocoplete doesn't return entity"); - $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$tag}')]")->click(); + if ($this->isElementPresent("//div[@id='s2id_orocrm_account_form_tags_autocomplete']")) { + $this->tags = $this->byXpath("//div[@id='s2id_orocrm_account_form_tags_autocomplete']//input"); + $this->tags->click(); + $this->tags->value($tag); + $this->waitForAjax(); + $this->assertElementPresent( + "//div[@id='select2-drop']//div[contains(., '{$tag}')]", + "Tag's autocoplete doesn't return entity" + ); + $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$tag}')]")->click(); - return $this; + return $this; + } else { + throw new \Exception("Tag field can't be found"); + } } public function getAccountName() @@ -119,10 +153,10 @@ public function setCountry($country) public function setState($state) { - if ($this->byId('orocrm_account_form_values_billing_address_address_state_text')->displayed()) { - $this->state = $this->byId('orocrm_account_form_values_billing_address_address_state_text'); + if ($this->byId('orocrm_account_form_billingAddress_state_text')->displayed()) { + $this->state = $this->byId('orocrm_account_form_billingAddress_state_text'); } else { - $this->state = $this->byXpath("//div[@id='s2id_orocrm_account_form_values_billing_address_address_state']/a"); + $this->state = $this->byXpath("//div[@id='s2id_orocrm_account_form_billingAddress_state']/a"); } $this->state->click(); diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/BusinessUnit.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/BusinessUnit.php new file mode 100644 index 00000000000..2a2f42f43e6 --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/BusinessUnit.php @@ -0,0 +1,68 @@ +businessunitname = $this->byId('oro_business_unit_form_name'); + $this->owner = $this->select($this->byId('oro_business_unit_form_owner')); + + return $this; + } + + /** + * @param $unitname + * @return $this + */ + public function setBusinessUnitName($unitname) + { + $this->businessunitname->clear(); + $this->businessunitname->value($unitname); + return $this; + } + + /** + * @return string + */ + public function getBusinessUnitName() + { + return $this->businessunitname->value(); + } + + public function setOwner($owner) + { + $this->owner->selectOptionByLabel($owner); + + return $this; + } + + public function getOwner() + { + return trim($this->owner->selectedLabel()); + } + + public function edit() + { + $this->byXPath("//div[@class='pull-left btn-group icons-holder']/a[@title = 'Edit business unit']")->click(); + $this->waitPageToLoad(); + $this->waitForAjax(); + $this->init(); + return $this; + } + + public function delete() + { + $this->byXPath("//div[@class='pull-left btn-group icons-holder']/a[contains(., 'Delete')]")->click(); + $this->byXPath("//div[div[contains(., 'Delete Confirmation')]]//a[text()='Yes, Delete']")->click(); + $this->waitPageToLoad(); + $this->waitForAjax(); + return new BusinessUnits($this->test, false); + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/BusinessUnits.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/BusinessUnits.php new file mode 100644 index 00000000000..ef2ddb7c065 --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/BusinessUnits.php @@ -0,0 +1,59 @@ +redirectUrl = self::URL; + parent::__construct($testCase, $redirect); + } + + public function add() + { + $this->test->byXPath("//div[@class = 'container-fluid']//a[contains(., 'Create business unit')]")->click(); + //due to bug BAP-965 + sleep(1); + $this->waitPageToLoad(); + $this->waitForAjax(); + $businessunit = new BusinessUnit($this->test); + return $businessunit->init(); + } + + /** + * @param array $entityData + * @return BusinessUnit + */ + public function open($entityData = array()) + { + $contact = $this->getEntity($entityData); + $contact->click(); + sleep(1); + $this->waitPageToLoad(); + $this->waitForAjax(); + return new BusinessUnit($this->test); + } + + /** + * @param $unitname + * @param $contextname + * @return $this + */ + public function checkContextMenu($unitname, $contextname) + { + $this->filterBy('Name', $unitname); + $this->waitForAjax(); + if ($this->isElementPresent("//td[@class='action-cell']//a[contains(., '...')]")) { + $this->byXPath("//td[@class='action-cell']//a[contains(., '...')]")->click(); + $this->waitForAjax(); + return $this->assertElementNotPresent("//td[@class='action-cell']//a[@title= '{$contextname}']"); + } + + return $this; + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Contact.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Contact.php index c25ffcd965b..38cf010421a 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Contact.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Contact.php @@ -14,6 +14,7 @@ class Contact extends AbstractEntity implements Entity protected $email; protected $assignedto; protected $reportsto; + protected $addressCollection; public function __construct($testCase, $redirect = true) { @@ -22,15 +23,15 @@ public function __construct($testCase, $redirect = true) public function init() { - $this->nameprefix = $this->byId('orocrm_contact_form_values_name_prefix_varchar'); - $this->firstname = $this->byId('orocrm_contact_form_values_first_name_varchar'); - $this->lastname = $this->byId('orocrm_contact_form_values_last_name_varchar'); - $this->namesuffix = $this->byId('orocrm_contact_form_values_name_suffix_varchar'); - $this->email = $this->byId('orocrm_contact_form_values_email_varchar'); - $this->assignedto = $this->byXpath("//div[@id='s2id_orocrm_contact_form_values_assigned_to_user']/a"); - $this->reportsto = $this->byXpath("//div[@id='s2id_orocrm_contact_form_values_reports_to_contact']/a"); - $this->tags = $this->byXpath("//div[@id='s2id_orocrm_contact_form_tags']"); + $this->nameprefix = $this->byId('orocrm_contact_form_namePrefix'); + $this->firstname = $this->byId('orocrm_contact_form_firstName'); + $this->lastname = $this->byId('orocrm_contact_form_lastName'); + $this->namesuffix = $this->byId('orocrm_contact_form_nameSuffix'); + $this->email = $this->byId('orocrm_contact_form_emails_0_email'); + $this->assignedto = $this->byXpath("//div[@id='s2id_orocrm_contact_form_assignedTo']/a"); + $this->reportsto = $this->byXpath("//div[@id='s2id_orocrm_contact_form_reportsTo']/a"); $this->addressCollection = $this->byId('orocrm_contact_form_addresses_collection'); + $this->owner = $this->byXpath("//div[@id='s2id_orocrm_contact_form_owner']/a"); return $this; } @@ -59,6 +60,27 @@ public function getLastName() return $this->lastname->value(); } + public function setOwner($owner) + { + $this->owner->click(); + $this->waitForAjax(); + $this->byXpath("//div[@id='select2-drop']/div/input")->value($owner); + $this->waitForAjax(); + $this->assertElementPresent( + "//div[@id='select2-drop']//div[contains(., '{$owner}')]", + "Owner autocoplete doesn't return search value" + ); + $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$owner}')]")->click(); + + return $this; + + } + + public function getOwner() + { + return; + } + public function setEmail($email) { $this->email->clear(); @@ -73,16 +95,22 @@ public function getEmail() public function verifyTag($tag) { - if ($this->isElementPresent("//div[@id='s2id_orocrm_contact_form_tags']")) { - $this->tags = $this->byXpath("//div[@id='s2id_orocrm_contact_form_tags']//input"); + if ($this->isElementPresent("//div[@id='s2id_orocrm_contact_form_tags_autocomplete']")) { + $this->tags = $this->byXpath("//div[@id='s2id_orocrm_contact_form_tags_autocomplete']//input"); $this->tags->click(); $this->tags->value(substr($tag, 0, (strlen($tag)-1))); $this->waitForAjax(); - $this->assertElementPresent("//div[@id='select2-drop']//div[contains(., '{$tag}')]", "Tag's autocoplete doesn't return entity"); + $this->assertElementPresent( + "//div[@id='select2-drop']//div[contains(., '{$tag}')]", + "Tag's autocoplete doesn't return entity" + ); $this->tags->clear(); } else { if ($this->isElementPresent("//div[@id='tags-holder']")) { - $this->assertElementPresent("//div[@id='tags-holder'][contains(., '{$tag}')]", 'Tag is not assigned to entity'); + $this->assertElementPresent( + "//div[@id='tags-holder']//li[contains(., '{$tag}')]", + 'Tag is not assigned to entity' + ); } else { throw new \Exception("Tag field can't be found"); } @@ -90,17 +118,28 @@ public function verifyTag($tag) return $this; } + /** + * @param $tag + * @return $this + * @throws \Exception + */ public function setTag($tag) { - $this->isElementPresent("//div[@id='s2id_orocrm_contact_form_tagss']"); - $this->tags = $this->byXpath("//div[@id='s2id_orocrm_contact_form_tags']//input"); - $this->tags->click(); - $this->tags->value($tag); - $this->waitForAjax(); - $this->assertElementPresent("//div[@id='select2-drop']//div[contains(., '{$tag}')]", "Tag's autocoplete doesn't return entity"); - $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$tag}')]")->click(); + if ($this->isElementPresent("//div[@id='s2id_orocrm_contact_form_tags_autocomplete']")) { + $this->tags = $this->byXpath("//div[@id='s2id_orocrm_contact_form_tags_autocomplete']//input"); + $this->tags->click(); + $this->tags->value($tag); + $this->waitForAjax(); + $this->assertElementPresent( + "//div[@id='select2-drop']//div[contains(., '{$tag}')]", + "Tag's autocoplete doesn't return entity" + ); + $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$tag}')]")->click(); - return $this; + return $this; + } else { + throw new \Exception("Tag field can't be found"); + } } public function setAddressTypes($values, $addressId = 0) @@ -266,7 +305,7 @@ public function setAddress($data, $addressId = 0) ) ) { //click Add - $this->byXpath("//a[@class='btn add-list-item']")->click(); + $this->byXpath("//div[@class='row-oro'][div[@id='orocrm_contact_form_addresses_collection']]//a[@class='btn add-list-item']")->click(); $this->waitForAjax(); } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/EmailTemplate.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/EmailTemplate.php new file mode 100644 index 00000000000..10dccf1823e --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/EmailTemplate.php @@ -0,0 +1,114 @@ +entityname = $this->select($this->byId('oro_email_emailtemplate_entityName')); + $this->name = $this->byId('oro_email_emailtemplate_name'); + $this->type = $this->byId('oro_email_emailtemplate_type'); + $this->subject = $this->byId('oro_email_emailtemplate_translations_defaultLocale_en_subject'); + $this->content = $this->byId('oro_email_emailtemplate_translations_defaultLocale_en_content'); + } + + /** + * @param $entityname + * @return $this + */ + public function setEntityName($entityname) + { + $this->entityname->selectOptionByLabel($entityname); + return $this; + } + + public function getEntityName() + { + return $this->entityname->selectedLabel(); + } + + /** + * @param $name + * @return $this + */ + public function setName($name) + { + $this->name->clear(); + $this->name->value($name); + return $this; + } + + public function getName() + { + return $this->name->attribute('value'); + } + + /** + * @param string $type + * @return $this + */ + public function setType($type) + { + $this->type->element($this->using('xpath')->value("div[label[text() = '{$type}']]/input"))->click(); + return $this; + } + + public function getType() + { + return $this->byXPath("//div[@id='oro_email_emailtemplate_type']/div[input[@checked = 'checked']]/label")->text(); + } + + /** + * @param $subject + * @return $this + */ + public function setSubject($subject) + { + $this->subject->clear(); + $this->subject->value($subject); + return $this; + } + + public function getSubject() + { + return $this->subject->attribute('value'); + } + + /** + * @param $content + * @return $this + */ + public function setContent($content) + { + $this->content->clear(); + $this->content->value($content); + return $this; + } + + public function getContent() + { + return $this->content->attribute('value'); + } + + public function getFields(&$values) + { + $values['entityname'] = $this->getEntityName(); + $values['type'] = $this->getType(); + $values['name'] = $this->getName(); + $values['subject'] = $this->getSubject(); + $values['content'] = $this->getContent(); + + return $this; + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/EmailTemplates.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/EmailTemplates.php new file mode 100644 index 00000000000..e8f7820fb94 --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/EmailTemplates.php @@ -0,0 +1,94 @@ +redirectUrl = self::URL; + parent::__construct($testCase, $redirect); + } + + /** + * @return EmailTemplate + */ + public function add() + { + $this->test->byXPath("//div[@class = 'container-fluid']//a[contains(., 'Create template')]")->click(); + //due to bug BAP-965 + sleep(1); + $this->waitPageToLoad(); + $this->waitForAjax(); + + return new EmailTemplate($this->test); + } + + /** + * @param array $entityData + * @return EmailTemplate + */ + public function open($entityData = array()) + { + $emailtemplate = $this->getEntity($entityData); + $emailtemplate->click(); + sleep(1); + $this->waitPageToLoad(); + $this->waitForAjax(); + return new EmailTemplate($this->test); + } + + /** + * @param $filterby + * @param $entityname + * @return $this + */ + public function delete($filterby, $entityname) + { + $this->filterBy($filterby, $entityname); + $this->waitForAjax(); + $this->byXPath("//td[@class='action-cell']//a[contains(., '...')]")->click(); + $this->waitForAjax(); + $this->byXpath("//td[@class='action-cell']//a[@title= 'Delete']")->click(); + $this->waitForAjax(); + $this->byXPath("//div[div[contains(., 'Delete Confirmation')]]//a[text()='Yes, Delete']")->click(); + $this->waitPageToLoad(); + $this->waitForAjax(); + + return $this; + } + + public function cloneEntity($filterby, $entityname) + { + $this->filterBy($filterby, $entityname); + $this->waitForAjax(); + $this->byXPath("//td[@class='action-cell']//a[contains(., '...')]")->click(); + $this->waitForAjax(); + $this->byXpath("//td[@class='action-cell']//a[@title= 'Clone']")->click(); + $this->waitPageToLoad(); + $this->waitForAjax(); + return new EmailTemplate($this->test); + } + + /** + * @param $entityname + * @param $contextname + * @return $this + */ + public function checkContextMenu($entityname, $contextname) + { + $this->filterBy('Recipient email', $entityname); + $this->waitForAjax(); + if ($this->isElementPresent("//td[@class='action-cell']//a[contains(., '...')]")) { + $this->byXPath("//td[@class='action-cell']//a[contains(., '...')]")->click(); + $this->waitForAjax(); + return $this->assertElementNotPresent("//td[@class='action-cell']//a[@title= '{$contextname}']"); + } + + return $this; + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Group.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Group.php index 499bd1abf6c..195b47cc616 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Group.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Group.php @@ -17,6 +17,7 @@ public function __construct($testCase, $redirect = true) parent::__construct($testCase, $redirect); $this->name = $this->byId('oro_user_group_form_name'); $this->roles = $this->select($this->byId('oro_user_group_form_roles')); + $this->owner = $this->select($this->byId('oro_user_group_form_owner')); } public function setName($name) @@ -30,6 +31,18 @@ public function getName() return $this->name->value(); } + public function setOwner($owner) + { + $this->owner->selectOptionByLabel($owner); + + return $this; + } + + public function getOwner() + { + return trim($this->owner->selectedLabel()); + } + public function setRoles($roles = array()) { foreach ($roles as $role) { diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Login.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Login.php index 54b5370e786..d4c4dfd6164 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Login.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Login.php @@ -6,7 +6,7 @@ class Login extends Page { - public function __construct($testCase, $args = array()) + public function __construct($testCase, $args = array('url' => '/')) { if (array_key_exists('url', $args)) { $this->redirectUrl = $args['url']; diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Role.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Role.php index f06b6ab52d6..75d97c5550a 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Role.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Role.php @@ -17,6 +17,7 @@ public function __construct($testCase, $redirect = true) parent::__construct($testCase, $redirect); $this->name = $this->byId('oro_user_role_form_role'); $this->label = $this->byId('oro_user_role_form_label'); + $this->owner = $this->select($this->byId('oro_user_role_form_owner')); } public function setName($name) @@ -41,6 +42,18 @@ public function getLabel() return $this->label->value(); } + public function setOwner($owner) + { + $this->owner->selectOptionByLabel($owner); + + return $this; + } + + public function getOwner() + { + return trim($this->owner->selectedLabel()); + } + public function selectAcl($aclName) { $this->byXPath("//div[@id='acl_tree']//a[contains(., '$aclName')]/ins[@class='jstree-checkbox']")->click(); diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tag.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tag.php index 37b45e405dd..26df24db4a6 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tag.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tag.php @@ -14,10 +14,12 @@ public function __construct($testCase, $redirect = true) parent::__construct($testCase, $redirect); } - public function init() + public function init($new = true) { - $this->tagname = $this->byId('oro_tag_tag_form_name'); - + if ($new) { + $this->tagname = $this->byId('oro_tag_tag_form_name'); + $this->owner = $this->byXpath("//div[@id='s2id_oro_tag_tag_form_owner']/a"); + } return $this; } @@ -33,6 +35,27 @@ public function getTagname() return $this->tagname->value(); } + public function setOwner($owner) + { + $this->owner->click(); + $this->waitForAjax(); + $this->byXpath("//div[@id='select2-drop']/div/input")->value($owner); + $this->waitForAjax(); + $this->assertElementPresent( + "//div[@id='select2-drop']//div[contains(., '{$owner}')]", + "Owner autocoplete doesn't return search value" + ); + $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$owner}')]")->click(); + + return $this; + + } + + public function getOwner() + { + return; + } + public function save() { $this->byXPath("//button[contains(., 'Save')]")->click(); diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tags.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tags.php index 3f9d4bde9ea..3ee14f123b9 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tags.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/Tags.php @@ -14,7 +14,7 @@ public function __construct($testCase, $redirect = true) parent::__construct($testCase, $redirect); } - public function add() + public function add($new = true) { $this->test->byXPath("//div[@class = 'container-fluid']//a[contains(., 'Create tag')]")->click(); //due to bug BAP-965 @@ -22,7 +22,7 @@ public function add() $this->waitPageToLoad(); $this->waitForAjax(); $tag = new Tag($this->test); - return $tag->init(); + return $tag->init($new); } public function open($entityData = array()) @@ -59,4 +59,12 @@ public function delete() return $this; } + + public function checkContextMenu($tagname, $contextname) + { + $this->filterBy('Tag', $tagname); + $this->byXPath("//td[@class='action-cell']//a[contains(., '...')]")->click(); + $this->waitForAjax(); + $this->assertElementNotPresent("//td[@class='action-cell']//a[@title= '{$contextname}']"); + } } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/TransactionEmail.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/TransactionEmail.php new file mode 100644 index 00000000000..4d6194df23b --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/TransactionEmail.php @@ -0,0 +1,107 @@ +entityname = $this->select($this->byId('emailnotification_entityName')); + $this->event = $this->select($this->byId('emailnotification_event')); + $this->template = $this->byXpath("//div[@id='s2id_emailnotification_template']/a"); + $this->user = $this->byXpath("//div[@id='s2id_emailnotification_recipientList_users']//input"); + $this->groups = $this->byId('emailnotification_recipientList_groups'); + $this->email = $this->byId('emailnotification_recipientList_email'); + } + + /** + * @param $entityname + * @return $this + */ + public function setEntityName($entityname) + { + $this->entityname->selectOptionByLabel($entityname); + $this->waitForAjax(); + return $this; + } + + /** + * @param $event + * @return $this + */ + public function setEvent($event) + { + $this->event->selectOptionByLabel($event); + return $this; + } + + /** + * @param $template + * @return $this + */ + public function setTemplate($template) + { + $this->template->click(); + $this->waitForAjax(); + $this->byXpath("//div[@id='select2-drop']/div/input")->value($template); + $this->waitForAjax(); + $this->assertElementPresent("//div[@id='select2-drop']//div[contains(., '{$template}')]", "Template autocoplete doesn't return search value"); + $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$template}')]")->click(); + + return $this; + } + + /** + * @param $user + * @return $this + */ + public function setUser($user) + { + $this->user->click(); + $this->user->value($user); + $this->waitForAjax(); + $this->assertElementPresent( + "//div[@id='select2-drop']//div[contains(., '{$user}')]", + "Users autocoplete field doesn't return entity" + ); + $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$user}')]")->click(); + + return $this; + } + + /** + * @param array $groups + * @return $this + */ + public function setGroups($groups = array()) + { + foreach ($groups as $group) { + $this->groups->element($this->using('xpath')->value("div[label[text() = '{$group}']]/input"))->click(); + } + + return $this; + } + + /** + * @param $email + * @return $this + */ + public function setEmail($email) + { + $this->email->clear(); + $this->email->value($email); + + return $this; + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/TransactionEmails.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/TransactionEmails.php new file mode 100644 index 00000000000..17fffa1b2ea --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/TransactionEmails.php @@ -0,0 +1,77 @@ +redirectUrl = self::URL; + parent::__construct($testCase, $redirect); + } + + /** + * @return TransactionsEmail + */ + public function add() + { + $this->test->byXPath("//div[@class = 'container-fluid']//a[contains(., 'Create notification rule')]")->click(); + //due to bug BAP-965 + sleep(1); + $this->waitPageToLoad(); + $this->waitForAjax(); + + return new TransactionEmail($this->test); + } + + /** + * @param array $entityData + * @return TransactionsEmail + */ + public function open($entityData = array()) + { + $transactionemail = $this->getEntity($entityData); + $transactionemail->click(); + sleep(1); + $this->waitPageToLoad(); + $this->waitForAjax(); + return new TransactionEmail($this->test); + } + + public function delete($filterby, $entityname) + { + $this->filterBy($filterby, $entityname); + $this->waitForAjax(); + $this->byXPath("//td[@class='action-cell']//a[contains(., '...')]")->click(); + $this->waitForAjax(); + $this->byXpath("//td[@class='action-cell']//a[@title= 'Delete']")->click(); + $this->waitForAjax(); + $this->byXPath("//div[div[contains(., 'Delete Confirmation')]]//a[text()='Yes, Delete']")->click(); + $this->waitPageToLoad(); + $this->waitForAjax(); + + return $this; + } + + /** + * @param $entityname + * @param $contextname + * @return $this + */ + public function checkContextMenu($entityname, $contextname) + { + $this->filterBy('Recipient email', $entityname); + $this->waitForAjax(); + if ($this->isElementPresent("//td[@class='action-cell']//a[contains(., '...')]")) { + $this->byXPath("//td[@class='action-cell']//a[contains(., '...')]")->click(); + $this->waitForAjax(); + return $this->assertElementNotPresent("//td[@class='action-cell']//a[@title= '{$contextname}']"); + } + + return $this; + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/User.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/User.php index bfbe865d11a..1520d2f8a38 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/User.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Objects/User.php @@ -26,11 +26,6 @@ class User extends AbstractEntity implements Entity protected $gender; protected $website; - public function __construct($testCase, $redirect = true) - { - parent::__construct($testCase, $redirect); - } - public function init($new = false) { $this->username = $this->byId('oro_user_user_form_username'); @@ -44,9 +39,23 @@ public function init($new = false) $this->email = $this->byId('oro_user_user_form_email'); $this->groups = $this->byId('oro_user_user_form_groups'); $this->roles = $this->byId('oro_user_user_form_rolesCollection'); + $this->owner = $this->select($this->byId('oro_user_user_form_owner')); return $this; } + + public function setOwner($owner) + { + $this->owner->selectOptionByLabel($owner); + + return $this; + } + + public function getOwner() + { + return trim($this->owner->selectedLabel()); + } + public function setUsername($name) { $this->username->clear(); @@ -133,16 +142,22 @@ public function getEmail() public function verifyTag($tag) { - if ($this->isElementPresent("//div[@id='s2id_oro_user_user_form_tags']")) { - $this->tags = $this->byXpath("//div[@id='s2id_oro_user_user_form_tags']//input"); + if ($this->isElementPresent("//div[@id='s2id_oro_user_user_form_tags_autocomplete']")) { + $this->tags = $this->byXpath("//div[@id='s2id_oro_user_user_form_tags_autocomplete']//input"); $this->tags->click(); $this->tags->value(substr($tag, 0, (strlen($tag)-1))); $this->waitForAjax(); - $this->assertElementPresent("//div[@id='select2-drop']//div[contains(., '{$tag}')]", "Tag's autocoplete doesn't return entity"); + $this->assertElementPresent( + "//div[@id='select2-drop']//div[contains(., '{$tag}')]", + "Tag's autocoplete doesn't return entity" + ); $this->tags->clear(); } else { if ($this->isElementPresent("//div[@id='tags-holder']")) { - $this->assertElementPresent("//div[@id='tags-holder'][contains(., '{$tag}')]", 'Tag is not assigned to entity'); + $this->assertElementPresent( + "//div[@id='tags-holder']//li[contains(., '{$tag}')]", + 'Tag is not assigned to entity' + ); } else { throw new \Exception("Tag field can't be found"); } @@ -150,17 +165,28 @@ public function verifyTag($tag) return $this; } + /** + * @param $tag + * @return $this + * @throws \Exception + */ public function setTag($tag) { - $this->isElementPresent("//div[@id='s2id_oro_user_user_form_tags']"); - $this->tags = $this->byXpath("//div[@id='s2id_oro_user_user_form_tags']//input"); - $this->tags->click(); - $this->tags->value($tag); - $this->waitForAjax(); - $this->assertElementPresent("//div[@id='select2-drop']//div[contains(., '{$tag}')]", "Tag's autocoplete doesn't return entity"); - $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$tag}')]")->click(); + if ($this->isElementPresent("//div[@id='s2id_oro_user_user_form_tags_autocomplete']")) { + $this->tags = $this->byXpath("//div[@id='s2id_oro_user_user_form_tags_autocomplete']//input"); + $this->tags->click(); + $this->tags->value($tag); + $this->waitForAjax(); + $this->assertElementPresent( + "//div[@id='select2-drop']//div[contains(., '{$tag}')]", + "Tag's autocoplete doesn't return entity" + ); + $this->byXpath("//div[@id='select2-drop']//div[contains(., '{$tag}')]")->click(); - return $this; + return $this; + } else { + throw new \Exception("Tag field can't be found"); + } } public function setRoles($roles = array()) diff --git a/src/Oro/Bundle/TestFrameworkBundle/Pages/Page.php b/src/Oro/Bundle/TestFrameworkBundle/Pages/Page.php index 95bbc9faff0..89c8d4bb62a 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Pages/Page.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Pages/Page.php @@ -2,6 +2,7 @@ namespace Oro\Bundle\TestFrameworkBundle\Pages; +use Oro\Bundle\TestFrameworkBundle\Pages\Objects\Login; use PHPUnit_Framework_Assert; use Oro\Bundle\TestFrameworkBundle\Pages\Objects\Users; use Oro\Bundle\TestFrameworkBundle\Pages\Objects\Roles; @@ -203,4 +204,10 @@ protected function clearInput($element) $tx = $element->value(); } } + + public function logout() + { + $this->url('/user/logout'); + return new Login($this->test); + } } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Resources/public/scripts/phpunit_coverage.php b/src/Oro/Bundle/TestFrameworkBundle/Resources/public/scripts/phpunit_coverage.php index d9058b24e71..6a2b1050c9b 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Resources/public/scripts/phpunit_coverage.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Resources/public/scripts/phpunit_coverage.php @@ -48,7 +48,12 @@ // Set this to the directory that contains the code coverage files. // It defaults to getcwd(). If you have configured a different directory // in prepend.php, you need to configure the same directory here. -$GLOBALS['PHPUNIT_COVERAGE_DATA_DIRECTORY'] = realpath($_SERVER['DOCUMENT_ROOT'] . '/../app/logs'); +if (!isset($GLOBALS['PHPUNIT_COVERAGE_DATA_DIRECTORY'])) { + $PHPUNIT_COVERAGE_DATA_DIRECTORY = realpath( + $_SERVER['DOCUMENT_ROOT'] . DIRECTORY_SEPARATOR . '..' . + DIRECTORY_SEPARATOR . 'app' . DIRECTORY_SEPARATOR . 'logs' + ); +} if (isset($_GET['PHPUNIT_SELENIUM_TEST_ID'])) { $facade = new File_Iterator_Facade; @@ -61,7 +66,7 @@ foreach ($files as $file) { $data = unserialize(file_get_contents($file)); - unlink($file); + //unlink($file); unset($file); $filter = new PHP_CodeCoverage_Filter(); diff --git a/src/Oro/Bundle/TestFrameworkBundle/Test/BehatSeleniumContext.php b/src/Oro/Bundle/TestFrameworkBundle/Test/BehatSeleniumContext.php new file mode 100644 index 00000000000..77c67e30080 --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Test/BehatSeleniumContext.php @@ -0,0 +1,105 @@ +setParentContext($this); + $this->subcontexts[$alias] = $context; + } + + /** + * @see Behat\Behat\Context\ExtendedContextInterface::setParentContext() + * @see Behat\Behat\Context\BehatContext::useContext() + */ + public function setParentContext(ExtendedContextInterface $parentContext) + { + $this->parentContext = $parentContext; + } + + /** + * @see Behat\Behat\Context\ExtendedContextInterface::getMainContext() + */ + public function getMainContext() + { + if (null !== $this->parentContext) { + return $this->parentContext->getMainContext(); + } + + return $this; + } + + /** + * @see Behat\Behat\Context\ExtendedContextInterface::getSubcontext() + */ + public function getSubcontext($alias) + { + // search in current context subcontexts + if (isset($this->subcontexts[$alias])) { + return $this->subcontexts[$alias]; + } + + // search in subcontexts childs contexts + foreach ($this->subcontexts as $subcontext) { + if (null !== $context = $subcontext->getSubcontext($alias)) { + return $context; + } + } + } + + /** + * @see Behat\Behat\Context\ContextInterface::getSubcontexts() + */ + public function getSubcontexts() + { + return $this->subcontexts; + } + + /** + * @see Behat\Behat\Context\ContextInterface::getSubcontextByClassName() + */ + public function getSubcontextByClassName($className) + { + foreach ($this->getSubcontexts() as $subcontext) { + if (get_class($subcontext) === $className) { + return $subcontext; + } + if ($context = $subcontext->getSubcontextByClassName($className)) { + return $context; + } + } + } + + /** + * Prints beautified debug string. + * + * @param string $string debug string + */ + public function printDebug($string) + { + echo "\n\033[36m| " . strtr($string, array("\n" => "\n| ")) . "\033[0m\n\n"; + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Test/BehatTestCase.php b/src/Oro/Bundle/TestFrameworkBundle/Test/BehatTestCase.php new file mode 100644 index 00000000000..4c01d34048a --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Test/BehatTestCase.php @@ -0,0 +1,68 @@ +getKernel(); + $application = new \Symfony\Bundle\FrameworkBundle\Console\Application($kernel); + $application->setAutoExit(false); + self::$internalApplication = $application; + + $command = new BehatCommand(); + $command->setApplication($application); + self::$internalCommand = $command; + return $client; + } + + protected function runFeature($path = '', $feature = null, $parameters = array(), $config = 'behat.yml') + { + $parameters = array_merge( + $parameters, + array('behat', '-f' => 'progress', '-v' => '', '-c' => $path . DIRECTORY_SEPARATOR . $config) + ); + + if (!$feature) { + $parameters = array_merge($parameters, array('features' => $feature)); + } + + try { + $input = new ArrayInput($parameters); + $output = new ConsoleOutput(); + $result = self::$internalCommand->run($input, $output); + $this->assertEquals(0, $result); + } catch (\Exception $exception) { + $this->fail($exception->getMessage()); + } + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Test/BehatWebContext.php b/src/Oro/Bundle/TestFrameworkBundle/Test/BehatWebContext.php new file mode 100644 index 00000000000..331ddd5c520 --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Test/BehatWebContext.php @@ -0,0 +1,105 @@ +setParentContext($this); + $this->subcontexts[$alias] = $context; + } + + /** + * @see Behat\Behat\Context\ExtendedContextInterface::setParentContext() + * @see Behat\Behat\Context\BehatContext::useContext() + */ + public function setParentContext(ExtendedContextInterface $parentContext) + { + $this->parentContext = $parentContext; + } + + /** + * @see Behat\Behat\Context\ExtendedContextInterface::getMainContext() + */ + public function getMainContext() + { + if (null !== $this->parentContext) { + return $this->parentContext->getMainContext(); + } + + return $this; + } + + /** + * @see Behat\Behat\Context\ExtendedContextInterface::getSubcontext() + */ + public function getSubcontext($alias) + { + // search in current context subcontexts + if (isset($this->subcontexts[$alias])) { + return $this->subcontexts[$alias]; + } + + // search in subcontexts childs contexts + foreach ($this->subcontexts as $subcontext) { + if (null !== $context = $subcontext->getSubcontext($alias)) { + return $context; + } + } + } + + /** + * @see Behat\Behat\Context\ContextInterface::getSubcontexts() + */ + public function getSubcontexts() + { + return $this->subcontexts; + } + + /** + * @see Behat\Behat\Context\ContextInterface::getSubcontextByClassName() + */ + public function getSubcontextByClassName($className) + { + foreach ($this->getSubcontexts() as $subcontext) { + if (get_class($subcontext) === $className) { + return $subcontext; + } + if ($context = $subcontext->getSubcontextByClassName($className)) { + return $context; + } + } + } + + /** + * Prints beautified debug string. + * + * @param string $string debug string + */ + public function printDebug($string) + { + echo "\n\033[36m| " . strtr($string, array("\n" => "\n| ")) . "\033[0m\n\n"; + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Test/Client.php b/src/Oro/Bundle/TestFrameworkBundle/Test/Client.php index e518cb03d66..9defdc3400a 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Test/Client.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Test/Client.php @@ -7,6 +7,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\TerminableInterface; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Driver\PDOConnection; class Client extends BaseClient { @@ -14,40 +15,42 @@ class Client extends BaseClient const LOCAL_URL = 'http://localhost'; /** @var SoapClient */ - public $soapClient; + static protected $soapClient; /** * @var \Symfony\Component\Routing\RouterInterface */ protected $router = null; - /** @var \Doctrine\DBAL\Connection shared doctrine connection */ - static protected $connection = null; + /** @var \Doctrine\DBAL\Driver\PDOConnection shared PDO connection */ + static protected $pdoConnection = null; protected $hasPerformedRequest; + /** + * @param \Symfony\Component\HttpKernel\KernelInterface $kernel + * @param array $server + * @param null $history + * @param null $cookieJar + */ public function __construct($kernel, array $server = array(), $history = null, $cookieJar = null) { parent::__construct($kernel, $server, $history, $cookieJar); - if (is_null(self::$connection)) { - self::$connection = $this->getContainer()->get('doctrine.dbal.default_connection'); - } $this->router = $this->getContainer()->get('router'); } public function __destruct() { - if (isset($this->soapClient)) { - unset($this->soapClient); - } - if (!is_null(self::$connection)) { - if (self::$connection->getTransactionNestingLevel()>0) { - self::$connection->rollback(); - } - self::$connection = null; - } + $this->setSoapClient(null); } + /** + * @param null|SoapClient $value + */ + public function setSoapClient($value) + { + self::$soapClient = $value; + } /** * @param $name * @param array $parameters @@ -59,41 +62,70 @@ public function generate($name, $parameters = array(), $absolute = false) return $this->router->generate($name, $parameters, $absolute); } - public function request($method, $uri, array $parameters = array(), array $files = array(), array $server = array(), $content = null, $changeHistory = true) - { + /** + * @param string $method + * @param string $uri + * @param array $parameters + * @param array $files + * @param array $server + * @param null $content + * @param bool $changeHistory + * @return \Symfony\Component\DomCrawler\Crawler + */ + public function request( + $method, + $uri, + array $parameters = array(), + array $files = array(), + array $server = array(), + $content = null, + $changeHistory = true + ) { if (strpos($uri, 'http://') === false) { $uri = self::LOCAL_URL . $uri; } return parent::request($method, $uri, $parameters, $files, $server, $content, $changeHistory); } + /** * @param null $wsdl * @param array $options + * @param bool $new * @throws \Exception */ - public function soap($wsdl = null, array $options = null) + public function soap($wsdl = null, array $options = null, $new = false) { - if (is_null($wsdl)) { - throw new \Exception('wsdl should not be NULL'); - } + if (!self::$soapClient || $new) { + if (is_null($wsdl)) { + throw new \Exception('wsdl should not be NULL'); + } - $this->request('GET', $wsdl); - $status = $this->getResponse()->getStatusCode(); - $statusText = Response::$statusTexts[$status]; - if ($status >= 400) { - throw new \Exception($statusText, $status); - } + $this->request('GET', $wsdl); + $status = $this->getResponse()->getStatusCode(); + $statusText = Response::$statusTexts[$status]; + if ($status >= 400) { + throw new \Exception($statusText, $status); + } + + $wsdl = $this->getResponse()->getContent(); + //save to file + $file=tempnam(sys_get_temp_dir(), date("Ymd") . '_') . '.xml'; + $fl = fopen($file, "w"); + fwrite($fl, $wsdl); + fclose($fl); - $wsdl = $this->getResponse()->getContent(); - //save to file - $file=tempnam(sys_get_temp_dir(), date("Ymd") . '_') . '.xml'; - $fl = fopen($file, "w"); - fwrite($fl, $wsdl); - fclose($fl); + self::$soapClient = new SoapClient($file, $options, $this); - $this->soapClient = new SoapClient($file, $options, $this); + unlink($file); + } + } - unlink($file); + /** + * @return SoapClient + */ + public function getSoap() + { + return self::$soapClient; } /** @@ -109,8 +141,10 @@ protected function doRequest($request) $this->hasPerformedRequest = true; } - if (!is_null(self::$connection)) { - $this->getContainer()->set('doctrine.dbal.default_connection', self::$connection); + if (self::$pdoConnection) { + /** @var \Doctrine\DBAL\Connection $connection */ + $connection = $this->createConnection(self::$pdoConnection); + $this->getContainer()->set('doctrine.dbal.default_connection', $connection); } $response = $this->kernel->handle($request); @@ -128,10 +162,14 @@ public function appendFixtures($folder) { $loader = new \Doctrine\Common\DataFixtures\Loader; $fixtures = $loader->loadFromDirectory($folder); + foreach ($fixtures as $fixture) { $fixture->setContainer($this->getContainer()); } - $purger = new \Doctrine\Common\DataFixtures\Purger\ORMPurger($this->getContainer()->get('doctrine.orm.entity_manager')); + + $purger = new \Doctrine\Common\DataFixtures\Purger\ORMPurger( + $this->getContainer()->get('doctrine.orm.entity_manager') + ); $executor = new \Doctrine\Common\DataFixtures\Executor\ORMExecutor( $this->getContainer()->get('doctrine.orm.entity_manager'), $purger @@ -141,23 +179,57 @@ public function appendFixtures($folder) public function startTransaction() { - self::$connection = $this->getContainer()->get('doctrine.dbal.default_connection'); + self::$pdoConnection = $this->getContainer()->get('doctrine.dbal.default_connection')->getWrappedConnection(); + self::$pdoConnection->beginTransaction(); + } - if (self::$connection->getTransactionNestingLevel()<1) { - self::$connection->beginTransaction(); - } + /** + * @return PDOConnection|null + */ + public static function getPdoConnection() + { + return self::$pdoConnection; } + /** + * @param PDOConnection $pdoConnection + * @return Connection + */ + public function createConnection($pdoConnection) + { + /** @var \Doctrine\DBAL\Connection $conn */ + $dbalConnection = $this->getContainer()->get('doctrine.dbal.default_connection'); + + $connection = $this->getContainer()->get('doctrine.dbal.connection_factory') + ->createConnection( + array_merge($dbalConnection->getParams(), array('pdo' => $pdoConnection)), + $dbalConnection->getConfiguration(), + $dbalConnection->getEventManager() + ); + + //increment transaction level + $reflection = new \ReflectionProperty('Doctrine\DBAL\Connection', '_transactionNestingLevel'); + $reflection->setAccessible(true); + $reflection->setValue($connection, $dbalConnection->getTransactionNestingLevel()+1); + + $dbalConnection = null; + + return $connection; + } + + /** + * @return integer + */ public static function getTransactionLevel() { - return self::$connection->getTransactionNestingLevel(); + return self::$pdoConnection->getTransactionNestingLevel(); } - public static function rollbackTransaction() + public function rollbackTransaction() { - if (!is_null(self::$connection)) { - self::$connection->rollback(); - self::$connection = null; + if (!is_null(self::$pdoConnection)) { + self::$pdoConnection->rollback(); + self::$pdoConnection = null; } } } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Test/TestListener.php b/src/Oro/Bundle/TestFrameworkBundle/Test/TestListener.php index 32494cec1b2..84320ea9984 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Test/TestListener.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Test/TestListener.php @@ -3,6 +3,7 @@ // @codingStandardsIgnoreStart class TestListener implements \PHPUnit_Framework_TestListener { + // @codingStandardsIgnoreEnd private $directory; @@ -63,8 +64,11 @@ public function startTest(\PHPUnit_Framework_Test $test) public function startTestSuite(\PHPUnit_Framework_TestSuite $suite) { - if ($suite instanceof PHPUnit_Extensions_SeleniumTestSuite) { - $this->runPhantom(); + $groups = $suite->getGroups(); + if ($suite instanceof PHPUnit_Extensions_SeleniumTestSuite || + in_array('selenium', $groups) + ) { + $this->runPhantom(); } } @@ -80,15 +84,17 @@ private function runPhantom() if (PHP_OS == 'WINNT') { pclose(popen("start /b " . PHPUNIT_TESTSUITE_BROWSER_PATH_WINNT . " --webdriver=" . PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT, "r")); } else { - shell_exec("nohup " . PHPUNIT_TESTSUITE_BROWSER_PATH_LINUX . " --webdriver=" . PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT . - " > /dev/null 2> /dev/null &"); + shell_exec( + "nohup " . PHPUNIT_TESTSUITE_BROWSER_PATH_LINUX . " --webdriver=" . PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT . + " > /dev/null 2> /dev/null &" + ); } } $this->waitServerRun(5, PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_HOST, PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT); } } - private function waitServerRun($timeOut = 5, $url = 'localhost', $port = '4444') + private function waitServerRun($timeOut = 5, $url = 'localhost', $port = '4444') { $running = false; $i = 0; diff --git a/src/Oro/Bundle/TestFrameworkBundle/Test/ToolsAPI.php b/src/Oro/Bundle/TestFrameworkBundle/Test/ToolsAPI.php index c2cc54a0348..e12adcb355d 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Test/ToolsAPI.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Test/ToolsAPI.php @@ -117,10 +117,12 @@ public static function assertJsonResponse($response, $statusCode = 201, $content $response->getContent() ); - \PHPUnit_Framework_TestCase::assertTrue( - $response->headers->contains('Content-Type', $contentType), - $response->headers - ); + if ($contentType !== '') { + \PHPUnit_Framework_TestCase::assertTrue( + $response->headers->contains('Content-Type', $contentType), + $response->headers + ); + } } /** @@ -166,6 +168,15 @@ public static function randomGen($length) return $random; } + /** + * @param int $length + * @return string + */ + public static function generateRandomString($length = 10) + { + return substr(str_shuffle("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"), 0, $length); + } + /** * @param $value * @param $key diff --git a/src/Oro/Bundle/TestFrameworkBundle/Test/WebTestCase.php b/src/Oro/Bundle/TestFrameworkBundle/Test/WebTestCase.php index 71859267c71..4b9b558a5b5 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Test/WebTestCase.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Test/WebTestCase.php @@ -4,8 +4,9 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase; use Oro\Bundle\TestFrameworkBundle\Test\Client; +use Doctrine\ORM\EntityManager; -class WebTestCase extends BaseWebTestCase +abstract class WebTestCase extends BaseWebTestCase { const DB_ISOLATION = '/@db_isolation(.*)(\r|\n)/U'; const DB_REINDEX = '/@db_reindex(.*)(\r|\n)/U'; @@ -13,6 +14,11 @@ class WebTestCase extends BaseWebTestCase static protected $db_isolation = false; static protected $db_reindex = false; + /** + * @var Client + */ + static protected $internalClient; + /** * Creates a Client. * @@ -23,31 +29,56 @@ class WebTestCase extends BaseWebTestCase */ protected static function createClient(array $options = array(), array $server = array()) { - /** @var \Oro\Bundle\TestFrameworkBundle\Test\Client $client */ - $client = parent::createClient($options, $server); - - if (self::$db_isolation && Client::getTransactionLevel() < 1) { - //workaround MyISAM search tables are not on transaction - if (self::$db_reindex) { - $kernel = $client->getKernel(); - $application = new \Symfony\Bundle\FrameworkBundle\Console\Application($kernel); - $application->setAutoExit(false); - $options = array('command' => 'oro:search:reindex'); - $options['--env'] = "test"; - $options['--quiet'] = null; - $application->run(new \Symfony\Component\Console\Input\ArrayInput($options)); - } + if (!self::$internalClient) { + self::$internalClient = parent::createClient($options, $server); + + if (self::$db_isolation) { + /** @var Client $client */ + $client = self::$internalClient; - $client->startTransaction(); + //workaround MyISAM search tables are not on transaction + if (self::$db_reindex) { + $kernel = $client->getKernel(); + $application = new \Symfony\Bundle\FrameworkBundle\Console\Application($kernel); + $application->setAutoExit(false); + $options = array('command' => 'oro:search:reindex'); + $options['--env'] = "test"; + $options['--quiet'] = null; + $application->run(new \Symfony\Component\Console\Input\ArrayInput($options)); + } + + $client->startTransaction(); + $pdoConnection = Client::getPdoConnection(); + if ($pdoConnection) { + //set transaction level to 1 for entityManager + $connection = $client->createConnection($pdoConnection); + $client->getContainer()->set('doctrine.dbal.default_connection', $connection); + + /** @var EntityManager $entityManager */ + $entityManager = $client->getContainer()->get('doctrine.orm.entity_manager'); + if (spl_object_hash($entityManager->getConnection()) != spl_object_hash($connection)) { + $reflection = new \ReflectionProperty('Doctrine\ORM\EntityManager', 'conn'); + $reflection->setAccessible(true); + $reflection->setValue($entityManager, $connection); + } + } + } } - return $client; + + return self::$internalClient; } public static function tearDownAfterClass() { - if (self::$db_isolation) { - Client::rollbackTransaction(); - self::$db_isolation = false; + if (self::$internalClient) { + /** @var Client $client */ + $client = self::$internalClient; + if (self::$db_isolation) { + $client->rollbackTransaction(); + self::$db_isolation = false; + } + $client->setSoapClient(null); + self::$internalClient = null; } } @@ -68,13 +99,24 @@ public static function setUpBeforeClass() } } + /** + * @return bool + */ public function getIsolation() { return self::$db_isolation; } + /** + * @param bool $dbIsolation + */ public function setIsolation($dbIsolation = false) { self::$db_isolation = $dbIsolation; } + + public static function getInstance() + { + return self::$internalClient; + } } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/AdvancedSearch/AdvancedSearchTest.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/AdvancedSearch/AdvancedSearchTest.php index dc576dc2f1e..67451145089 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/AdvancedSearch/AdvancedSearchTest.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/AdvancedSearch/AdvancedSearchTest.php @@ -40,9 +40,10 @@ public function testAdvancedSearch($query, $userField) //Fill advanced search input field $login->byId('query')->value($query . $userData[$userField]); $login->byId('sendButton')->click(); + $login->waitPageToLoad(); $login->waitForAjax(); //Check that result is not null - $result = strtolower($userData[$userField]); + $result = strtolower($userData['USERNAME']); $login->assertElementPresent( "//div[@class='container-fluid']//div[@class='search_stats alert alert-info']//h3[contains(., '{$result}')]", 'Search results does not found' diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/BusinessUnits/BusinessUnitsAclTest.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/BusinessUnits/BusinessUnitsAclTest.php new file mode 100644 index 00000000000..c9be618430d --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/BusinessUnits/BusinessUnitsAclTest.php @@ -0,0 +1,228 @@ +setHost(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_HOST); + $this->setPort(intval(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT)); + $this->setBrowser(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM2_BROWSER); + $this->setBrowserUrl(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_TESTS_URL); + } + + protected function tearDown() + { + $this->cookie()->clear(); + } + + public function testCreateRole() + { + $randomPrefix = mt_rand(); + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openRoles() + ->add() + ->setName('ROLE_NAME_' . $randomPrefix) + ->setLabel('Label_' . $randomPrefix) + ->setOwner('Main') + ->selectAcl('Root') + ->save() + ->assertMessage('Role successfully saved') + ->close(); + + return ($randomPrefix); + } + + /** + * @depends testCreateRole + * @param $role + * @return string + */ + public function testCreateUser($role) + { + $username = 'User_'.mt_rand(); + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openUsers() + ->add() + ->assertTitle('Create User - Users - System') + ->setUsername($username) + ->enable() + ->setOwner('Main') + ->setFirstpassword('123123q') + ->setSecondpassword('123123q') + ->setFirstname('First_'.$username) + ->setLastname('Last_'.$username) + ->setEmail($username.'@mail.com') + ->setRoles(array('Label_' . $role)) + ->save() + ->assertMessage('User successfully saved') + ->close() + ->assertTitle('Users - System'); + + return $username; + } + + /** + * @depends testCreateUser + * @return string + */ + public function testCreateBusinessUnit() + { + $unitname = 'Unit_'.mt_rand(); + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openBusinessUnits() + ->add() + ->assertTitle('Create Business Unit - Business Units - System') + ->setBusinessUnitName($unitname) + ->setOwner('Main') + ->save() + ->assertMessage('Business Unit successfully saved') + ->assertTitle('Business Units - System') + ->close(); + + return $unitname; + } + + /** + * @depends testCreateUser + * @depends testCreateRole + * @depends testCreateBusinessUnit + * @param $username + * @param $role + * @param $unitname + * @param string $aclcase + * @dataProvider columnTitle + */ + public function testBusinessUnitAcl($aclcase, $username, $role, $unitname) + { + $rolename = 'ROLE_NAME_' . $role; + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit(); + switch ($aclcase) { + case 'delete': + $this->deleteAcl($login, $rolename, $username, $unitname); + break; + case 'update': + $this->updateAcl($login, $rolename, $username, $unitname); + break; + case 'create': + $this->createAcl($login, $rolename, $username); + break; + case 'view': + $this->viewAcl($login, $username, $rolename, $unitname); + break; + case 'view list': + $this->viewListAcl($login, $rolename, $username); + break; + } + } + + public function deleteAcl($login, $rolename, $username, $unitname) + { + $login->openRoles() + ->filterBy('Role', $rolename) + ->open(array($rolename)) + ->selectAcl('Delete business unit') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openBusinessUnits() + ->checkContextMenu($unitname, 'Delete'); + } + + public function updateAcl($login, $rolename, $username, $unitname) + { + $login->openRoles() + ->filterBy('Role', $rolename) + ->open(array($rolename)) + ->selectAcl('Edit business unit') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openBusinessUnits() + ->checkContextMenu($unitname, 'Update'); + } + + public function createAcl($login, $rolename, $username) + { + $login->openRoles() + ->filterBy('Role', $rolename) + ->open(array($rolename)) + ->selectAcl('Create business unit') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openBusinessUnits() + ->assertElementNotPresent("//div[@class = 'container-fluid']//a[contains(., 'Create business unit')]"); + } + + public function viewAcl($login, $username, $rolename, $unitname) + { + $login->openRoles() + ->filterBy('Role', $rolename) + ->open(array($rolename)) + ->selectAcl('View business unit') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openBusinessUnits() + ->checkContextMenu($unitname, 'View'); + } + + public function viewListAcl($login, $rolename, $username) + { + $login->openRoles() + ->filterBy('Role', $rolename) + ->open(array($rolename)) + ->selectAcl('View business units list') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openBusinessUnits() + ->assertTitle('403 - Forbidden'); + } + + /** + * Data provider for Tags ACL test + * + * @return array + */ + public function columnTitle() + { + return array( + 'delete' => array('delete'), + 'update' => array('update'), + 'create' => array('create'), + 'view' => array('view'), + 'view list' => array('view list'), + ); + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/BusinessUnits/BusinessUnitsTest.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/BusinessUnits/BusinessUnitsTest.php new file mode 100644 index 00000000000..efdf95d6415 --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/BusinessUnits/BusinessUnitsTest.php @@ -0,0 +1,90 @@ +setHost(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_HOST); + $this->setPort(intval(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT)); + $this->setBrowser(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM2_BROWSER); + $this->setBrowserUrl(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_TESTS_URL); + } + + protected function tearDown() + { + $this->cookie()->clear(); + } + + /** + * @return string + */ + public function testCreateBusinessUnit() + { + $unitname = 'Unit_'.mt_rand(); + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openBusinessUnits() + ->add() + ->assertTitle('Create Business Unit - Business Units - System') + ->setBusinessUnitName($unitname) + ->setOwner('Main') + ->save() + ->assertMessage('Business Unit successfully saved') + ->assertTitle('Business Units - System') + ->close(); + + return $unitname; + } + + /** + * @depends testCreateBusinessUnit + * @param $unitname + * @return string + */ + public function testUpdateBusinessUnit($unitname) + { + $newunitname = 'Update_' . $unitname; + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openBusinessUnits() + ->filterBy('Name', $unitname) + ->open(array($unitname)) + ->edit() + ->setBusinessUnitName($newunitname) + ->save() + ->assertTitle('Business Units - System') + ->assertMessage('Business Unit successfully saved'); + + return $newunitname; + } + + /** + * @depends testUpdateBusinessUnit + * @param $unitname + */ + public function testDeleteBusinessUnit($unitname) + { + $this->markTestSkipped('BAP-726'); + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openBusinessUnits() + ->filterBy('Name', $unitname) + ->open(array($unitname)) + ->delete() + ->assertTitle('Business Units - System') + ->assertMessage('Item was deleted'); + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/EmailTemplate/EmailTemplateTest.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/EmailTemplate/EmailTemplateTest.php new file mode 100644 index 00000000000..4366cff9d2b --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/EmailTemplate/EmailTemplateTest.php @@ -0,0 +1,119 @@ +setHost(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_HOST); + $this->setPort(intval(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT)); + $this->setBrowser(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM2_BROWSER); + $this->setBrowserUrl(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_TESTS_URL); + } + + protected function tearDown() + { + $this->cookie()->clear(); + } + + /** + * @return string + */ + public function testCreateEmailTemplate() + { + $templatename = 'EmailTemplate_'.mt_rand(); + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openEmailTemplates() + ->add() + ->assertTitle('Create Email Template - Email Templates - System') + ->setEntityName('User') + ->setType('Html') + ->setName($templatename) + ->setSubject('Subject') + ->setContent('Template content') + ->save() + ->assertMessage('Template sucessfully saved') + ->assertTitle('Email Templates - System') + ->close(); + + return $templatename; + } + + /** + * @depends testCreateEmailTemplate + * @param $templatename + * @return string + */ + public function testCloneEmailTemplate($templatename) + { + $newtemplatename = 'Clone_' . $templatename; + $fields = array(); + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openEmailTemplates() + ->cloneEntity('Template name', $templatename) + ->setName($newtemplatename) + ->save() + ->assertMessage('Template sucessfully saved') + ->assertTitle('Email Templates - System') + ->close() + ->open(array($newtemplatename)) + ->getFields($fields); + $this->assertEquals('User', $fields['entityname']); + $this->assertEquals('Html', $fields['type']); + $this->assertEquals('Subject', $fields['subject']); + $this->assertEquals('Template content', $fields['content']); + + return $newtemplatename; + } + + /** + * @depends testCreateEmailTemplate + * @param $templatename + * @return string + */ + public function testUpdateEmailTemplate($templatename) + { + $newtemplatename = 'Update_' . $templatename; + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openEmailTemplates() + ->open(array($templatename)) + ->setName($newtemplatename) + ->save() + ->assertMessage('Template sucessfully saved') + ->assertTitle('Email Templates - System') + ->close(); + + return $newtemplatename; + } + + /** + * @depends testUpdateEmailTemplate + * @param $templatename + */ + public function testDeleteEmailTemplate($templatename) + { + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openEmailTemplates() + ->delete('Template name', $templatename) + ->assertTitle('Email Templates - System') + ->assertMessage('Item was deleted'); + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Grids/GridTest.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Grids/GridTest.php index 7be4651072b..d2ef4c5fec1 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Grids/GridTest.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Grids/GridTest.php @@ -155,8 +155,9 @@ public function testSorting($columnName) } $columnOrder = $dateArray; } - $sortedColumnOrder = $columnOrder; - rsort($sortedColumnOrder); + $sortedColumnOrder = $columnOrder; + natcasesort($sortedColumnOrder); + $sortedColumnOrder = array_reverse($sortedColumnOrder); $this->assertTrue($columnOrder === $sortedColumnOrder, print_r(array('expected' => $sortedColumnOrder, 'actual' => $columnOrder), true)); @@ -171,8 +172,8 @@ public function testSorting($columnName) } $columnOrder = $dateArray; } - $sortedColumnOrder = $columnOrder; - sort($sortedColumnOrder); + $sortedColumnOrder = $columnOrder; + natcasesort($sortedColumnOrder); $this->assertTrue($columnOrder === $sortedColumnOrder, print_r(array('expected' => $sortedColumnOrder, 'actual' => $columnOrder), true)); } diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Groups/GroupsTest.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Groups/GroupsTest.php index e2fa797662c..ea8dc938d74 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Groups/GroupsTest.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Groups/GroupsTest.php @@ -57,6 +57,7 @@ public function testGroupsGridDefaultContent() $this->assertArrayHasKey($content, $this->defaultGroups['header']); } + $checks = 0; foreach ($records as $row) { $columns = $row->elements($this->using('xpath')->value("td[not(contains(@style, 'display: none;'))]")); $id = null; @@ -65,9 +66,13 @@ public function testGroupsGridDefaultContent() if (is_null($id)) { $id = $content; } - $this->assertArrayHasKey($content, $this->defaultGroups[$id]); + if (array_key_exists($id, $this->defaultGroups)) { + $this->assertArrayHasKey($content, $this->defaultGroups[$id]); + } } + $checks = $checks + 1; } + $this->assertGreaterThanOrEqual(count($this->defaultGroups)-1, $checks); } public function testGroupAdd() @@ -84,6 +89,7 @@ public function testGroupAdd() ->openGroups(false) ->add() ->setName($this->newGroup['NAME'] . $randomPrefix) + ->setOwner('Main') ->setRoles(array($this->newGroup['ROLE'])) ->save() ->assertMessage('Group successfully saved') diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Roles/RolesTest.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Roles/RolesTest.php index 7966856fc7f..1f5b317425c 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Roles/RolesTest.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Roles/RolesTest.php @@ -58,6 +58,7 @@ public function testRolesGridDefaultContent() $this->assertArrayHasKey($content, $this->defaultRoles['header']); } + $checks = 0; foreach ($records as $row) { $columns = $row->elements($this->using('xpath')->value("td[not(contains(@style, 'display: none;'))]")); $id = null; @@ -66,10 +67,13 @@ public function testRolesGridDefaultContent() if (is_null($id)) { $id = trim($content); } - $this->assertArrayHasKey($content, $this->defaultRoles[$id]); + if (array_key_exists($id, $this->defaultRoles)) { + $this->assertArrayHasKey($content, $this->defaultRoles[$id]); + } } + $checks = $checks + 1; } - + $this->assertGreaterThanOrEqual(count($this->defaultRoles)-1, $checks); } public function testRolesAdd() @@ -88,6 +92,7 @@ public function testRolesAdd() ->add() ->setName($this->newRole['ROLE_NAME'] . $randomPrefix) ->setLabel($this->newRole['LABEL']) + ->setOwner('Main') ->save() ->assertMessage('Role successfully saved') ->close(); diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php new file mode 100644 index 00000000000..f7fd5c75cdb --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsAcl.php @@ -0,0 +1,269 @@ +setHost(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_HOST); + $this->setPort(intval(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT)); + $this->setBrowser(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM2_BROWSER); + $this->setBrowserUrl(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_TESTS_URL); + } + + protected function tearDown() + { + $this->cookie()->clear(); + } + + public function testCreateRole() + { + $randomPrefix = mt_rand(); + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openRoles() + ->add() + ->setName('ROLE_NAME_' . $randomPrefix) + ->setLabel('Label_' . $randomPrefix) + ->setOwner('Main') + ->selectAcl('Root') + ->save() + ->assertMessage('Role successfully saved') + ->close(); + + return ($randomPrefix); + } + + /** + * @depends testCreateRole + * @param $role + * @return string + */ + public function testCreateUser($role) + { + $username = 'User_'.mt_rand(); + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openUsers() + ->add() + ->assertTitle('Create User - Users - System') + ->setUsername($username) + ->setOwner('Default') + ->enable() + ->setFirstpassword('123123q') + ->setSecondpassword('123123q') + ->setFirstname('First_'.$username) + ->setLastname('Last_'.$username) + ->setEmail($username.'@mail.com') + ->setRoles(array('Label_' . $role)) + ->save() + ->assertMessage('User successfully saved') + ->close() + ->assertTitle('Users - System'); + + return $username; + } + + /** + * @depends testCreateUser + * @return string + */ + public function testCreateTag() + { + $tagname = 'Tag_'.mt_rand(); + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openTags() + ->add() + ->assertTitle('Create Tag - Tags - System') + ->setTagname($tagname) + ->setOwner('admin') + ->save() + ->assertMessage('Tag successfully saved') + ->assertTitle('Tags - System') + ->close(); + + return $tagname; + } + + /** + * @depends testCreateUser + * @depends testCreateRole + * @depends testCreateTag + * @param $username + * @param $role + * @param $tagname + * @param string $aclcase + * @dataProvider columnTitle + */ + public function testTagAcl($aclcase, $username, $role, $tagname) + { + $rolename = 'ROLE_NAME_' . $role; + $rolelabel = 'Label_' . $role; + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit(); + switch ($aclcase) { + case 'delete': + $this->deleteAcl($login, $rolename, $username, $tagname); + break; + case 'update': + $this->updateAcl($login, $rolename, $username, $tagname); + break; + case 'create': + $this->createAcl($login, $rolename, $username); + break; + case 'view list': + $this->viewListAcl($login, $rolename, $username); + break; + case 'unassign global': + $this->unassignGlobalAcl($login, $rolename, $rolelabel, $tagname); + break; + case 'assign/unassign': + $this->assignAcl($login, $rolename, $username); + break; + } + } + + public function deleteAcl($login, $role, $username, $tagname) + { + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Delete tags') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->checkContextMenu($tagname, 'Delete'); + } + + public function updateAcl($login, $role, $username, $tagname) + { + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Update tag') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->checkContextMenu($tagname, 'Update'); + } + + public function createAcl($login, $role, $username) + { + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Create tag') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->assertElementNotPresent("//div[@class = 'container-fluid']//a[contains(., 'Create tag')]"); + } + + public function viewListAcl($login, $role, $username) + { + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('View list of tags') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTags() + ->assertTitle('403 - Forbidden'); + } + + public function unassignGlobalAcl($login, $rolename, $rolelabel, $tagname) + { + $username = 'user' . mt_rand(); + $login->openRoles() + ->filterBy('Role', $rolename) + ->open(array($rolename)) + ->selectAcl('Tag unassign global') + ->save() + ->openUsers() + ->add() + ->setUsername($username) + ->enable() + ->setOwner('Main') + ->setFirstpassword('123123q') + ->setSecondpassword('123123q') + ->setFirstname('First_'.$username) + ->setLastname('Last_'.$username) + ->setEmail($username.'@mail.com') + ->setRoles(array($rolelabel)) + ->setTag($tagname) + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openUsers() + ->filterBy('Username', $username) + ->open(array($username)) + ->edit() + ->assertElementNotPresent( + "//div[@id='s2id_oro_user_user_form_tags']//li[contains(., '{$tagname}')]/a[@class='select2-search-choice-close']" + ); + } + + public function assignAcl($login, $role, $username) + { + $login->openRoles() + ->filterBy('Role', $role) + ->open(array($role)) + ->selectAcl('Tag assign/unassign') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openAccounts() + ->add(false) + ->assertElementPresent("//div[@class='select2-container select2-container-multi select2-container-disabled']"); + } + + /** + * Data provider for Tags ACL test + * + * @return array + */ + public function columnTitle() + { + return array( + 'delete' => array('delete'), + 'update' => array('update'), + 'create' => array('create'), + 'view list' => array('view list'), + 'unassign global' => array('unassign global'), + 'assign/unassign' => array('assign/unassign'), + ); + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsTest.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsTest.php index 77893c0cdc8..1b32bf5e267 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsTest.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/Tags/TagsTest.php @@ -36,6 +36,7 @@ public function testCreateTag() ->add() ->assertTitle('Create Tag - Tags - System') ->setTagname($tagname) + ->setOwner('admin') ->save() ->assertMessage('Tag successfully saved') ->assertTitle('Tags - System') diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/TransactionEmails/TransactionEmailsAcl.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/TransactionEmails/TransactionEmailsAcl.php new file mode 100644 index 00000000000..56f5abf852c --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/TransactionEmails/TransactionEmailsAcl.php @@ -0,0 +1,213 @@ +setHost(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_HOST); + $this->setPort(intval(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT)); + $this->setBrowser(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM2_BROWSER); + $this->setBrowserUrl(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_TESTS_URL); + } + + protected function tearDown() + { + $this->cookie()->clear(); + } + + public function testCreateRole() + { + $randomPrefix = mt_rand(); + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openRoles() + ->add() + ->setName('ROLE_NAME_' . $randomPrefix) + ->setLabel('Label_' . $randomPrefix) + ->setOwner('Main') + ->selectAcl('Root') + ->save() + ->assertMessage('Role successfully saved') + ->close(); + + return ($randomPrefix); + } + + /** + * @depends testCreateRole + * @param $role + * @return string + */ + public function testCreateUser($role) + { + $username = 'User_'.mt_rand(); + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openUsers() + ->add() + ->assertTitle('Create User - Users - System') + ->setUsername($username) + ->enable() + ->setOwner('Main') + ->setFirstpassword('123123q') + ->setSecondpassword('123123q') + ->setFirstname('First_'.$username) + ->setLastname('Last_'.$username) + ->setEmail($username.'@mail.com') + ->setRoles(array('Label_' . $role)) + ->save() + ->assertMessage('User successfully saved') + ->close() + ->assertTitle('Users - System'); + + return $username; + } + + /** + * @depends testCreateUser + * @return string + */ + public function testCreateTransactionEmail() + { + $email = 'Email'.mt_rand() . '@mail.com'; + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openTransactionEmails() + ->add() + ->assertTitle('Add Notification Rule - Transaction Emails - System') + ->setEmail($email) + ->setEntityName('User') + ->setEvent('Entity create') + ->setTemplate('user') + ->setUser('admin') + ->setGroups(array('Marketing')) + ->save() + ->assertMessage('Email notification rule has been saved') + ->assertTitle('Transaction Emails - System') + ->close(); + + return $email; + } + + /** + * @depends testCreateUser + * @depends testCreateRole + * @depends testCreateTransactionEmail + * @param $username + * @param $role + * @param $email + * @param string $aclcase + * @dataProvider columnTitle + */ + public function testBusinessUnitAcl($aclcase, $username, $role, $email) + { + $rolename = 'ROLE_NAME_' . $role; + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit(); + switch ($aclcase) { + case 'delete': + $this->deleteAcl($login, $rolename, $username, $email); + break; + case 'update': + $this->updateAcl($login, $rolename, $username, $email); + break; + case 'create': + $this->createAcl($login, $rolename, $username); + break; + case 'view list': + $this->viewListAcl($login, $rolename, $username); + break; + } + } + + public function deleteAcl($login, $rolename, $username, $email) + { + $login->openRoles() + ->filterBy('Role', $rolename) + ->open(array($rolename)) + ->selectAcl('Delete notification rule') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTransactionEmails() + ->checkContextMenu($email, 'Delete'); + } + + public function updateAcl($login, $rolename, $username, $email) + { + $login->openRoles() + ->filterBy('Role', $rolename) + ->open(array($rolename)) + ->selectAcl('Edit notification rule') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTransactionEmails() + ->checkContextMenu($email, 'Update'); + } + + public function createAcl($login, $rolename, $username) + { + $login->openRoles() + ->filterBy('Role', $rolename) + ->open(array($rolename)) + ->selectAcl('Create notification rule') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTransactionEmails() + ->assertElementNotPresent("//div[@class = 'container-fluid']//a[contains(., 'Create notification rule')]"); + } + + public function viewListAcl($login, $rolename, $username) + { + $login->openRoles() + ->filterBy('Role', $rolename) + ->open(array($rolename)) + ->selectAcl('View List of notification rules') + ->save() + ->logout() + ->setUsername($username) + ->setPassword('123123q') + ->submit() + ->openTransactionEmails() + ->assertTitle('403 - Forbidden'); + } + + /** + * Data provider for Tags ACL test + * + * @return array + */ + public function columnTitle() + { + return array( + 'delete' => array('delete'), + 'update' => array('update'), + 'create' => array('create'), + 'view list' => array('view list'), + ); + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/TransactionEmails/TransactionEmailsTest.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/TransactionEmails/TransactionEmailsTest.php new file mode 100644 index 00000000000..d60f4688942 --- /dev/null +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/TransactionEmails/TransactionEmailsTest.php @@ -0,0 +1,90 @@ +setHost(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_HOST); + $this->setPort(intval(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PORT)); + $this->setBrowser(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM2_BROWSER); + $this->setBrowserUrl(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_TESTS_URL); + } + + protected function tearDown() + { + $this->cookie()->clear(); + } + + /** + * @return string + */ + public function testCreateTransactionEmail() + { + $email = 'Email'.mt_rand() . '@mail.com'; + + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openTransactionEmails() + ->add() + ->assertTitle('Add Notification Rule - Transaction Emails - System') + ->setEntityName('User') + ->setEvent('Entity create') + ->setTemplate('user') + ->setUser('admin') + ->setGroups(array('Marketing')) + ->setEmail($email) + ->save() + ->assertMessage('Email notification rule has been saved') + ->assertTitle('Transaction Emails - System') + ->close(); + + return $email; + } + + /** + * @depends testCreateTransactionEmail + * @param $email + * @return string + */ + public function testUpdateTransactionEmail($email) + { + $newemail = 'Update_' . $email; + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openTransactionEmails() + ->open(array($email)) + ->setEmail($newemail) + ->save() + ->assertMessage('Email notification rule has been saved') + ->assertTitle('Transaction Emails - System') + ->close(); + + return $newemail; + } + + /** + * @depends testUpdateTransactionEmail + * @param $email + */ + public function testDeleteTransactionEmail($email) + { + $login = new Login($this); + $login->setUsername(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_LOGIN) + ->setPassword(PHPUNIT_TESTSUITE_EXTENSION_SELENIUM_PASS) + ->submit() + ->openTransactionEmails() + ->delete('Recipient email', $email) + ->assertTitle('Transaction Emails - System') + ->assertMessage('Item was deleted'); + } +} diff --git a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/User/UsersTest.php b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/User/UsersTest.php index 6f629c34fdb..efd3808e407 100644 --- a/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/User/UsersTest.php +++ b/src/Oro/Bundle/TestFrameworkBundle/Tests/Selenium/User/UsersTest.php @@ -40,6 +40,7 @@ public function testCreateUser() ->assertTitle('Create User - Users - System') ->setUsername($username) ->enable() + ->setOwner('Main') ->setFirstpassword('123123q') ->setSecondpassword('123123q') ->setFirstname('First_'.$username) diff --git a/src/Oro/Bundle/TranslationBundle/Extractor/PhpCodeExtractor.php b/src/Oro/Bundle/TranslationBundle/Extractor/PhpCodeExtractor.php new file mode 100644 index 00000000000..37f880b97fc --- /dev/null +++ b/src/Oro/Bundle/TranslationBundle/Extractor/PhpCodeExtractor.php @@ -0,0 +1,118 @@ +container = $container; + } + + /** + * {@inheritDoc} + */ + public function extract($directory, MessageCatalogue $catalog) + { + $finder = new Finder(); + $files = $finder->files()->name('*.php')->in($directory. '/../../')->exclude(array('Tests', 'Resources')); + foreach ($files as $file) { + $this->parseTokens(token_get_all(file_get_contents($file)), $catalog); + } + } + + /** + * Extracts trans message from php tokens. + * + * @param array $tokens + * @param MessageCatalogue $catalog + */ + protected function parseTokens($tokens, MessageCatalogue $catalog) + { + $vendorName = $this->getVendorName($tokens); + + // trying to find messages for translation only in case if vendor name found + if ($vendorName !== false) { + foreach ($tokens as $token) { + if (is_array($token) && $token[0] == T_CONSTANT_ENCAPSED_STRING) { + $message = $token[1]; + $message = trim($message, '\'"'); + + if ($message) { + $messageToCheck = explode('.', $message); + + if (count($messageToCheck) > 2 + && strcmp($messageToCheck[0], $vendorName) === 0 + && !$this->container->has($message) + && !$this->container->hasParameter($message)) { + + $catalog->set($message, $this->prefix . $message); + } + } + } + } + } + } + + /** + * @param $tokens + * @return bool|string + */ + protected function getVendorName($tokens) + { + $vendorName = false; + + $sequence = array( + 'namespace', + ' ', + self::MESSAGE_TOKEN, + ); + + foreach ($tokens as $k => $token) { + foreach ($sequence as $id => $item) { + if ($this->normalizeToken($tokens[$k + $id]) == $item) { + continue; + } elseif (self::MESSAGE_TOKEN == $item) { + $vendorName = strtolower($this->normalizeToken($tokens[$k + $id])); + } else { + break; + } + } + } + + return $vendorName; + } + + /** + * @param $token + * @return mixed + */ + protected function normalizeToken($token) + { + return is_array($token) ? $token[1] : $token; + } + + /** + * {@inheritDoc} + */ + public function setPrefix($prefix) + { + $this->prefix = $prefix; + } +} diff --git a/src/Oro/Bundle/TranslationBundle/Resources/config/services.yml b/src/Oro/Bundle/TranslationBundle/Resources/config/services.yml index 4572212dbf5..c339b9fc033 100644 --- a/src/Oro/Bundle/TranslationBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/TranslationBundle/Resources/config/services.yml @@ -1,8 +1,15 @@ parameters: translator.class: Oro\Bundle\TranslationBundle\Translation\Translator oro_translation.controller.class: Oro\Bundle\TranslationBundle\Controller\Controller + oro_translation.extractor.php_code_extractor.class: Oro\Bundle\TranslationBundle\Extractor\PhpCodeExtractor services: oro_translation.controller: class: %oro_translation.controller.class% arguments: [@translator, @templating, OroTranslationBundle:Translation:translation.js.twig, ""] + + oro_translation.extractor.php_code_extractor: + class: %oro_translation.extractor.php_code_extractor.class% + arguments: [@service_container] + tags: + - { name: translation.extractor, alias: oro_translation_php_extractor } diff --git a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Extractor/PhpCodeExtractorTest.php b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Extractor/PhpCodeExtractorTest.php new file mode 100644 index 00000000000..4725a0b52bb --- /dev/null +++ b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Extractor/PhpCodeExtractorTest.php @@ -0,0 +1,74 @@ +container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface') + ->disableOriginalConstructor()->getMock(); + + $this->extractor = new PhpCodeExtractor($this->container); + } + + public function tearDown() + { + unset($this->container); + unset($this->extractor); + } + + public function testExtraction() + { + // Arrange + $this->extractor->setPrefix('prefix'); + $catalogue = new MessageCatalogue('en'); + $this->container->expects($this->atLeastOnce())->method('has') + ->will( + $this->returnCallback( + function ($id) { + if ($id == SomeClass::STRING_NOT_TO_TRANSLATE) { + return true; + } + + return false; + } + ) + ); + + // Act + $this->extractor->extract(__DIR__ . '/../Fixtures/Resources/views', $catalogue); + + // Assert + $this->assertCount(2, $catalogue->all('messages'), '->extract() should find 3 translations'); + $this->assertTrue($catalogue->has(SomeClass::STRING_TO_TRANSLATE), '->extract() should extract constants'); + $this->assertTrue($catalogue->has(self::MESSAGE_FROM_VARIABLE), '->extract() should extract variables'); + $this->assertFalse( + $catalogue->has(self::MESSAGE_FROM_ARGUMENT), + '->extract() should not extract messages from another vendor namespace' + ); + $this->assertFalse( + $catalogue->has(SomeClass::STRING_NOT_TO_TRANSLATE), + '->extract() should not extract existed services' + ); + $this->assertEquals( + 'prefix' . SomeClass::STRING_TO_TRANSLATE, + $catalogue->get(SomeClass::STRING_TO_TRANSLATE), + '->extract() should apply "prefix" as prefix' + ); + } +} diff --git a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Fixtures/Resources/views/.gitkeep b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Fixtures/Resources/views/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/Oro/Bundle/TranslationBundle/Tests/Unit/Fixtures/SomeClass.php b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Fixtures/SomeClass.php new file mode 100644 index 00000000000..b55d6d6b192 --- /dev/null +++ b/src/Oro/Bundle/TranslationBundle/Tests/Unit/Fixtures/SomeClass.php @@ -0,0 +1,22 @@ +someAnotherFunc('/../Resources/config', 'vendor.bundle.type.message_string'); + return $someVariable; + } + + protected function someAnotherFunc($arg1, $arg2) + { + return array($arg1, $arg2); + } +} diff --git a/src/Oro/Bundle/UIBundle/Event/BeforeFormRenderEvent.php b/src/Oro/Bundle/UIBundle/Event/BeforeFormRenderEvent.php new file mode 100644 index 00000000000..ed90a7f2307 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Event/BeforeFormRenderEvent.php @@ -0,0 +1,68 @@ +form = $form; + $this->formData = $formData; + $this->twigEnvironment = $twigEnvironment; + } + + /** + * @return FormView + */ + public function getForm() + { + return $this->form; + } + + /** + * @return array + */ + public function getFormData() + { + return $this->formData; + } + + public function setFormData(array $formData) + { + $this->formData = $formData; + } + + /** + * @return \Twig_Environment + */ + public function getTwigEnvironment() + { + return $this->twigEnvironment; + } +} diff --git a/src/Oro/Bundle/UIBundle/Event/Events.php b/src/Oro/Bundle/UIBundle/Event/Events.php new file mode 100644 index 00000000000..e53ef248ce6 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Event/Events.php @@ -0,0 +1,8 @@ + with {'form' : form} %} ``` + +## Templates Hinting + +UIBundle allows to enable templates hinting and in such a way helps to frontend developer to find proper template. +This option can be enabled in application configuration with redefining base template class for twig: + + ```yaml + twig: + base_template_class: Oro\Bundle\UIBundle\Twig\Template + ``` + +As e result of such change user can find HTML comments on the page +```html + +... + +``` +or see "template_name" variable for AJAX requests that expecting JSON +```json +"template_name":"BundleName:template_name.html.twig" +``` + +Templates hinting is enabled by default in development mode. \ No newline at end of file diff --git a/src/Oro/Bundle/UIBundle/Resources/config/assets.yml b/src/Oro/Bundle/UIBundle/Resources/config/assets.yml index 724fc254d0a..c8c50e78e6b 100644 --- a/src/Oro/Bundle/UIBundle/Resources/config/assets.yml +++ b/src/Oro/Bundle/UIBundle/Resources/config/assets.yml @@ -2,7 +2,7 @@ js: 'less_compiller': - '@OroUIBundle/Resources/public/lib/less-1.3.1.min.js' 'jquery': - - '@OroUIBundle/Resources/public/lib/jquery.min.js' + - '@OroUIBundle/Resources/public/lib/jquery-1.10.2.js' - '@OroUIBundle/Resources/public/lib/jquery-ui.min.js' - '@OroUIBundle/Resources/public/lib/base64.js' 'backbone': @@ -14,7 +14,7 @@ js: - '@OroUIBundle/Resources/public/lib/jquery/jquery.mousewheel.min.js' - '@OroUIBundle/Resources/public/lib/jquery/jquery.numeric.js' - '@OroUIBundle/Resources/public/lib/jquery/jquery.uniform.min.js' - - '@OroUIBundle/Resources/public/lib/jquery/select2.min.js' + - '@OroUIBundle/Resources/public/lib/jquery/select2.js' 'bootstrap': - '@OroUIBundle/Resources/public/lib/bootstrap.min.js' - '@OroUIBundle/Resources/public/lib/bootstrap/js/bootstrap-tooltip.js' @@ -44,6 +44,10 @@ js: - '@OroUIBundle/Resources/public/js/form_buttons.js' 'js_routes': - 'js/routes.js' + 'widgets': + - '@OroUIBundle/Resources/public/js/backbone/widget/manager.js' + - '@OroUIBundle/Resources/public/js/backbone/widget/abstract.js' + - '@OroUIBundle/Resources/public/js/backbone/widget/block.js' css: 'UI': - 'bundles/oroui/css/less/main.less' diff --git a/src/Oro/Bundle/UIBundle/Resources/config/services.yml b/src/Oro/Bundle/UIBundle/Resources/config/services.yml index 4e7d23dbe36..0c6307af76c 100644 --- a/src/Oro/Bundle/UIBundle/Resources/config/services.yml +++ b/src/Oro/Bundle/UIBundle/Resources/config/services.yml @@ -3,6 +3,11 @@ parameters: oro_ui.twig.extension.class: Oro\Bundle\UIBundle\Twig\UiExtension oro_ui.twig.md5.class: Oro\Bundle\UIBundle\Twig\Md5Extension oro_ui.router.class: Oro\Bundle\UIBundle\Route\Router + oro_ui.twig.widget.class: Oro\Bundle\UIBundle\Twig\WidgetExtension + oro_ui.twig.date.class: Oro\Bundle\UIBundle\Twig\DateExtension + oro_ui.twig.skype_button.class: Oro\Bundle\UIBundle\Twig\SkypeButtonExtension + oro_ui.view.listener.class: Oro\Bundle\UIBundle\EventListener\TemplateListener + oro_ui.twig.form.class: Oro\Bundle\UIBundle\Twig\FormExtension services: oro_ui.router: @@ -25,3 +30,34 @@ services: class: %oro_ui.twig.md5.class% tags: - { name: twig.extension } + + oro_ui.twig.form_extension: + class: %oro_ui.twig.form.class% + arguments: + - @event_dispatcher + tags: + - { name: twig.extension } + + oro_ui.twig.widget_extension: + class: %oro_ui.twig.widget.class% + tags: + - { name: twig.extension } + + oro_ui.twig.date_extension: + class: %oro_ui.twig.date.class% + arguments: + - @translator + tags: + - { name: twig.extension } + + oro_ui.twig.skype_button_extension: + class: %oro_ui.twig.skype_button.class% + tags: + - { name: twig.extension } + + oro_ui.view.listener: + class: %oro_ui.view.listener.class% + arguments: + - @service_container + tags: + - { name: kernel.event_listener, event: kernel.view, method: onKernelView } \ No newline at end of file diff --git a/src/Oro/Bundle/UIBundle/Resources/doc/reference/js_tools_and_libraries.md b/src/Oro/Bundle/UIBundle/Resources/doc/reference/js_tools_and_libraries.md index 8157002fef4..b3e71092586 100644 --- a/src/Oro/Bundle/UIBundle/Resources/doc/reference/js_tools_and_libraries.md +++ b/src/Oro/Bundle/UIBundle/Resources/doc/reference/js_tools_and_libraries.md @@ -21,8 +21,6 @@ Oro is the global namespace for all JS widgets. Also it contains several useful * **unpackFromQueryString** (String query) - reverse action to packToQueryString function, converts query string to object; * **invertKeys** (Object object, Object keys) - replaces key in object according to data from keys; * **isEqualsLoosely** (value1, value2) - compares any type of values for equality; -* **createInstanceFromConstructor** (Function constructor, *arguments) - creates instance from constructor -with source arguments; * **deepClone** (Object value) - clones source value with all references. diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/jquery-ui.grid.css b/src/Oro/Bundle/UIBundle/Resources/public/css/jquery-ui.grid.css index 7809007efb9..5bcd3165f7c 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/jquery-ui.grid.css +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/jquery-ui.grid.css @@ -50,6 +50,8 @@ width: 17em; padding: .2em .2em 0; display: none; + /** z-index is !important as datepicker element can not be clicked when behind it placed styled select with z-index 700**/ + z-index: 800 !important; } .ui-datepicker .ui-datepicker-header { position: relative; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less index 20f23f7394b..f5adbfe6632 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less +++ b/src/Oro/Bundle/UIBundle/Resources/public/css/less/oro.less @@ -940,6 +940,9 @@ footer{ cursor: move; } } +.ui-dialog #adopted button.btn{ + margin-left: 5px; +} .ui-dialog .ui-resizable-se{ height: 14px; width: 14px; @@ -1061,6 +1064,9 @@ footer{ .btn{ #gradient > .vertical(#ffffff, #E6E6E6); } +.btn-uppercase{ + text-transform: uppercase; +} .btn-primary{ #gradient > .vertical(#739cdc, #4875bc); border: 1px solid; @@ -1614,6 +1620,147 @@ footer{ .icon-star.icon-gold{ background-position: 0 -188px; } +.icon-twitter-new{ + background: url("../../img/general-sprite.png") no-repeat 0 -473px; + width: 13px; + height: 12px; + margin-top: 1px; +} +.icon-twitter-new:hover{ + background-position: -13px -473px; +} +.icon-facebook-new{ + background: url("../../img/general-sprite.png") no-repeat 0 -485px; + width: 9px; + height: 14px; +} +.icon-facebook-new:hover{ + background-position: -9px -485px; +} +.icon-google-new{ + background: url("../../img/general-sprite.png") no-repeat 0 -499px; + width: 13px; + height: 14px; + margin-top: 1px; +} +.icon-google-new:hover{ + background-position: -14px -499px; +} +.icon-linkedin-new{ + background: url("../../img/general-sprite.png") no-repeat 0 -513px; + width: 14px; + height: 14px; +} +.icon-linkedin-new:hover{ + background-position: -14px -513px; +} +.icon-mail-new, +.icon-phone-new{ + background: url("../../img/sprite-horizontal.png") no-repeat; + width:33px; + height:32px; +} +.icon-mail-new{ + background-position: -33px 0; +} + +.new-line{ + clear: both; + display: block; + width: 100%; +} + +.list-inline{ + list-style: none; + padding-left: 0; +} +.list-inline > li { + display: inline-block; + padding-left: 5px; + padding-right: 5px; +} +.list-group{ + +} +.list-group > .list-group-item{ + padding: 10px; + border-top: 1px solid #efefef; + width: 100%; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; +} +.list-group > .list-group-item:after{ + content: ''; + clear: both; + display: block; +} +.list-group > .list-group-item:first-child{ + border: 0; +} +.nav-tabs-content{ + background-color: #f8f8f8; + padding: 4px 15px 0; + margin-bottom: 8px; +} +.tab-content-small{ + table.table:last-child{ + margin: 0; + } + .table td{ + padding: 5px 20px; + } + table.table thead th:last-child, + table.table thead th:first-child, + table.table thead th{ + padding: 3px 20px; + text-transform: uppercase; + font:bold 11px/12px Arial, Helvetica, sans-serif; + border: solid #dfdfdf; + border-width: 1px 0; + } +} +.nav-tabs-content > li{ + line-height: 16px; + height: 22px; + margin-right: -3px; +} +.nav-tabs-content > li > a{ + background: none; + border-right: 1px solid #e4e4e4; + padding: 1px 10px 0; + color: #666; +} +.nav-tabs-content > li:last-child > a{ + border-right: 0; +} + +.nav-tabs-content > li.active:focus > a:hover, +.nav-tabs-content > li.active:focus > a:focus, +.nav-tabs-content > li.active:hover > a:focus, +.nav-tabs-content > li.active:hover > a, +.nav-tabs-content > li:hover > a, +.nav-tabs-content > li.active > a:focus, +.nav-tabs-content > li.active > a:hover, +.nav-tabs-content > li.active > a{ + background-color: #fff; + border: solid #ddd; + border-width: 1px 1px 0; + padding-bottom: 1px; + color: #444; + font-weight: bold; +} + +.nav-tabs-content > li:hover > a{ + font-weight: normal; +} +.nav-tabs-content > li:hover > a:focus, +.nav-tabs-content > li > a:focus{ + font-weight: normal; + background: none; + border: 0; + border-right: 1px solid #e4e4e4; +} .badge-enabled, .badge-disabled{ padding: 3px 10px 3px 5px; @@ -1644,8 +1791,12 @@ footer{ line-height: 32px; } .grid-toolbar{ + /* FE */ + position: relative; + margin: 0 0 -15px; .actions-panel{ margin-left: 5px; + margin-bottom: 14px; } .pagination{ margin: 0 0 20px; @@ -1657,6 +1808,7 @@ footer{ .grid-container{ width: 100%; position: relative; + margin-top: 10px; overflow-x: auto; overflow-y: visible; } @@ -1667,6 +1819,9 @@ footer{ vertical-align: bottom; width: 60px; } + .select-row-cell { + width: 60px; + } thead th{ background-color: #eee; padding: 5px 10px; @@ -1954,6 +2109,9 @@ footer{ margin: 0; } +.modal{ + .border-radius(8px 8px 6px 6px); +} .oro-modal-danger{ top: 20%; background-color: #fde0e0; @@ -1987,6 +2145,52 @@ footer{ font:bold 16px/18px 'HelveticaNeue Medium', Arial, Helvetica, sans-serif; } } +.oro-modal-normal{ + background-color: #fff; + width: 700px; + height: 450px; + + top: 20%; + .modal-header{ + background-color: #303944; + border-bottom: 1px solid #303944; + padding: 14px 15px 14px 20px; + .border-radius(6px 6px 0 0); + -webkit-box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.5); + box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.5); + color: #fff; + } + .modal-body{ + color: #444; + font-size: 14px; + padding-left: 20px; + height: 83%; + } + .modal-header .close{ + color: @white; + font-size: 25px; + text-shadow: 2px 0px 2px rgba(111, 123, 148, 1); + .opacity(90); + &:hover, + &:focus { + color: @white; + .opacity(60); + + } + } + .modal-footer{ + background: none; + text-align: center; + border: none; + -webkit-box-shadow: none; + box-shadow: none; + padding: 15px 15px 35px; + } + .modal-header h3{ + font:bold 16px/18px 'HelveticaNeue Medium', Arial, Helvetica, sans-serif; + } + +} .modal-primary { top: 20%; background-color: #f9f9f9; @@ -2154,7 +2358,7 @@ footer{ .ui-dialog.ui-widget-content{ border: 1px solid #8e9195; } -.usser-info-state{ +.user-info-state{ padding: 10px 0 0; clear: right; } @@ -2311,6 +2515,9 @@ ul.inline-extra > li:first-child{ .customer-info-actions{ background-color: #f6f6f6; border-bottom: 1px solid #ebebeb; + .alert{ + margin-bottom: 0; + } } .customer-info-top-actions .btn .caret{ margin-top: 10px; @@ -2391,7 +2598,7 @@ label.required em, } .form-horizontal.span6 .controls div.selector, .form-horizontal .span6 .controls div.selector{ - margin:0 24px 0 0; + /*margin:0 24px 0 0;*/ } .form-horizontal.span6 .controls .removeRow, .form-horizontal .span6 .controls .removeRow { @@ -2450,6 +2657,9 @@ textarea{ padding: 30px 33px; } +.oro-drop.open-filter .filter-criteria.dropdown-menu{ + min-width: 120px; +} .filter-criteria .oro-action{ width: 100%; margin: 0; @@ -3849,20 +4059,43 @@ disabled look for disabled choices in the results dropdown background-position: 100% -21px !important; } } -.oro-address-collection > div{ + +#orocrm_contact_form_addresses_collection.oro-item-collection > div{ position: relative; padding: 10px 0 0; margin: 0 0 10px; border: 1px dashed #ededed; } -.oro-address-collection .add-list-item{ +#orocrm_contact_form_addresses_collection.oro-item-collection .add-list-item{ + margin-left: 183px; +} +#orocrm_contact_form_addresses_collection.oro-item-collection .removeRow{ + position: absolute; + top: 0; + right: 10px; +} + +.oro-item-collection > div{ + position: relative; + padding: 0; + margin: 0; + border: none; +} + +.oro-item-collection .add-list-item{ margin-left: 183px; } -.oro-address-collection .removeRow{ + +.oro-item-collection .removeRow{ position: absolute; top: 0; right: 10px; } + +.oro-item-collection .row-oro.oro-multiselect-holder{ + margin: 0 0 5px +} + #users-grid #loading-wrapper{ z-index: 888; } @@ -4139,6 +4372,144 @@ button.btn.minimize-button i.icon-minimize-active{ float:left; } -#pin-button-div{ - float:right; +.small-well{ + margin: 0 0 25px; + padding: 13px; +} +.box-type1{ + border: solid #eaeaea; + border-width: 1px 2px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + background-color: #fff; + .title{ + font:bold 14px/18px 'helveticaneue bold', Arial, Helvetica, sans-serif; + padding: 13px 20px; + border-bottom: 1px solid #eaeaea; + } + .box-content{ + margin: 20px 15px; + background: url("../../img/vertical-separator.png") repeat-y 50% 0; + } + .list-inline{ + margin-left: 0; + } +} +.well-small-bottom{ + margin-bottom: 10px; +} +.left-holder{ + padding: 0 0 0 15px; + float: left; + line-height: 16px; + .date{ + display: block; + clear: both; + } +} +.dark-bg{ + background-color: #f1f1f1; +} +.map-box{ + background: url("../../img/vertical-separator.png") repeat-y 40% 0; + &:after{ + content: ""; + clear: both; + display: block; + } + .map-address-list{ + width: 40%; + height: 420px; + background-color: #fbfbfb; + float: left; + overflow: hidden; + } + .map-address-list:hover{ + overflow: auto; + } + .map-visual-frame { + float: left; + width: 60%; + height: 420px; + } + .map-visual{ + border: 1px solid #d3d3d3; + height: 410px; + margin: 4px 5px; + img{ + max-width:none; + } + } + .map-unknown{ + line-height: 420px; + text-align: center; + } + .map-item{ + padding: 15px; + box-sizing: border-box; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + width: 100%; + } + .map-item.active{ + background-color: #ecf1f9; + } + .title-item{ + font: bold 14px/18px Arial, Helvetica, sans-serif; + float: left; + } + .map-actions{ + float: right; + .btn{ + border: 0; + background: 0; + -webkit-box-shadow: none; + box-shadow: none; + padding: 0 6px; + } + } +} +.email-holder{ + float: left; + .removeRow{ + margin: 0; + } +} +#orocrm_contact_form .removeRow{ + margin: 0 0 0 15px; +} + +pre.email-body { + width: 99%; + min-height: 5em; +} +iframe.email-body { + width: 99%; + min-height: 5em; + height: 25em; + #font > #family > .monospace; + color: @grayDark; + padding: (@baseLineHeight - 1) / 2; + margin: 0 0 @baseLineHeight / 2; + font-size: @baseFontSize - 1; // 14px to 13px + line-height: @baseLineHeight; + background-color: #f5f5f5; + border: 1px solid #ccc; // fallback for IE7-8 + border: 1px solid rgba(0,0,0,.15); + .border-radius(@baseBorderRadius); +} + +.skype-button { + padding: 0 5px; +} + +.skype-button:focus, .skype-button:hover { + text-decoration: none; +} + +.icon-skype { + background-image: url('../../img/skype-ico.png'); + background-position: 0; + margin-right: 0; } diff --git a/src/Oro/Bundle/UIBundle/Resources/public/img/info-envelope.png b/src/Oro/Bundle/UIBundle/Resources/public/img/info-envelope.png new file mode 100644 index 00000000000..f19263049b0 Binary files /dev/null and b/src/Oro/Bundle/UIBundle/Resources/public/img/info-envelope.png differ diff --git a/src/Oro/Bundle/UIBundle/Resources/public/img/map.png b/src/Oro/Bundle/UIBundle/Resources/public/img/map.png new file mode 100644 index 00000000000..97d0424aa2e Binary files /dev/null and b/src/Oro/Bundle/UIBundle/Resources/public/img/map.png differ diff --git a/src/Oro/Bundle/UIBundle/Resources/public/img/skype-ico.png b/src/Oro/Bundle/UIBundle/Resources/public/img/skype-ico.png new file mode 100644 index 00000000000..a2dab219c5b Binary files /dev/null and b/src/Oro/Bundle/UIBundle/Resources/public/img/skype-ico.png differ diff --git a/src/Oro/Bundle/UIBundle/Resources/public/img/sprite-horizontal.png b/src/Oro/Bundle/UIBundle/Resources/public/img/sprite-horizontal.png new file mode 100644 index 00000000000..b3b291cafd7 Binary files /dev/null and b/src/Oro/Bundle/UIBundle/Resources/public/img/sprite-horizontal.png differ diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/app.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/app.js index 2b407d3068d..1d5517101c2 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/app.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/app.js @@ -135,20 +135,6 @@ var Oro = { } }, - /** - * Creates instance based on constructor - * - * @param {Object} constructor - * @return {Object} - */ - createInstanceFromConstructor: function(constructor) { - var instance = new constructor(); - var instanceArguments = Array.prototype.splice.call(arguments, 1); - constructor.apply(instance, instanceArguments); - - return instance; - }, - /** * Deep clone a value * diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/bootstrap-modal.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/bootstrap-modal.js index 255a820da30..59bec1552d4 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/bootstrap-modal.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/bootstrap-modal.js @@ -29,10 +29,8 @@ var Oro = Oro || {}; <% } %>\ \ \ @@ -42,24 +40,27 @@ var Oro = Oro || {}; * @param {Object} options */ initialize: function(options) { - if (!options.cancelText) { - options.cancelText = ''; - } + options = _.extend({ + cancelText: _.__('Cancel') + }, options); + if (!options.okButtonClass) { options.okButtonClass = this.okButtonClass; } options = _.extend({ - template: this.template + template: this.template, + className: this.className }, options); + Backbone.BootstrapModal.prototype.initialize.apply(this, arguments); + }, + + open: function() { + Backbone.BootstrapModal.prototype.open.apply(this, arguments); + + this.once('cancel', _.bind(function() { + this.$el.trigger('hidden'); + }, this)); } }); })(jQuery, _, Backbone); -/* -* add to modal-footer if you need button "Cancel" - <% if (allowCancel) { %>\ - <% if (cancelText) { %>\ - <%= cancelText %>\ - <% } %>\ - <% } %>\ - */ diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/error.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/error.js index cb74b2417a4..10857a14558 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/error.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/error.js @@ -52,7 +52,8 @@ $(function() { var modal = new Oro.BootstrapModal({ title: Oro.BackboneError.Header, - content: message + content: message, + cancelText: false }); modal.open(); }, diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js new file mode 100644 index 00000000000..9297106714f --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/abstract.js @@ -0,0 +1,294 @@ +var Oro = Oro || {}; +Oro.widget = Oro.widget || {}; + +Oro.widget.Abstract = Backbone.View.extend({ + options: { + type: 'widget', + actionsEl: '.widget-actions', + url: false, + elementFirst: true, + title: '', + wid: null + }, + + setTitle: function(title) { + console.warn('Implement setTitle'); + }, + + getActionsElement: function() { + console.warn('Implement getActionsElement'); + }, + + remove: function() { + Oro.widget.Manager.removeWidget(this.getWid()); + Backbone.View.prototype.remove.call(this); + }, + + /** + * Initialize + */ + initializeWidget: function(options) { + if (this.options.wid) { + this._wid = this.options.wid; + } + + this.on('adoptedFormSubmitClick', _.bind(this._onAdoptedFormSubmitClick, this)); + this.on('adoptedFormResetClick', _.bind(this._onAdoptedFormResetClick, this)); + this.on('adoptedFormSubmit', _.bind(this._onAdoptedFormSubmit, this)); + + this.actions = {}; + this.firstRun = true; + }, + + getWid: function() { + if (!this._wid) { + this._wid = this._getUniqueIdentifier(); + } + return this._wid; + }, + + _getUniqueIdentifier: function() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); + }, + + /** + * Move form actions to widget actions + */ + _adoptWidgetActions: function() { + var adoptedActionsContainer = this._getAdoptedActionsContainer(); + if (adoptedActionsContainer.length > 0) { + var self = this; + var form = adoptedActionsContainer.closest('form'); + var actions = adoptedActionsContainer.find('button, input, a'); + + if (form.length > 0) { + this.form = form; + var formAction = this.form.attr('action'); + if (formAction.length > 0 && formAction[0] != '#') { + this.options.url = formAction; + } + this.form.submit(function(e) { + e.stopImmediatePropagation(); + self.trigger('adoptedFormSubmit', self.form, self); + return false; + }); + } + + self.actions['adopted'] = {}; + _.each(actions, function(action, idx) { + var $action = $(action); + var actionId = $action.data('action-name') ? $action.data('action-name') : 'adopted_action_' + idx; + switch (action.type.toLowerCase()) { + case 'submit': + actionId = 'form_submit'; + break; + case 'reset': + actionId = 'form_reset'; + break; + } + self.actions['adopted'][actionId] = $action; + }); + adoptedActionsContainer.remove(); + } + }, + + _getAdoptedActionsContainer: function() { + if (this.options.actionsEl !== undefined) { + if (typeof this.options.actionsEl == 'string') { + return this.$el.find(this.options.actionsEl); + } else if (_.isElement(this.options.actionsEl )) { + return this.options.actionsEl; + } + } + return false; + }, + + _onAdoptedFormSubmitClick: function(form) + { + form.submit(); + }, + + _onAdoptedFormSubmit: function(form) + { + this.loadContent(form.serialize(), form.attr('method')); + }, + + _onAdoptedFormResetClick: function(form) + { + $(form).trigger('reset'); + }, + + addAction: function(key, actionElement, section) { + if (section === undefined) { + section = 'main'; + } + if (!this.hasAction(key, section)) { + this.actions[key] = actionElement; + this.getActionsElement().append(actionElement); + } + }, + + getActions: function() { + return this.actions; + }, + + removeAction: function(key, section) { + var self = this; + var remove = function(actions, key) { + if (_.isElement(self.actions[key])) { + self.actions[key].remove(); + } + delete self.actions[key]; + }; + if (this.hasAction(key, section)) { + if (section !== undefined) { + remove(this.actions[section], key); + } else { + _.each(this.actions, function(actions, section) { + if (self.hasAction(key, section)) { + remove(actions, key); + } + }); + } + } + }, + + hasAction: function(key, section) { + if (section !== undefined) { + return this.actions[section].hasOwnProperty(key); + } else { + var hasAction = false; + _.each(this.actions, function(actions) { + if (actions.hasOwnProperty(key)) { + hasAction = true; + } + }); + return hasAction; + } + }, + + getAction: function(key, section) { + var action = null; + if (this.hasAction(key, section)) { + if (section !== undefined) { + action = this.actions[section][key]; + } else { + _.each(this.actions, function(actions) { + if (actions.hasOwnProperty(key)) { + action = actions[key]; + } + }); + } + } + return action; + }, + + _renderActions: function() { + var self = this; + this._clearActionsContainer(); + var container = this.getActionsElement(); + + _.each(this.actions, function(actions, section) { + var sectionContainer = $('
    '); + _.each(actions, function(action) { + self._initActionEvents(action); + sectionContainer.append(action); + }); + container.append(sectionContainer); + }); + }, + + _initActionEvents: function(action) { + var self = this; + switch (action.attr('type').toLowerCase()) { + case 'submit': + action.on('click', function() { + self.trigger('adoptedFormSubmitClick', self.form, self); + return false; + }); + break; + + case 'reset': + action.on('click', function() { + self.trigger('adoptedFormResetClick', self.form, self); + }); + break; + } + }, + + _clearActionsContainer: function() { + this.getActionsElement().empty(); + }, + + /** + * Render widget + */ + render: function() { + var loadAllowed = this.$el.html().length == 0 || !this.options.elementFirst + || (this.options.elementFirst && !this.firstRun); + if (loadAllowed && this.options.url !== false) { + this.loadContent(); + } else { + this._show(); + } + this.firstRun = false; + }, + + /** + * Load content + * + * @param {Object|null} data + * @param {String|null} method + */ + loadContent: function(data, method) { + var url = this.options.url; + if (url === undefined || !url) { + url = window.location.href; + } + if (this.firstRun || method === undefined || !method) { + method = 'get'; + } + var options = { + url: url, + type: method + }; + if (data !== undefined) { + options.data = data; + } + options.data = (options.data !== undefined ? options.data + '&' : '') + + '_widgetContainer=' + this.options.type + '&_wid=' + this.getWid(); + + Backbone.$.ajax(options).done(_.bind(function(content) { + try { + this.trigger('contentLoad', content, this); + this.actionsEl = null; + this.setElement($(content)); + this._show(); + } catch (error) { + // Remove state with unrestorable content + this.trigger('contentLoadError', this); + } + }, this)); + }, + + _show: function() { + this.trigger('renderStart', this.$el, this); + this._adoptWidgetActions(); + this.show(); + this.trigger('renderComplete', this.$el, this); + }, + + show: function() { + this.$el.attr('data-wid', this.getWid()); + this._renderActions(); + if (this.$el.hasClass('widget-content')) { + this.$el.trigger('widgetize', this); + } else { + this.$el.find('.widget-content').trigger('widgetize', this); + } + this.trigger('widgetRender', this.$el, this); + } +}); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js new file mode 100644 index 00000000000..b3024411725 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/block.js @@ -0,0 +1,77 @@ +var Oro = Oro || {}; +Oro.widget = Oro.widget || {}; + +Oro.widget.Block = Oro.widget.Abstract.extend({ + options: _.extend( + _.extend({}, Oro.widget.Abstract.prototype.options), + { + type: 'block', + titleContainer: '.widget-title', + actionsContainer: '.widget-actions-container', + contentContainer: '.row-fluid', + contentClasses: ['box-content'], + template: _.template('
    ' + + '
    ' + + '
    ' + + '<%- title %>' + + '
    ' + + '
    ' + + '
    ') + } + ), + + initialize: function(options) { + options = options || {} + this.initializeWidget(options); + + this.widget = Backbone.$(this.options.template({ + 'title': this.options.title, + 'contentClasses': this.options.contentClasses + })); + this.widgetContentContainer = this.widget.find(this.options.contentContainer); + }, + + setTitle: function(title) { + this.options.title = title; + this._getTitleContainer().html(this.options.title); + }, + + getActionsElement: function() { + if (this.actionsContainer === undefined) { + this.actionsContainer = this.widget.find(this.options.actionsContainer); + } + return this.actionsContainer; + }, + + _getTitleContainer: function() { + if (this.titleContainer === undefined) { + this.titleContainer = this.widget.find(this.options.titleContainer); + } + return this.titleContainer; + }, + + show: function() { + if (!this.$el.data('wid')) { + if (this.$el.parent().length) { + this._showStatic(); + } else { + this._showRemote(); + } + } + Oro.widget.Abstract.prototype.show.apply(this); + }, + + _showStatic: function() { + var anchorDiv = Backbone.$('
    '); + anchorDiv.insertAfter(this.$el); + this.widgetContentContainer.append(this.$el); + anchorDiv.replaceWith(Backbone.$(this.widget)); + }, + + _showRemote: function() { + this.widgetContentContainer.empty(); + this.widgetContentContainer.append(this.$el); + } +}); + +Oro.widget.Manager.registerWidgetContainer('block', Oro.widget.Block); diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/manager.js b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/manager.js new file mode 100644 index 00000000000..92b01d18d9d --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/backbone/widget/manager.js @@ -0,0 +1,29 @@ +var Oro = Oro || {}; +Oro.widget = Oro.widget || {}; + +Oro.widget.Manager = { + types: {}, + widgets: {}, + + isSupportedType: function(type) { + return this.types.hasOwnProperty(type); + }, + + registerWidgetContainer: function(type, initializer) { + this.types[type] = initializer; + }, + + createWidget: function(type, options) { + var widget = new this.types[type](options); + this.widgets[widget.getWid()] = widget; + return widget; + }, + + getWidgetInstance: function(wid) { + return this.widgets[wid]; + }, + + removeWidget: function(wid) { + delete this.widgets[wid]; + } +}; diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/height_fix.js b/src/Oro/Bundle/UIBundle/Resources/public/js/height_fix.js index dc6a0226318..697c7ae5709 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/height_fix.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/height_fix.js @@ -1,4 +1,8 @@ /* dynamic height for central column */ +jQuery.expr[':'].parents = function(a, i, m){ + return jQuery(a).parents(m[3]).length < 1; +}; + $(document).ready(function () { var debugBar = $('.sf-toolbar'); var anchor = $('#bottom-anchor'); @@ -17,7 +21,7 @@ $(document).ready(function () { var initializeContent = function() { if (!content) { - content = $('.scrollable-container'); + content = $('.scrollable-container').filter(':parents(.ui-widget)'); content.css('overflow', 'auto'); $('.scrollable-substructure').css({ diff --git a/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js b/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js index e4b771418e5..cc87f5e1fea 100644 --- a/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js +++ b/src/Oro/Bundle/UIBundle/Resources/public/js/layout.js @@ -110,7 +110,13 @@ $(document).ready(function () { * ============================================================ */ var dropdownToggles = $('.oro-dropdown-toggle'); dropdownToggles.click(function(e) { - $(this).parent().toggleClass('open') + var $parent = $(this).parent().toggleClass('open'); + if ($parent.hasClass('open')) { + $parent.find('input[type=text]').first().focus().select(); + } + }); + $('body').on('focus.dropdown.data-api', '[data-toggle=dropdown]', function (e) { + $(e.target).parent().find('input[type=text]').first().focus(); }); $('html').click(function(e) { @@ -172,7 +178,12 @@ function initLayout() { el = $(el); el.datepicker({ - dateFormat: el.attr('data-dateformat') ? el.attr('data-dateformat') : 'm/d/y' + dateFormat: el.attr('data-dateformat') ? el.attr('data-dateformat') : 'm/d/y', + changeMonth: true, + changeYear: true, + yearRange: '-80:+1', + showButtonPanel: true, + currentText: _.__('Now') }); }); } @@ -183,7 +194,12 @@ function initLayout() { el.datetimepicker({ dateFormat: el.attr('data-dateformat') ? el.attr('data-dateformat') : 'm/d/y', - timeFormat: el.attr('data-timeformat') ? el.attr('data-timeformat') : 'hh:mm tt' + timeFormat: el.attr('data-timeformat') ? el.attr('data-timeformat') : 'hh:mm tt', + changeMonth: true, + changeYear: true, + yearRange: '-80:+1', + showButtonPanel: true, + currentText: _.__('Now') }); }); } diff --git a/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery-1.10.2.js b/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery-1.10.2.js new file mode 100644 index 00000000000..c5c648255c1 --- /dev/null +++ b/src/Oro/Bundle/UIBundle/Resources/public/lib/jquery-1.10.2.js @@ -0,0 +1,9789 @@ +/*! + * jQuery JavaScript Library v1.10.2 + * http://jquery.com/ + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * + * Copyright 2005, 2013 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2013-07-03T13:48Z + */ +(function( window, undefined ) { + +// Can't do this because several apps including ASP.NET trace +// the stack via arguments.caller.callee and Firefox dies if +// you try to trace through "use strict" call chains. (#13335) +// Support: Firefox 18+ +//"use strict"; +var + // The deferred used on DOM ready + readyList, + + // A central reference to the root jQuery(document) + rootjQuery, + + // Support: IE<10 + // For `typeof xmlNode.method` instead of `xmlNode.method !== undefined` + core_strundefined = typeof undefined, + + // Use the correct document accordingly with window argument (sandbox) + location = window.location, + document = window.document, + docElem = document.documentElement, + + // Map over jQuery in case of overwrite + _jQuery = window.jQuery, + + // Map over the $ in case of overwrite + _$ = window.$, + + // [[Class]] -> type pairs + class2type = {}, + + // List of deleted data cache ids, so we can reuse them + core_deletedIds = [], + + core_version = "1.10.2", + + // Save a reference to some core methods + core_concat = core_deletedIds.concat, + core_push = core_deletedIds.push, + core_slice = core_deletedIds.slice, + core_indexOf = core_deletedIds.indexOf, + core_toString = class2type.toString, + core_hasOwn = class2type.hasOwnProperty, + core_trim = core_version.trim, + + // Define a local copy of jQuery + jQuery = function( selector, context ) { + // The jQuery object is actually just the init constructor 'enhanced' + return new jQuery.fn.init( selector, context, rootjQuery ); + }, + + // Used for matching numbers + core_pnum = /[+-]?(?:\d*\.|)\d+(?:[eE][+-]?\d+|)/.source, + + // Used for splitting on whitespace + core_rnotwhite = /\S+/g, + + // Make sure we trim BOM and NBSP (here's looking at you, Safari 5.0 and IE) + rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, + + // 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-]*))$/, + + // Match a standalone tag + rsingleTag = /^<(\w+)\s*\/?>(?:<\/\1>|)$/, + + // JSON RegExp + rvalidchars = /^[\],:{}\s]*$/, + rvalidbraces = /(?:^|:|,)(?:\s*\[)+/g, + rvalidescape = /\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g, + rvalidtokens = /"[^"\\\r\n]*"|true|false|null|-?(?:\d+\.|)\d+(?:[eE][+-]?\d+|)/g, + + // Matches dashed string for camelizing + rmsPrefix = /^-ms-/, + rdashAlpha = /-([\da-z])/gi, + + // Used by jQuery.camelCase as callback to replace() + fcamelCase = function( all, letter ) { + return letter.toUpperCase(); + }, + + // The ready event handler + completed = function( event ) { + + // readyState === "complete" is good enough for us to call the dom ready in oldIE + if ( document.addEventListener || event.type === "load" || document.readyState === "complete" ) { + detach(); + jQuery.ready(); + } + }, + // Clean-up method for dom ready events + detach = function() { + if ( document.addEventListener ) { + document.removeEventListener( "DOMContentLoaded", completed, false ); + window.removeEventListener( "load", completed, false ); + + } else { + document.detachEvent( "onreadystatechange", completed ); + window.detachEvent( "onload", completed ); + } + }; + +jQuery.fn = jQuery.prototype = { + // The current version of jQuery being used + jquery: core_version, + + constructor: jQuery, + init: function( selector, context, rootjQuery ) { + var match, elem; + + // HANDLE: $(""), $(null), $(undefined), $(false) + if ( !selector ) { + return this; + } + + // Handle HTML strings + if ( typeof selector === "string" ) { + if ( selector.charAt(0) === "<" && selector.charAt( 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 + 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 ) { + // Handle the case where IE and Opera return items + // by name instead of ID + if ( elem.id !== match[2] ) { + return rootjQuery.find( selector ); + } + + // Otherwise, we 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 rootjQuery.ready( selector ); + } + + if ( selector.selector !== undefined ) { + this.selector = selector.selector; + this.context = selector.context; + } + + return jQuery.makeArray( selector, this ); + }, + + // Start with an empty selector + selector: "", + + // The default length of a jQuery object is 0 + length: 0, + + toArray: function() { + return core_slice.call( this ); + }, + + // Get the Nth element in the matched element set OR + // Get the whole matched element set as a clean array + get: function( num ) { + return num == null ? + + // Return a 'clean' array + this.toArray() : + + // Return just the object + ( num < 0 ? this[ this.length + num ] : this[ num ] ); + }, + + // Take an array of elements and push it onto the stack + // (returning the new matched element set) + pushStack: function( elems ) { + + // Build a new jQuery matched element set + var ret = jQuery.merge( this.constructor(), elems ); + + // Add the old object onto the stack (as a reference) + ret.prevObject = this; + ret.context = this.context; + + // Return the newly-formed element set + return ret; + }, + + // Execute a callback for every element in the matched set. + // (You can seed the arguments with an array of args, but this is + // only used internally.) + each: function( callback, args ) { + return jQuery.each( this, callback, args ); + }, + + ready: function( fn ) { + // Add the callback + jQuery.ready.promise().done( fn ); + + return this; + }, + + slice: function() { + return this.pushStack( core_slice.apply( this, arguments ) ); + }, + + first: function() { + return this.eq( 0 ); + }, + + last: function() { + return this.eq( -1 ); + }, + + eq: function( i ) { + var len = this.length, + j = +i + ( i < 0 ? len : 0 ); + return this.pushStack( j >= 0 && j < len ? [ this[j] ] : [] ); + }, + + map: function( callback ) { + return this.pushStack( jQuery.map(this, function( elem, i ) { + return callback.call( elem, i, elem ); + })); + }, + + end: function() { + return this.prevObject || this.constructor(null); + }, + + // For internal use only. + // Behaves like an Array's method, not like a jQuery method. + push: core_push, + sort: [].sort, + splice: [].splice +}; + +// Give the init function the jQuery prototype for later instantiation +jQuery.fn.init.prototype = jQuery.fn; + +jQuery.extend = jQuery.fn.extend = function() { + var src, copyIsArray, copy, name, options, clone, + target = arguments[0] || {}, + i = 1, + length = arguments.length, + deep = false; + + // Handle a deep copy situation + if ( typeof target === "boolean" ) { + deep = target; + target = arguments[1] || {}; + // skip the boolean and the target + i = 2; + } + + // Handle case when target is a string or something (possible in deep copy) + if ( typeof target !== "object" && !jQuery.isFunction(target) ) { + target = {}; + } + + // extend jQuery itself if only one argument is passed + if ( length === i ) { + target = this; + --i; + } + + for ( ; i < length; i++ ) { + // Only deal with non-null/undefined values + if ( (options = arguments[ i ]) != null ) { + // Extend the base object + for ( name in options ) { + src = target[ name ]; + copy = options[ name ]; + + // Prevent never-ending loop + if ( target === copy ) { + continue; + } + + // Recurse if we're merging plain objects or arrays + if ( deep && copy && ( jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)) ) ) { + if ( copyIsArray ) { + copyIsArray = false; + clone = src && jQuery.isArray(src) ? src : []; + + } else { + clone = src && jQuery.isPlainObject(src) ? src : {}; + } + + // Never move original objects, clone them + target[ name ] = jQuery.extend( deep, clone, copy ); + + // Don't bring in undefined values + } else if ( copy !== undefined ) { + target[ name ] = copy; + } + } + } + } + + // Return the modified object + return target; +}; + +jQuery.extend({ + // Unique for each copy of jQuery on the page + // Non-digits removed to match rinlinejQuery + expando: "jQuery" + ( core_version + Math.random() ).replace( /\D/g, "" ), + + noConflict: function( deep ) { + if ( window.$ === jQuery ) { + window.$ = _$; + } + + if ( deep && window.jQuery === jQuery ) { + window.jQuery = _jQuery; + } + + return jQuery; + }, + + // 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; + } + + // Make sure body exists, at least, in case IE gets a little overzealous (ticket #5443). + if ( !document.body ) { + return setTimeout( jQuery.ready ); + } + + // 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.trigger ) { + jQuery( document ).trigger("ready").off("ready"); + } + }, + + // See test/unit/core.js for details concerning isFunction. + // Since version 1.3, DOM methods and functions like alert + // aren't supported. They return false on IE (#2968). + isFunction: function( obj ) { + return jQuery.type(obj) === "function"; + }, + + isArray: Array.isArray || function( obj ) { + return jQuery.type(obj) === "array"; + }, + + isWindow: function( obj ) { + /* jshint eqeqeq: false */ + return obj != null && obj == obj.window; + }, + + isNumeric: function( obj ) { + return !isNaN( parseFloat(obj) ) && isFinite( obj ); + }, + + type: function( obj ) { + if ( obj == null ) { + return String( obj ); + } + return typeof obj === "object" || typeof obj === "function" ? + class2type[ core_toString.call(obj) ] || "object" : + typeof obj; + }, + + isPlainObject: function( obj ) { + var key; + + // Must be an Object. + // Because of IE, we also have to check the presence of the constructor property. + // Make sure that DOM nodes and window objects don't pass through, as well + if ( !obj || jQuery.type(obj) !== "object" || obj.nodeType || jQuery.isWindow( obj ) ) { + return false; + } + + try { + // Not own constructor property must be Object + if ( obj.constructor && + !core_hasOwn.call(obj, "constructor") && + !core_hasOwn.call(obj.constructor.prototype, "isPrototypeOf") ) { + return false; + } + } catch ( e ) { + // IE8,9 Will throw exceptions on certain host objects #9897 + return false; + } + + // Support: IE<9 + // Handle iteration over inherited properties before own properties. + if ( jQuery.support.ownLast ) { + for ( key in obj ) { + return core_hasOwn.call( obj, key ); + } + } + + // Own properties are enumerated firstly, so to speed up, + // if last one is own, then all properties are own. + for ( key in obj ) {} + + return key === undefined || core_hasOwn.call( obj, key ); + }, + + isEmptyObject: function( obj ) { + var name; + for ( name in obj ) { + return false; + } + return true; + }, + + error: function( msg ) { + throw new Error( msg ); + }, + + // data: string of html + // context (optional): If specified, the fragment will be created in this context, defaults to document + // keepScripts (optional): If true, will include scripts passed in the html string + parseHTML: function( data, context, keepScripts ) { + if ( !data || typeof data !== "string" ) { + return null; + } + if ( typeof context === "boolean" ) { + keepScripts = context; + context = false; + } + context = context || document; + + var parsed = rsingleTag.exec( data ), + scripts = !keepScripts && []; + + // Single tag + if ( parsed ) { + return [ context.createElement( parsed[1] ) ]; + } + + parsed = jQuery.buildFragment( [ data ], context, scripts ); + if ( scripts ) { + jQuery( scripts ).remove(); + } + return jQuery.merge( [], parsed.childNodes ); + }, + + parseJSON: function( data ) { + // Attempt to parse using the native JSON parser first + if ( window.JSON && window.JSON.parse ) { + return window.JSON.parse( data ); + } + + if ( data === null ) { + return data; + } + + if ( typeof data === "string" ) { + + // Make sure leading/trailing whitespace is removed (IE can't handle it) + data = jQuery.trim( data ); + + if ( data ) { + // Make sure the incoming data is actual JSON + // Logic borrowed from http://json.org/json2.js + if ( rvalidchars.test( data.replace( rvalidescape, "@" ) + .replace( rvalidtokens, "]" ) + .replace( rvalidbraces, "")) ) { + + return ( new Function( "return " + data ) )(); + } + } + } + + jQuery.error( "Invalid JSON: " + data ); + }, + + // Cross-browser xml parsing + parseXML: function( data ) { + var xml, tmp; + if ( !data || typeof data !== "string" ) { + return null; + } + try { + if ( window.DOMParser ) { // Standard + tmp = new DOMParser(); + xml = tmp.parseFromString( data , "text/xml" ); + } else { // IE + xml = new ActiveXObject( "Microsoft.XMLDOM" ); + xml.async = "false"; + xml.loadXML( data ); + } + } catch( e ) { + xml = undefined; + } + if ( !xml || !xml.documentElement || xml.getElementsByTagName( "parsererror" ).length ) { + jQuery.error( "Invalid XML: " + data ); + } + return xml; + }, + + noop: function() {}, + + // Evaluates a script in a global context + // Workarounds based on findings by Jim Driscoll + // http://weblogs.java.net/blog/driscoll/archive/2009/09/08/eval-javascript-global-context + globalEval: function( data ) { + if ( data && jQuery.trim( data ) ) { + // We use execScript on Internet Explorer + // We use an anonymous function so that context is window + // rather than jQuery in Firefox + ( window.execScript || function( data ) { + window[ "eval" ].call( window, data ); + } )( data ); + } + }, + + // Convert dashed to camelCase; used by the css and data modules + // Microsoft forgot to hump their vendor prefix (#9572) + camelCase: function( string ) { + return string.replace( rmsPrefix, "ms-" ).replace( rdashAlpha, fcamelCase ); + }, + + nodeName: function( elem, name ) { + return elem.nodeName && elem.nodeName.toLowerCase() === name.toLowerCase(); + }, + + // args is for internal usage only + each: function( obj, callback, args ) { + var value, + i = 0, + length = obj.length, + isArray = isArraylike( obj ); + + if ( args ) { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.apply( obj[ i ], args ); + + if ( value === false ) { + break; + } + } + } + + // A special, fast, case for the most common use of each + } else { + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } else { + for ( i in obj ) { + value = callback.call( obj[ i ], i, obj[ i ] ); + + if ( value === false ) { + break; + } + } + } + } + + return obj; + }, + + // Use native String.trim function wherever possible + trim: core_trim && !core_trim.call("\uFEFF\xA0") ? + function( text ) { + return text == null ? + "" : + core_trim.call( text ); + } : + + // Otherwise use our own trimming functionality + function( text ) { + return text == null ? + "" : + ( text + "" ).replace( rtrim, "" ); + }, + + // results is for internal usage only + makeArray: function( arr, results ) { + var ret = results || []; + + if ( arr != null ) { + if ( isArraylike( Object(arr) ) ) { + jQuery.merge( ret, + typeof arr === "string" ? + [ arr ] : arr + ); + } else { + core_push.call( ret, arr ); + } + } + + return ret; + }, + + inArray: function( elem, arr, i ) { + var len; + + if ( arr ) { + if ( core_indexOf ) { + return core_indexOf.call( arr, elem, i ); + } + + len = arr.length; + i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0; + + for ( ; i < len; i++ ) { + // Skip accessing in sparse arrays + if ( i in arr && arr[ i ] === elem ) { + return i; + } + } + } + + return -1; + }, + + merge: function( first, second ) { + var l = second.length, + i = first.length, + j = 0; + + if ( typeof l === "number" ) { + for ( ; j < l; j++ ) { + first[ i++ ] = second[ j ]; + } + } else { + while ( second[j] !== undefined ) { + first[ i++ ] = second[ j++ ]; + } + } + + first.length = i; + + return first; + }, + + grep: function( elems, callback, inv ) { + var retVal, + ret = [], + i = 0, + length = elems.length; + inv = !!inv; + + // Go through the array, only saving the items + // that pass the validator function + for ( ; i < length; i++ ) { + retVal = !!callback( elems[ i ], i ); + if ( inv !== retVal ) { + ret.push( elems[ i ] ); + } + } + + return ret; + }, + + // arg is for internal usage only + map: function( elems, callback, arg ) { + var value, + i = 0, + length = elems.length, + isArray = isArraylike( elems ), + ret = []; + + // Go through the array, translating each of the items to their + if ( isArray ) { + for ( ; i < length; i++ ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + + // Go through every key on the object, + } else { + for ( i in elems ) { + value = callback( elems[ i ], i, arg ); + + if ( value != null ) { + ret[ ret.length ] = value; + } + } + } + + // Flatten any nested arrays + return core_concat.apply( [], ret ); + }, + + // A global GUID counter for objects + guid: 1, + + // Bind a function to a context, optionally partially applying any + // arguments. + proxy: function( fn, context ) { + var args, proxy, tmp; + + if ( typeof context === "string" ) { + tmp = fn[ context ]; + context = fn; + fn = tmp; + } + + // Quick check to determine if target is callable, in the spec + // this throws a TypeError, but we will just return undefined. + if ( !jQuery.isFunction( fn ) ) { + return undefined; + } + + // Simulated bind + args = core_slice.call( arguments, 2 ); + proxy = function() { + return fn.apply( context || this, args.concat( core_slice.call( arguments ) ) ); + }; + + // Set the guid of unique handler to the same of original handler, so it can be removed + proxy.guid = fn.guid = fn.guid || jQuery.guid++; + + return proxy; + }, + + // Multifunctional method to get and set values of a collection + // The value/s can optionally be executed if it's a function + access: function( elems, fn, key, value, chainable, emptyGet, raw ) { + var i = 0, + length = 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 < length; i++ ) { + fn( elems[i], key, raw ? value : value.call( elems[i], i, fn( elems[i], key ) ) ); + } + } + } + + return chainable ? + elems : + + // Gets + bulk ? + fn.call( elems ) : + length ? fn( elems[0], key ) : emptyGet; + }, + + now: function() { + return ( new Date() ).getTime(); + }, + + // A method for quickly swapping in/out CSS properties to get correct calculations. + // Note: this method belongs to the css module but it's needed here for the support module. + // If support gets modularized, this method should be moved back to the css module. + swap: function( elem, options, callback, args ) { + var ret, name, + old = {}; + + // Remember the old values, and insert the new ones + for ( name in options ) { + old[ name ] = elem.style[ name ]; + elem.style[ name ] = options[ name ]; + } + + ret = callback.apply( elem, args || [] ); + + // Revert the old values + for ( name in options ) { + elem.style[ name ] = old[ name ]; + } + + return ret; + } +}); + +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 ); + + // Standards-based browsers support DOMContentLoaded + } else if ( document.addEventListener ) { + // Use the handy event callback + document.addEventListener( "DOMContentLoaded", completed, false ); + + // A fallback to window.onload, that will always work + window.addEventListener( "load", completed, false ); + + // If IE event model is used + } else { + // Ensure firing before onload, maybe late but safe also for iframes + document.attachEvent( "onreadystatechange", completed ); + + // A fallback to window.onload, that will always work + window.attachEvent( "onload", completed ); + + // If IE and not a frame + // continually check to see if the document is ready + var top = false; + + try { + top = window.frameElement == null && document.documentElement; + } catch(e) {} + + if ( top && top.doScroll ) { + (function doScrollCheck() { + if ( !jQuery.isReady ) { + + try { + // Use the trick by Diego Perini + // http://javascript.nwbox.com/IEContentLoaded/ + top.doScroll("left"); + } catch(e) { + return setTimeout( doScrollCheck, 50 ); + } + + // detach all dom ready events + detach(); + + // and execute any waiting functions + jQuery.ready(); + } + })(); + } + } + } + return readyList.promise( obj ); +}; + +// Populate the class2type map +jQuery.each("Boolean Number String Function Array Date RegExp Object Error".split(" "), function(i, name) { + class2type[ "[object " + name + "]" ] = name.toLowerCase(); +}); + +function isArraylike( obj ) { + var length = obj.length, + type = jQuery.type( obj ); + + if ( jQuery.isWindow( obj ) ) { + return false; + } + + if ( obj.nodeType === 1 && length ) { + return true; + } + + return type === "array" || type !== "function" && + ( length === 0 || + typeof length === "number" && length > 0 && ( length - 1 ) in obj ); +} + +// All jQuery objects should point back to these +rootjQuery = jQuery(document); +/*! + * Sizzle CSS Selector Engine v1.10.2 + * http://sizzlejs.com/ + * + * Copyright 2013 jQuery Foundation, Inc. and other contributors + * Released under the MIT license + * http://jquery.org/license + * + * Date: 2013-07-03 + */ +(function( window, undefined ) { + +var i, + support, + cachedruns, + Expr, + getText, + isXML, + compile, + outermostContext, + sortInput, + + // Local document vars + setDocument, + document, + docElem, + documentIsHTML, + rbuggyQSA, + rbuggyMatches, + matches, + contains, + + // Instance-specific data + expando = "sizzle" + -(new Date()), + preferredDoc = window.document, + dirruns = 0, + done = 0, + classCache = createCache(), + tokenCache = createCache(), + compilerCache = createCache(), + hasDuplicate = false, + sortOrder = function( a, b ) { + if ( a === b ) { + hasDuplicate = true; + return 0; + } + return 0; + }, + + // General-purpose constants + strundefined = typeof undefined, + MAX_NEGATIVE = 1 << 31, + + // Instance methods + hasOwn = ({}).hasOwnProperty, + arr = [], + pop = arr.pop, + push_native = arr.push, + push = arr.push, + slice = arr.slice, + // Use a stripped-down indexOf if we can't use a native one + indexOf = arr.indexOf || function( elem ) { + var i = 0, + len = this.length; + for ( ; i < len; i++ ) { + if ( this[i] === elem ) { + return i; + } + } + return -1; + }, + + booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped", + + // Regular expressions + + // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace + whitespace = "[\\x20\\t\\r\\n\\f]", + // http://www.w3.org/TR/css3-syntax/#characters + characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+", + + // Loosely modeled on CSS identifier characters + // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors + // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier + identifier = characterEncoding.replace( "w", "w#" ), + + // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors + attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace + + "*(?:([*^$|!~]?=)" + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]", + + // Prefer arguments quoted, + // then not containing pseudos/brackets, + // then attribute selectors/non-parenthetical expressions, + // then anything else + // These preferences are here to reduce the number of selectors + // needing tokenize in the PSEUDO preFilter + pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)", + + // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter + rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ), + + rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ), + rcombinators = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace + "*" ), + + rsibling = new RegExp( whitespace + "*[+~]" ), + rattributeQuotes = new RegExp( "=" + whitespace + "*([^\\]'\"]*)" + whitespace + "*\\]", "g" ), + + rpseudo = new RegExp( pseudos ), + ridentifier = new RegExp( "^" + identifier + "$" ), + + matchExpr = { + "ID": new RegExp( "^#(" + characterEncoding + ")" ), + "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ), + "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ), + "ATTR": new RegExp( "^" + attributes ), + "PSEUDO": new RegExp( "^" + pseudos ), + "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace + + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace + + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ), + "bool": new RegExp( "^(?:" + booleans + ")$", "i" ), + // For use in libraries implementing .is() + // We use this for POS matching in `select` + "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + + whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" ) + }, + + rnative = /^[^{]+\{\s*\[native \w/, + + // Easily-parseable/retrievable ID or TAG or CLASS selectors + rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/, + + rinputs = /^(?:input|select|textarea|button)$/i, + rheader = /^h\d$/i, + + rescape = /'|\\/g, + + // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters + runescape = new RegExp( "\\\\([\\da-f]{1,6}" + whitespace + "?|(" + whitespace + ")|.)", "ig" ), + funescape = function( _, escaped, escapedWhitespace ) { + var high = "0x" + escaped - 0x10000; + // NaN means non-codepoint + // Support: Firefox + // Workaround erroneous numeric interpretation of +"0x" + return high !== high || escapedWhitespace ? + escaped : + // BMP codepoint + high < 0 ? + String.fromCharCode( high + 0x10000 ) : + // Supplemental Plane codepoint (surrogate pair) + String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 ); + }; + +// Optimize for push.apply( _, NodeList ) +try { + push.apply( + (arr = slice.call( preferredDoc.childNodes )), + preferredDoc.childNodes + ); + // Support: Android<4.0 + // Detect silently failing push.apply + arr[ preferredDoc.childNodes.length ].nodeType; +} catch ( e ) { + push = { apply: arr.length ? + + // Leverage slice if possible + function( target, els ) { + push_native.apply( target, slice.call(els) ); + } : + + // Support: IE<9 + // Otherwise append directly + function( target, els ) { + var j = target.length, + i = 0; + // Can't trust NodeList.length + while ( (target[j++] = els[i++]) ) {} + target.length = j - 1; + } + }; +} + +function Sizzle( selector, context, results, seed ) { + var match, elem, m, nodeType, + // QSA vars + i, groups, old, nid, newContext, newSelector; + + if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) { + setDocument( context ); + } + + context = context || document; + results = results || []; + + if ( !selector || typeof selector !== "string" ) { + return results; + } + + if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) { + return []; + } + + if ( documentIsHTML && !seed ) { + + // Shortcuts + if ( (match = rquickExpr.exec( selector )) ) { + // Speed-up: Sizzle("#ID") + if ( (m = match[1]) ) { + if ( nodeType === 9 ) { + elem = context.getElementById( m ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + if ( elem && elem.parentNode ) { + // Handle the case where IE, Opera, and Webkit return items + // by name instead of ID + if ( elem.id === m ) { + results.push( elem ); + return results; + } + } else { + return results; + } + } else { + // Context is not a document + if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) && + contains( context, elem ) && elem.id === m ) { + results.push( elem ); + return results; + } + } + + // Speed-up: Sizzle("TAG") + } else if ( match[2] ) { + push.apply( results, context.getElementsByTagName( selector ) ); + return results; + + // Speed-up: Sizzle(".CLASS") + } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) { + push.apply( results, context.getElementsByClassName( m ) ); + return results; + } + } + + // QSA path + if ( support.qsa && (!rbuggyQSA || !rbuggyQSA.test( selector )) ) { + nid = old = expando; + newContext = context; + newSelector = nodeType === 9 && selector; + + // qSA works strangely on Element-rooted queries + // We can work around this by specifying an extra ID on the root + // and working up from there (Thanks to Andrew Dupont for the technique) + // IE 8 doesn't work on object elements + if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) { + groups = tokenize( selector ); + + if ( (old = context.getAttribute("id")) ) { + nid = old.replace( rescape, "\\$&" ); + } else { + context.setAttribute( "id", nid ); + } + nid = "[id='" + nid + "'] "; + + i = groups.length; + while ( i-- ) { + groups[i] = nid + toSelector( groups[i] ); + } + newContext = rsibling.test( selector ) && context.parentNode || context; + newSelector = groups.join(","); + } + + if ( newSelector ) { + try { + push.apply( results, + newContext.querySelectorAll( newSelector ) + ); + return results; + } catch(qsaError) { + } finally { + if ( !old ) { + context.removeAttribute("id"); + } + } + } + } + } + + // All others + return select( selector.replace( rtrim, "$1" ), context, results, seed ); +} + +/** + * Create key-value caches of limited size + * @returns {Function(string, Object)} Returns the Object data after storing it on itself with + * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength) + * deleting the oldest entry + */ +function createCache() { + var keys = []; + + function cache( key, value ) { + // Use (key + " ") to avoid collision with native prototype properties (see Issue #157) + if ( keys.push( key += " " ) > Expr.cacheLength ) { + // Only keep the most recent entries + delete cache[ keys.shift() ]; + } + return (cache[ key ] = value); + } + return cache; +} + +/** + * Mark a function for special use by Sizzle + * @param {Function} fn The function to mark + */ +function markFunction( fn ) { + fn[ expando ] = true; + return fn; +} + +/** + * Support testing using an element + * @param {Function} fn Passed the created div and expects a boolean result + */ +function assert( fn ) { + var div = document.createElement("div"); + + try { + return !!fn( div ); + } catch (e) { + return false; + } finally { + // Remove from its parent by default + if ( div.parentNode ) { + div.parentNode.removeChild( div ); + } + // release memory in IE + div = null; + } +} + +/** + * Adds the same handler for all of the specified attrs + * @param {String} attrs Pipe-separated list of attributes + * @param {Function} handler The method that will be applied + */ +function addHandle( attrs, handler ) { + var arr = attrs.split("|"), + i = attrs.length; + + while ( i-- ) { + Expr.attrHandle[ arr[i] ] = handler; + } +} + +/** + * Checks document order of two siblings + * @param {Element} a + * @param {Element} b + * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b + */ +function siblingCheck( a, b ) { + var cur = b && a, + diff = cur && a.nodeType === 1 && b.nodeType === 1 && + ( ~b.sourceIndex || MAX_NEGATIVE ) - + ( ~a.sourceIndex || MAX_NEGATIVE ); + + // Use IE sourceIndex if available on both nodes + if ( diff ) { + return diff; + } + + // Check if b follows a + if ( cur ) { + while ( (cur = cur.nextSibling) ) { + if ( cur === b ) { + return -1; + } + } + } + + return a ? 1 : -1; +} + +/** + * Returns a function to use in pseudos for input types + * @param {String} type + */ +function createInputPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return name === "input" && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for buttons + * @param {String} type + */ +function createButtonPseudo( type ) { + return function( elem ) { + var name = elem.nodeName.toLowerCase(); + return (name === "input" || name === "button") && elem.type === type; + }; +} + +/** + * Returns a function to use in pseudos for positionals + * @param {Function} fn + */ +function createPositionalPseudo( fn ) { + return markFunction(function( argument ) { + argument = +argument; + return markFunction(function( seed, matches ) { + var j, + matchIndexes = fn( [], seed.length, argument ), + i = matchIndexes.length; + + // Match elements found at the specified indexes + while ( i-- ) { + if ( seed[ (j = matchIndexes[i]) ] ) { + seed[j] = !(matches[j] = seed[j]); + } + } + }); + }); +} + +/** + * Detect xml + * @param {Element|Object} elem An element or a document + */ +isXML = Sizzle.isXML = function( elem ) { + // documentElement is verified for cases where it doesn't yet exist + // (such as loading iframes in IE - #4833) + var documentElement = elem && (elem.ownerDocument || elem).documentElement; + return documentElement ? documentElement.nodeName !== "HTML" : false; +}; + +// Expose support vars for convenience +support = Sizzle.support = {}; + +/** + * Sets document-related variables once based on the current document + * @param {Element|Object} [doc] An element or document object to use to set the document + * @returns {Object} Returns the current document + */ +setDocument = Sizzle.setDocument = function( node ) { + var doc = node ? node.ownerDocument || node : preferredDoc, + parent = doc.defaultView; + + // If no document and documentElement is available, return + if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) { + return document; + } + + // Set our document + document = doc; + docElem = doc.documentElement; + + // Support tests + documentIsHTML = !isXML( doc ); + + // Support: IE>8 + // If iframe document is assigned to "document" variable and if iframe has been reloaded, + // IE will throw "permission denied" error when accessing "document" variable, see jQuery #13936 + // IE6-8 do not support the defaultView property so parent will be undefined + if ( parent && parent.attachEvent && parent !== parent.top ) { + parent.attachEvent( "onbeforeunload", function() { + setDocument(); + }); + } + + /* Attributes + ---------------------------------------------------------------------- */ + + // Support: IE<8 + // Verify that getAttribute really returns attributes and not properties (excepting IE8 booleans) + support.attributes = assert(function( div ) { + div.className = "i"; + return !div.getAttribute("className"); + }); + + /* getElement(s)By* + ---------------------------------------------------------------------- */ + + // Check if getElementsByTagName("*") returns only elements + support.getElementsByTagName = assert(function( div ) { + div.appendChild( doc.createComment("") ); + return !div.getElementsByTagName("*").length; + }); + + // Check if getElementsByClassName can be trusted + support.getElementsByClassName = assert(function( div ) { + div.innerHTML = "
    "; + + // Support: Safari<4 + // Catch class over-caching + div.firstChild.className = "i"; + // Support: Opera<10 + // Catch gEBCN failure to find non-leading classes + return div.getElementsByClassName("i").length === 2; + }); + + // Support: IE<10 + // Check if getElementById returns elements by name + // The broken getElementById methods don't pick up programatically-set names, + // so use a roundabout getElementsByName test + support.getById = assert(function( div ) { + docElem.appendChild( div ).id = expando; + return !doc.getElementsByName || !doc.getElementsByName( expando ).length; + }); + + // ID find and filter + if ( support.getById ) { + Expr.find["ID"] = function( id, context ) { + if ( typeof context.getElementById !== strundefined && documentIsHTML ) { + var m = context.getElementById( id ); + // Check parentNode to catch when Blackberry 4.6 returns + // nodes that are no longer in the document #6963 + return m && m.parentNode ? [m] : []; + } + }; + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + return elem.getAttribute("id") === attrId; + }; + }; + } else { + // Support: IE6/7 + // getElementById is not reliable as a find shortcut + delete Expr.find["ID"]; + + Expr.filter["ID"] = function( id ) { + var attrId = id.replace( runescape, funescape ); + return function( elem ) { + var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id"); + return node && node.value === attrId; + }; + }; + } + + // Tag + Expr.find["TAG"] = support.getElementsByTagName ? + function( tag, context ) { + if ( typeof context.getElementsByTagName !== strundefined ) { + return context.getElementsByTagName( tag ); + } + } : + function( tag, context ) { + var elem, + tmp = [], + i = 0, + results = context.getElementsByTagName( tag ); + + // Filter out possible comments + if ( tag === "*" ) { + while ( (elem = results[i++]) ) { + if ( elem.nodeType === 1 ) { + tmp.push( elem ); + } + } + + return tmp; + } + return results; + }; + + // Class + Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) { + if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) { + return context.getElementsByClassName( className ); + } + }; + + /* QSA/matchesSelector + ---------------------------------------------------------------------- */ + + // QSA and matchesSelector support + + // matchesSelector(:active) reports false when true (IE9/Opera 11.5) + rbuggyMatches = []; + + // qSa(:focus) reports false when true (Chrome 21) + // We allow this because of a bug in IE8/9 that throws an error + // whenever `document.activeElement` is accessed on an iframe + // So, we allow :focus to pass through QSA all the time to avoid the IE error + // See http://bugs.jquery.com/ticket/13378 + rbuggyQSA = []; + + if ( (support.qsa = rnative.test( doc.querySelectorAll )) ) { + // Build QSA regex + // Regex strategy adopted from Diego Perini + assert(function( div ) { + // Select is set to empty string on purpose + // This is to test IE's treatment of not explicitly + // setting a boolean content attribute, + // since its presence should be enough + // http://bugs.jquery.com/ticket/12359 + div.innerHTML = ""; + + // 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: Opera 10-12/IE8 + // ^= $= *= and empty values + // Should not select anything + // Support: Windows 8 Native Apps + // The type attribute is restricted during .innerHTML assignment + var input = doc.createElement("input"); + input.setAttribute( "type", "hidden" ); + div.appendChild( input ).setAttribute( "t", "" ); + + if ( div.querySelectorAll("[t^='']").length ) { + rbuggyQSA.push( "[*^$]=" + 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.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 + ---------------------------------------------------------------------- */ + + // Element contains another + // Purposefully does not implement inclusive descendent + // As in, an element does not contain itself + contains = rnative.test( docElem.contains ) || docElem.compareDocumentPosition ? + 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 = docElem.compareDocumentPosition ? + function( a, b ) { + + // Flag for duplicate removal + if ( a === b ) { + hasDuplicate = true; + return 0; + } + + var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b ); + + if ( compare ) { + // 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 || contains(preferredDoc, a) ) { + return -1; + } + if ( b === doc || contains(preferredDoc, b) ) { + return 1; + } + + // Maintain original order + return sortInput ? + ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) : + 0; + } + + return compare & 4 ? -1 : 1; + } + + // Not directly comparable, sort on existence of method + return a.compareDocumentPosition ? -1 : 1; + } : + function( a, b ) { + var cur, + i = 0, + aup = a.parentNode, + bup = b.parentNode, + ap = [ a ], + bp = [ b ]; + + // Exit early if the nodes are identical + if ( a === b ) { + hasDuplicate = true; + return 0; + + // Parentless nodes are either documents or disconnected + } else 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 ? + support.attributes || !documentIsHTML ? + elem.getAttribute( name ) : + (val = elem.getAttributeNode(name)) && val.specified ? + val.value : + null : + val; +}; + +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 ); + } + } + + 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 + for ( ; (node = elem[i]); 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 (see #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[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[5] && match[2]; + + if ( matchExpr["CHILD"].test( match[0] ) ) { + return null; + } + + // Accept quoted arguments as-is + if ( match[3] && match[4] !== undefined ) { + match[2] = match[4]; + + // 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 only affected by element nodes and content nodes(including text(3), cdata(4)), + // not comment, processing instructions, or others + // Thanks to Diego Perini for the nodeName shortcut + // Greater than "@" means alpha characters (specifically not starting with "#" or "?") + for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) { + if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) { + 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; + // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc) + // use getAttribute instead to test this case + return elem.nodeName.toLowerCase() === "input" && + elem.type === "text" && + ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type ); + }, + + // 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(); + +function tokenize( 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 data, cache, outerCache, + dirkey = 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 ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) { + if ( (data = cache[1]) === true || data === cachedruns ) { + return data === true; + } + } else { + cache = outerCache[ dir ] = [ dirkey ]; + cache[1] = matcher( elem, context, xml ) || cachedruns; + if ( cache[1] === true ) { + 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 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 ) { + // A counter to specify which element is currently being matched + var matcherCachedRuns = 0, + bySet = setMatchers.length > 0, + byElement = elementMatchers.length > 0, + superMatcher = function( seed, context, xml, results, expandContext ) { + var elem, j, matcher, + setMatched = [], + matchedCount = 0, + i = "0", + unmatched = seed && [], + outermost = expandContext != null, + contextBackup = outermostContext, + // We must always have either seed elements or context + elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ), + // Use integer dirruns iff this is the outermost matcher + dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1); + + if ( outermost ) { + outermostContext = context !== document && context; + cachedruns = matcherCachedRuns; + } + + // Add elements passing elementMatchers directly to results + // Keep `i` a string if there are no elements so `matchedCount` will be "00" below + for ( ; (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; + cachedruns = ++matcherCachedRuns; + } + } + + // 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, group /* 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 ( !group ) { + group = tokenize( selector ); + } + i = group.length; + while ( i-- ) { + cached = matcherFromTokens( group[i] ); + if ( cached[ expando ] ) { + setMatchers.push( cached ); + } else { + elementMatchers.push( cached ); + } + } + + // Cache the compiled function + cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) ); + } + return cached; +}; + +function multipleContexts( selector, contexts, results ) { + var i = 0, + len = contexts.length; + for ( ; i < len; i++ ) { + Sizzle( selector, contexts[i], results ); + } + return results; +} + +function select( selector, context, results, seed ) { + var i, tokens, token, type, find, + match = tokenize( selector ); + + if ( !seed ) { + // Try to minimize operations if there is 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; + } + 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 ) && 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 + // Provide `match` to avoid retokenization if we modified the selector above + compile( selector, match )( + seed, + context, + !documentIsHTML, + results, + rsibling.test( selector ) + ); + 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 (val = elem.getAttributeNode( name )) && val.specified ? + val.value : + elem[ name ] === true ? name.toLowerCase() : null; + } + }); +} + +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; + + +})( window ); +// 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( core_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 // Flag to know if list is currently firing + firing, + // Last fire value (for non-forgettable lists) + memory, + // Flag to know if list was already fired + fired, + // End of the loop when firing + firingLength, + // Index of currently firing callback (modified by remove if needed) + firingIndex, + // First callback to fire (used internally by add and fireWith) + firingStart, + // 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 action = tuple[ 0 ], + 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[ action + "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 = core_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 ? core_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(); + } +}); +jQuery.support = (function( support ) { + + var all, a, input, select, fragment, opt, eventName, isSupported, i, + div = document.createElement("div"); + + // Setup + div.setAttribute( "className", "t" ); + div.innerHTML = "
    a"; + + // Finish early in limited (non-browser) environments + all = div.getElementsByTagName("*") || []; + a = div.getElementsByTagName("a")[ 0 ]; + if ( !a || !a.style || !all.length ) { + return support; + } + + // First batch of tests + select = document.createElement("select"); + opt = select.appendChild( document.createElement("option") ); + input = div.getElementsByTagName("input")[ 0 ]; + + a.style.cssText = "top:1px;float:left;opacity:.5"; + + // Test setAttribute on camelCase class. If it works, we need attrFixes when doing get/setAttribute (ie6/7) + support.getSetAttribute = div.className !== "t"; + + // IE strips leading whitespace when .innerHTML is used + support.leadingWhitespace = div.firstChild.nodeType === 3; + + // Make sure that tbody elements aren't automatically inserted + // IE will insert them into empty tables + support.tbody = !div.getElementsByTagName("tbody").length; + + // Make sure that link elements get serialized correctly by innerHTML + // This requires a wrapper element in IE + support.htmlSerialize = !!div.getElementsByTagName("link").length; + + // Get the style information from getAttribute + // (IE uses .cssText instead) + support.style = /top/.test( a.getAttribute("style") ); + + // Make sure that URLs aren't manipulated + // (IE normalizes it by default) + support.hrefNormalized = a.getAttribute("href") === "/a"; + + // Make sure that element opacity exists + // (IE uses filter instead) + // Use a regex to work around a WebKit issue. See #5145 + support.opacity = /^0.5/.test( a.style.opacity ); + + // Verify style float existence + // (IE uses styleFloat instead of cssFloat) + support.cssFloat = !!a.style.cssFloat; + + // Check the default checkbox/radio value ("" on WebKit; "on" elsewhere) + support.checkOn = !!input.value; + + // Make sure that a selected-by-default option has a working selected property. + // (WebKit defaults to false instead of true, IE too, if it's in an optgroup) + support.optSelected = opt.selected; + + // Tests for enctype support on a form (#6743) + support.enctype = !!document.createElement("form").enctype; + + // Makes sure cloning an html5 element does not cause problems + // Where outerHTML is undefined, this still works + support.html5Clone = document.createElement("nav").cloneNode( true ).outerHTML !== "<:nav>"; + + // Will be defined later + support.inlineBlockNeedsLayout = false; + support.shrinkWrapBlocks = false; + support.pixelPosition = false; + support.deleteExpando = true; + support.noCloneEvent = true; + support.reliableMarginRight = true; + support.boxSizingReliable = true; + + // Make sure checked status is properly cloned + input.checked = true; + support.noCloneChecked = input.cloneNode( true ).checked; + + // Make sure that the options inside disabled selects aren't marked as disabled + // (WebKit marks them as disabled) + select.disabled = true; + support.optDisabled = !opt.disabled; + + // Support: IE<9 + try { + delete div.test; + } catch( e ) { + support.deleteExpando = false; + } + + // Check if we can trust getAttribute("value") + input = document.createElement("input"); + input.setAttribute( "value", "" ); + support.input = input.getAttribute( "value" ) === ""; + + // Check if an input maintains its value after becoming a radio + input.value = "t"; + input.setAttribute( "type", "radio" ); + support.radioValue = input.value === "t"; + + // #11217 - WebKit loses check when the name is after the checked attribute + input.setAttribute( "checked", "t" ); + input.setAttribute( "name", "t" ); + + fragment = document.createDocumentFragment(); + fragment.appendChild( input ); + + // Check if a disconnected checkbox will retain its checked + // value of true after appended to the DOM (IE6/7) + support.appendChecked = input.checked; + + // WebKit doesn't clone checked state correctly in fragments + support.checkClone = fragment.cloneNode( true ).cloneNode( true ).lastChild.checked; + + // Support: IE<9 + // Opera does not clone events (and typeof div.attachEvent === undefined). + // IE9-10 clones events bound via attachEvent, but they don't trigger with .click() + if ( div.attachEvent ) { + div.attachEvent( "onclick", function() { + support.noCloneEvent = false; + }); + + div.cloneNode( true ).click(); + } + + // Support: IE<9 (lack submit/change bubble), Firefox 17+ (lack focusin event) + // Beware of CSP restrictions (https://developer.mozilla.org/en/Security/CSP) + for ( i in { submit: true, change: true, focusin: true }) { + div.setAttribute( eventName = "on" + i, "t" ); + + support[ i + "Bubbles" ] = eventName in window || div.attributes[ eventName ].expando === false; + } + + div.style.backgroundClip = "content-box"; + div.cloneNode( true ).style.backgroundClip = ""; + support.clearCloneStyle = div.style.backgroundClip === "content-box"; + + // Support: IE<9 + // Iteration over object's inherited properties before its own. + for ( i in jQuery( support ) ) { + break; + } + support.ownLast = i !== "0"; + + // Run tests that need a body at doc ready + jQuery(function() { + var container, marginDiv, tds, + divReset = "padding:0;margin:0;border:0;display:block;box-sizing:content-box;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;", + body = document.getElementsByTagName("body")[0]; + + if ( !body ) { + // Return for frameset docs that don't have a body + return; + } + + container = document.createElement("div"); + container.style.cssText = "border:0;width:0;height:0;position:absolute;top:0;left:-9999px;margin-top:1px"; + + body.appendChild( container ).appendChild( div ); + + // Support: IE8 + // Check if table cells still have offsetWidth/Height when they are set + // to display:none and there are still other visible table cells in a + // table row; if so, offsetWidth/Height are not reliable for use when + // determining if an element has been hidden directly using + // display:none (it is still safe to use offsets if a parent element is + // hidden; don safety goggles and see bug #4512 for more information). + div.innerHTML = "
    t
    "; + tds = div.getElementsByTagName("td"); + tds[ 0 ].style.cssText = "padding:0;margin:0;border:0;display:none"; + isSupported = ( tds[ 0 ].offsetHeight === 0 ); + + tds[ 0 ].style.display = ""; + tds[ 1 ].style.display = "none"; + + // Support: IE8 + // Check if empty table cells still have offsetWidth/Height + support.reliableHiddenOffsets = isSupported && ( tds[ 0 ].offsetHeight === 0 ); + + // Check box-sizing and margin behavior. + div.innerHTML = ""; + div.style.cssText = "box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;"; + + // Workaround failing boxSizing test due to offsetWidth returning wrong value + // with some non-1 values of body zoom, ticket #13543 + jQuery.swap( body, body.style.zoom != null ? { zoom: 1 } : {}, function() { + support.boxSizing = div.offsetWidth === 4; + }); + + // Use window.getComputedStyle because jsdom on node.js will break without it. + if ( window.getComputedStyle ) { + support.pixelPosition = ( window.getComputedStyle( div, null ) || {} ).top !== "1%"; + support.boxSizingReliable = ( window.getComputedStyle( div, null ) || { width: "4px" } ).width === "4px"; + + // Check if div with explicit width and no margin-right incorrectly + // gets computed margin-right based on width of container. (#3333) + // Fails in WebKit before Feb 2011 nightlies + // WebKit Bug 13343 - getComputedStyle returns wrong value for margin-right + marginDiv = div.appendChild( document.createElement("div") ); + marginDiv.style.cssText = div.style.cssText = divReset; + marginDiv.style.marginRight = marginDiv.style.width = "0"; + div.style.width = "1px"; + + support.reliableMarginRight = + !parseFloat( ( window.getComputedStyle( marginDiv, null ) || {} ).marginRight ); + } + + if ( typeof div.style.zoom !== core_strundefined ) { + // Support: IE<8 + // Check if natively block-level elements act like inline-block + // elements when setting their display to 'inline' and giving + // them layout + div.innerHTML = ""; + div.style.cssText = divReset + "width:1px;padding:1px;display:inline;zoom:1"; + support.inlineBlockNeedsLayout = ( div.offsetWidth === 3 ); + + // Support: IE6 + // Check if elements with layout shrink-wrap their children + div.style.display = "block"; + div.innerHTML = "
    "; + div.firstChild.style.width = "5px"; + support.shrinkWrapBlocks = ( div.offsetWidth !== 3 ); + + if ( support.inlineBlockNeedsLayout ) { + // Prevent IE 6 from affecting layout for positioned elements #11048 + // Prevent IE from shrinking the body in IE 7 mode #12869 + // Support: IE<8 + body.style.zoom = 1; + } + } + + body.removeChild( container ); + + // Null elements to avoid leaks in IE + container = div = tds = marginDiv = null; + }); + + // Null elements to avoid leaks in IE + all = select = fragment = opt = a = input = null; + + return support; +})({}); + +var rbrace = /(?:\{[\s\S]*\}|\[[\s\S]*\])$/, + rmultiDash = /([A-Z])/g; + +function internalData( elem, name, data, pvt /* Internal Use Only */ ){ + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var ret, thisCache, + internalKey = jQuery.expando, + + // We have to handle DOM nodes and JS objects differently because IE6-7 + // can't GC object references properly across the DOM-JS boundary + isNode = elem.nodeType, + + // Only DOM nodes need the global jQuery cache; JS object data is + // attached directly to the object so GC can occur automatically + cache = isNode ? jQuery.cache : elem, + + // Only defining an ID for JS objects if its cache already exists allows + // the code to shortcut on the same path as a DOM node with no cache + id = isNode ? elem[ internalKey ] : elem[ internalKey ] && internalKey; + + // Avoid doing any more work than we need to when trying to get data on an + // object that has no data at all + if ( (!id || !cache[id] || (!pvt && !cache[id].data)) && data === undefined && typeof name === "string" ) { + return; + } + + if ( !id ) { + // Only DOM nodes need a new unique ID for each element since their data + // ends up in the global cache + if ( isNode ) { + id = elem[ internalKey ] = core_deletedIds.pop() || jQuery.guid++; + } else { + id = internalKey; + } + } + + if ( !cache[ id ] ) { + // Avoid exposing jQuery metadata on plain JS objects when the object + // is serialized using JSON.stringify + cache[ id ] = isNode ? {} : { toJSON: jQuery.noop }; + } + + // An object can be passed to jQuery.data instead of a key/value pair; this gets + // shallow copied over onto the existing cache + if ( typeof name === "object" || typeof name === "function" ) { + if ( pvt ) { + cache[ id ] = jQuery.extend( cache[ id ], name ); + } else { + cache[ id ].data = jQuery.extend( cache[ id ].data, name ); + } + } + + thisCache = cache[ id ]; + + // jQuery data() is stored in a separate object inside the object's internal data + // cache in order to avoid key collisions between internal data and user-defined + // data. + if ( !pvt ) { + if ( !thisCache.data ) { + thisCache.data = {}; + } + + thisCache = thisCache.data; + } + + if ( data !== undefined ) { + thisCache[ jQuery.camelCase( name ) ] = data; + } + + // Check for both converted-to-camel and non-converted data property names + // If a data property was specified + if ( typeof name === "string" ) { + + // First Try to find as-is property data + ret = thisCache[ name ]; + + // Test for null|undefined property data + if ( ret == null ) { + + // Try to find the camelCased property + ret = thisCache[ jQuery.camelCase( name ) ]; + } + } else { + ret = thisCache; + } + + return ret; +} + +function internalRemoveData( elem, name, pvt ) { + if ( !jQuery.acceptData( elem ) ) { + return; + } + + var thisCache, i, + isNode = elem.nodeType, + + // See jQuery.data for more information + cache = isNode ? jQuery.cache : elem, + id = isNode ? elem[ jQuery.expando ] : jQuery.expando; + + // If there is already no cache entry for this object, there is no + // purpose in continuing + if ( !cache[ id ] ) { + return; + } + + if ( name ) { + + thisCache = pvt ? cache[ id ] : cache[ id ].data; + + if ( thisCache ) { + + // Support array or space separated string names for data keys + if ( !jQuery.isArray( name ) ) { + + // try the string as a key before any manipulation + if ( name in thisCache ) { + name = [ name ]; + } else { + + // split the camel cased version by spaces unless a key with the spaces exists + name = jQuery.camelCase( name ); + if ( name in thisCache ) { + name = [ name ]; + } else { + name = name.split(" "); + } + } + } else { + // 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 = name.concat( jQuery.map( name, jQuery.camelCase ) ); + } + + i = name.length; + while ( i-- ) { + delete thisCache[ name[i] ]; + } + + // If there is no data left in the cache, we want to continue + // and let the cache object itself get destroyed + if ( pvt ? !isEmptyDataObject(thisCache) : !jQuery.isEmptyObject(thisCache) ) { + return; + } + } + } + + // See jQuery.data for more information + if ( !pvt ) { + delete cache[ id ].data; + + // Don't destroy the parent cache unless the internal data object + // had been the only thing left in it + if ( !isEmptyDataObject( cache[ id ] ) ) { + return; + } + } + + // Destroy the cache + if ( isNode ) { + jQuery.cleanData( [ elem ], true ); + + // Use delete when supported for expandos or `cache` is not a window per isWindow (#10080) + /* jshint eqeqeq: false */ + } else if ( jQuery.support.deleteExpando || cache != cache.window ) { + /* jshint eqeqeq: true */ + delete cache[ id ]; + + // When all else fails, null + } else { + cache[ id ] = null; + } +} + +jQuery.extend({ + cache: {}, + + // The following elements throw uncatchable exceptions if you + // attempt to add expando properties to them. + noData: { + "applet": true, + "embed": true, + // Ban all objects except for Flash (which handle expandos) + "object": "clsid:D27CDB6E-AE6D-11cf-96B8-444553540000" + }, + + hasData: function( elem ) { + elem = elem.nodeType ? jQuery.cache[ elem[jQuery.expando] ] : elem[ jQuery.expando ]; + return !!elem && !isEmptyDataObject( elem ); + }, + + data: function( elem, name, data ) { + return internalData( elem, name, data ); + }, + + removeData: function( elem, name ) { + return internalRemoveData( elem, name ); + }, + + // For internal use only. + _data: function( elem, name, data ) { + return internalData( elem, name, data, true ); + }, + + _removeData: function( elem, name ) { + return internalRemoveData( elem, name, true ); + }, + + // A method for determining if a DOM node can handle the data expando + acceptData: function( elem ) { + // Do not set data on non-element because it will not be cleared (#8335). + if ( elem.nodeType && elem.nodeType !== 1 && elem.nodeType !== 9 ) { + return false; + } + + var noData = elem.nodeName && jQuery.noData[ elem.nodeName.toLowerCase() ]; + + // nodes accept data unless otherwise specified; rejection can be conditional + return !noData || noData !== true && elem.getAttribute("classid") === noData; + } +}); + +jQuery.fn.extend({ + data: function( key, value ) { + var attrs, name, + data = null, + i = 0, + elem = this[0]; + + // Special expections of .data basically thwart jQuery.access, + // so implement the relevant behavior ourselves + + // Gets all values + if ( key === undefined ) { + if ( this.length ) { + data = jQuery.data( elem ); + + if ( elem.nodeType === 1 && !jQuery._data( elem, "parsedAttrs" ) ) { + attrs = elem.attributes; + for ( ; i < attrs.length; i++ ) { + name = attrs[i].name; + + if ( name.indexOf("data-") === 0 ) { + name = jQuery.camelCase( name.slice(5) ); + + dataAttr( elem, name, data[ name ] ); + } + } + jQuery._data( elem, "parsedAttrs", true ); + } + } + + return data; + } + + // Sets multiple values + if ( typeof key === "object" ) { + return this.each(function() { + jQuery.data( this, key ); + }); + } + + return arguments.length > 1 ? + + // Sets one value + this.each(function() { + jQuery.data( this, key, value ); + }) : + + // Gets one value + // Try to fetch any internally stored data first + elem ? dataAttr( elem, key, jQuery.data( elem, key ) ) : null; + }, + + removeData: function( key ) { + return this.each(function() { + jQuery.removeData( this, key ); + }); + } +}); + +function dataAttr( elem, key, data ) { + // If nothing was found internally, try to fetch any + // data from the HTML5 data-* attribute + if ( data === undefined && elem.nodeType === 1 ) { + + var 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 + jQuery.data( elem, key, data ); + + } else { + data = undefined; + } + } + + return data; +} + +// checks a cache object for emptiness +function isEmptyDataObject( obj ) { + var name; + for ( name in obj ) { + + // if the public data object is empty, the private is still empty + if ( name === "data" && jQuery.isEmptyObject( obj[name] ) ) { + continue; + } + if ( name !== "toJSON" ) { + return false; + } + } + + return true; +} +jQuery.extend({ + queue: function( elem, type, data ) { + var queue; + + if ( elem ) { + type = ( type || "fx" ) + "queue"; + queue = jQuery._data( elem, type ); + + // Speed up dequeue by getting out quickly if this is just a lookup + if ( data ) { + if ( !queue || jQuery.isArray(data) ) { + queue = jQuery._data( 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 jQuery._data( elem, key ) || jQuery._data( elem, key, { + empty: jQuery.Callbacks("once memory").add(function() { + jQuery._removeData( elem, type + "queue" ); + jQuery._removeData( elem, 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 ); + }); + }, + // Based off of the plugin by Clint Helfers, with permission. + // http://blindsignals.com/index.php/2009/07/jquery-delay/ + delay: function( time, type ) { + time = jQuery.fx ? jQuery.fx.speeds[ time ] || time : time; + type = type || "fx"; + + return this.queue( type, function( next, hooks ) { + var timeout = setTimeout( next, time ); + hooks.stop = function() { + clearTimeout( timeout ); + }; + }); + }, + 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 = jQuery._data( elements[ i ], type + "queueHooks" ); + if ( tmp && tmp.empty ) { + count++; + tmp.empty.add( resolve ); + } + } + resolve(); + return defer.promise( obj ); + } +}); +var nodeHook, boolHook, + rclass = /[\t\r\n\f]/g, + rreturn = /\r/g, + rfocusable = /^(?:input|select|textarea|button|object)$/i, + rclickable = /^(?:a|area)$/i, + ruseDefault = /^(?:checked|selected)$/i, + getSetAttribute = jQuery.support.getSetAttribute, + getSetInput = jQuery.support.input; + +jQuery.fn.extend({ + attr: function( name, value ) { + return jQuery.access( this, jQuery.attr, name, value, arguments.length > 1 ); + }, + + removeAttr: function( name ) { + return this.each(function() { + jQuery.removeAttr( this, name ); + }); + }, + + prop: function( name, value ) { + return jQuery.access( this, jQuery.prop, name, value, arguments.length > 1 ); + }, + + removeProp: function( name ) { + name = jQuery.propFix[ name ] || name; + return this.each(function() { + // try/catch handles cases where IE balks (such as removing a property on window) + try { + this[ name ] = undefined; + delete this[ name ]; + } catch( e ) {} + }); + }, + + addClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).addClass( value.call( this, j, this.className ) ); + }); + } + + if ( proceed ) { + // The disjunction here is for better compressibility (see removeClass) + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + " " + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + if ( cur.indexOf( " " + clazz + " " ) < 0 ) { + cur += clazz + " "; + } + } + elem.className = jQuery.trim( cur ); + + } + } + } + + return this; + }, + + removeClass: function( value ) { + var classes, elem, cur, clazz, j, + i = 0, + len = this.length, + proceed = arguments.length === 0 || typeof value === "string" && value; + + if ( jQuery.isFunction( value ) ) { + return this.each(function( j ) { + jQuery( this ).removeClass( value.call( this, j, this.className ) ); + }); + } + if ( proceed ) { + classes = ( value || "" ).match( core_rnotwhite ) || []; + + for ( ; i < len; i++ ) { + elem = this[ i ]; + // This expression is here for better compressibility (see addClass) + cur = elem.nodeType === 1 && ( elem.className ? + ( " " + elem.className + " " ).replace( rclass, " " ) : + "" + ); + + if ( cur ) { + j = 0; + while ( (clazz = classes[j++]) ) { + // Remove *all* instances + while ( cur.indexOf( " " + clazz + " " ) >= 0 ) { + cur = cur.replace( " " + clazz + " ", " " ); + } + } + elem.className = value ? jQuery.trim( cur ) : ""; + } + } + } + + return this; + }, + + toggleClass: function( value, stateVal ) { + var type = typeof value; + + if ( typeof stateVal === "boolean" && type === "string" ) { + return stateVal ? this.addClass( value ) : this.removeClass( value ); + } + + if ( jQuery.isFunction( value ) ) { + return this.each(function( i ) { + jQuery( this ).toggleClass( value.call(this, i, this.className, stateVal), stateVal ); + }); + } + + return this.each(function() { + if ( type === "string" ) { + // toggle individual class names + var className, + i = 0, + self = jQuery( this ), + classNames = value.match( core_rnotwhite ) || []; + + while ( (className = classNames[ i++ ]) ) { + // check each className given, space separated list + if ( self.hasClass( className ) ) { + self.removeClass( className ); + } else { + self.addClass( className ); + } + } + + // Toggle whole class name + } else if ( type === core_strundefined || type === "boolean" ) { + if ( this.className ) { + // store className if set + jQuery._data( this, "__className__", this.className ); + } + + // If the element has a class name or if we're passed "false", + // then remove the whole classname (if there was one, the above saved it). + // Otherwise bring back whatever was previously saved (if anything), + // falling back to the empty string if nothing was stored. + this.className = this.className || value === false ? "" : jQuery._data( this, "__className__" ) || ""; + } + }); + }, + + hasClass: function( selector ) { + var className = " " + selector + " ", + i = 0, + l = this.length; + for ( ; i < l; i++ ) { + if ( this[i].nodeType === 1 && (" " + this[i].className + " ").replace(rclass, " ").indexOf( className ) >= 0 ) { + return true; + } + } + + return false; + }, + + val: function( value ) { + var ret, hooks, isFunction, + elem = this[0]; + + if ( !arguments.length ) { + if ( elem ) { + hooks = jQuery.valHooks[ elem.type ] || jQuery.valHooks[ elem.nodeName.toLowerCase() ]; + + if ( hooks && "get" in hooks && (ret = hooks.get( elem, "value" )) !== undefined ) { + return ret; + } + + ret = elem.value; + + return typeof ret === "string" ? + // handle most common string cases + ret.replace(rreturn, "") : + // handle cases where value is null/undef or number + ret == null ? "" : ret; + } + + return; + } + + isFunction = jQuery.isFunction( value ); + + return this.each(function( i ) { + var val; + + if ( this.nodeType !== 1 ) { + return; + } + + if ( isFunction ) { + val = value.call( this, i, jQuery( this ).val() ); + } else { + val = value; + } + + // Treat null/undefined as ""; convert numbers to string + if ( val == null ) { + val = ""; + } else if ( typeof val === "number" ) { + val += ""; + } else if ( jQuery.isArray( val ) ) { + val = jQuery.map(val, function ( value ) { + return value == null ? "" : value + ""; + }); + } + + hooks = jQuery.valHooks[ this.type ] || jQuery.valHooks[ this.nodeName.toLowerCase() ]; + + // If set returns undefined, fall back to normal setting + if ( !hooks || !("set" in hooks) || hooks.set( this, val, "value" ) === undefined ) { + this.value = val; + } + }); + } +}); + +jQuery.extend({ + valHooks: { + option: { + get: function( elem ) { + // Use proper attribute retrieval(#6932, #12072) + var val = jQuery.find.attr( elem, "value" ); + return val != null ? + val : + elem.text; + } + }, + select: { + get: function( elem ) { + var value, option, + options = elem.options, + index = elem.selectedIndex, + one = elem.type === "select-one" || index < 0, + values = one ? null : [], + max = one ? index + 1 : options.length, + i = index < 0 ? + max : + one ? index : 0; + + // Loop through all the selected options + for ( ; i < max; i++ ) { + option = options[ i ]; + + // oldIE doesn't update selected after form reset (#2551) + if ( ( option.selected || i === index ) && + // Don't return options that are disabled or in a disabled optgroup + ( jQuery.support.optDisabled ? !option.disabled : option.getAttribute("disabled") === null ) && + ( !option.parentNode.disabled || !jQuery.nodeName( option.parentNode, "optgroup" ) ) ) { + + // Get the specific value for the option + value = jQuery( option ).val(); + + // We don't need an array for one selects + if ( one ) { + return value; + } + + // Multi-Selects return an array + values.push( value ); + } + } + + return values; + }, + + set: function( elem, value ) { + var optionSet, option, + options = elem.options, + values = jQuery.makeArray( value ), + i = options.length; + + while ( i-- ) { + option = options[ i ]; + if ( (option.selected = jQuery.inArray( jQuery(option).val(), values ) >= 0) ) { + optionSet = true; + } + } + + // force browsers to behave consistently when non-matching value is set + if ( !optionSet ) { + elem.selectedIndex = -1; + } + return values; + } + } + }, + + attr: function( elem, name, value ) { + var hooks, ret, + nType = elem.nodeType; + + // don't get/set attributes on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + // Fallback to prop when attributes are not supported + if ( typeof elem.getAttribute === core_strundefined ) { + return jQuery.prop( elem, name, value ); + } + + // All attributes are lowercase + // Grab necessary hook if one is defined + if ( nType !== 1 || !jQuery.isXMLDoc( elem ) ) { + name = name.toLowerCase(); + hooks = jQuery.attrHooks[ name ] || + ( jQuery.expr.match.bool.test( name ) ? boolHook : nodeHook ); + } + + if ( value !== undefined ) { + + if ( value === null ) { + jQuery.removeAttr( elem, name ); + + } else if ( hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ) { + return ret; + + } else { + elem.setAttribute( name, value + "" ); + return value; + } + + } else if ( hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ) { + return ret; + + } else { + ret = jQuery.find.attr( elem, name ); + + // Non-existent attributes return null, we normalize to undefined + return ret == null ? + undefined : + ret; + } + }, + + removeAttr: function( elem, value ) { + var name, propName, + i = 0, + attrNames = value && value.match( core_rnotwhite ); + + if ( attrNames && elem.nodeType === 1 ) { + while ( (name = attrNames[i++]) ) { + propName = jQuery.propFix[ name ] || name; + + // Boolean attributes get special treatment (#10870) + if ( jQuery.expr.match.bool.test( name ) ) { + // Set corresponding property to false + if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + elem[ propName ] = false; + // Support: IE<9 + // Also clear defaultChecked/defaultSelected (if appropriate) + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = + elem[ propName ] = false; + } + + // See #9699 for explanation of this approach (setting first, then removal) + } else { + jQuery.attr( elem, name, "" ); + } + + elem.removeAttribute( getSetAttribute ? name : propName ); + } + } + }, + + attrHooks: { + type: { + set: function( elem, value ) { + if ( !jQuery.support.radioValue && value === "radio" && jQuery.nodeName(elem, "input") ) { + // Setting the type on a radio button after the value resets the value in IE6-9 + // Reset value to default in case type is set after value during creation + var val = elem.value; + elem.setAttribute( "type", value ); + if ( val ) { + elem.value = val; + } + return value; + } + } + } + }, + + propFix: { + "for": "htmlFor", + "class": "className" + }, + + prop: function( elem, name, value ) { + var ret, hooks, notxml, + nType = elem.nodeType; + + // don't get/set properties on text, comment and attribute nodes + if ( !elem || nType === 3 || nType === 8 || nType === 2 ) { + return; + } + + notxml = nType !== 1 || !jQuery.isXMLDoc( elem ); + + if ( notxml ) { + // Fix name and attach hooks + name = jQuery.propFix[ name ] || name; + hooks = jQuery.propHooks[ name ]; + } + + if ( value !== undefined ) { + return hooks && "set" in hooks && (ret = hooks.set( elem, value, name )) !== undefined ? + ret : + ( elem[ name ] = value ); + + } else { + return hooks && "get" in hooks && (ret = hooks.get( elem, name )) !== null ? + ret : + elem[ name ]; + } + }, + + propHooks: { + tabIndex: { + get: function( elem ) { + // elem.tabIndex doesn't always return the correct value when it hasn't been explicitly set + // http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/ + // Use proper attribute retrieval(#12072) + var tabindex = jQuery.find.attr( elem, "tabindex" ); + + return tabindex ? + parseInt( tabindex, 10 ) : + rfocusable.test( elem.nodeName ) || rclickable.test( elem.nodeName ) && elem.href ? + 0 : + -1; + } + } + } +}); + +// Hooks for boolean attributes +boolHook = { + set: function( elem, value, name ) { + if ( value === false ) { + // Remove boolean attributes when set to false + jQuery.removeAttr( elem, name ); + } else if ( getSetInput && getSetAttribute || !ruseDefault.test( name ) ) { + // IE<8 needs the *property* name + elem.setAttribute( !getSetAttribute && jQuery.propFix[ name ] || name, name ); + + // Use defaultChecked and defaultSelected for oldIE + } else { + elem[ jQuery.camelCase( "default-" + name ) ] = elem[ name ] = true; + } + + return name; + } +}; +jQuery.each( jQuery.expr.match.bool.source.match( /\w+/g ), function( i, name ) { + var getter = jQuery.expr.attrHandle[ name ] || jQuery.find.attr; + + jQuery.expr.attrHandle[ name ] = getSetInput && getSetAttribute || !ruseDefault.test( name ) ? + function( elem, name, isXML ) { + var fn = jQuery.expr.attrHandle[ name ], + ret = isXML ? + undefined : + /* jshint eqeqeq: false */ + (jQuery.expr.attrHandle[ name ] = undefined) != + getter( elem, name, isXML ) ? + + name.toLowerCase() : + null; + jQuery.expr.attrHandle[ name ] = fn; + return ret; + } : + function( elem, name, isXML ) { + return isXML ? + undefined : + elem[ jQuery.camelCase( "default-" + name ) ] ? + name.toLowerCase() : + null; + }; +}); + +// fix oldIE attroperties +if ( !getSetInput || !getSetAttribute ) { + jQuery.attrHooks.value = { + set: function( elem, value, name ) { + if ( jQuery.nodeName( elem, "input" ) ) { + // Does not return so that setAttribute is also used + elem.defaultValue = value; + } else { + // Use nodeHook if defined (#1954); otherwise setAttribute is fine + return nodeHook && nodeHook.set( elem, value, name ); + } + } + }; +} + +// IE6/7 do not support getting/setting some attributes with get/setAttribute +if ( !getSetAttribute ) { + + // Use this for any attribute in IE6/7 + // This fixes almost every IE6/7 issue + nodeHook = { + set: function( elem, value, name ) { + // Set the existing or create a new attribute node + var ret = elem.getAttributeNode( name ); + if ( !ret ) { + elem.setAttributeNode( + (ret = elem.ownerDocument.createAttribute( name )) + ); + } + + ret.value = value += ""; + + // Break association with cloned elements by also using setAttribute (#9646) + return name === "value" || value === elem.getAttribute( name ) ? + value : + undefined; + } + }; + jQuery.expr.attrHandle.id = jQuery.expr.attrHandle.name = jQuery.expr.attrHandle.coords = + // Some attributes are constructed with empty-string values when not defined + function( elem, name, isXML ) { + var ret; + return isXML ? + undefined : + (ret = elem.getAttributeNode( name )) && ret.value !== "" ? + ret.value : + null; + }; + jQuery.valHooks.button = { + get: function( elem, name ) { + var ret = elem.getAttributeNode( name ); + return ret && ret.specified ? + ret.value : + undefined; + }, + set: nodeHook.set + }; + + // Set contenteditable to false on removals(#10429) + // Setting to empty string throws an error as an invalid value + jQuery.attrHooks.contenteditable = { + set: function( elem, value, name ) { + nodeHook.set( elem, value === "" ? false : value, name ); + } + }; + + // Set width and height to auto instead of 0 on empty string( Bug #8150 ) + // This is for removals + jQuery.each([ "width", "height" ], function( i, name ) { + jQuery.attrHooks[ name ] = { + set: function( elem, value ) { + if ( value === "" ) { + elem.setAttribute( name, "auto" ); + return value; + } + } + }; + }); +} + + +// Some attributes require a special call on IE +// http://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx +if ( !jQuery.support.hrefNormalized ) { + // href/src property should get the full normalized URL (#10299/#12915) + jQuery.each([ "href", "src" ], function( i, name ) { + jQuery.propHooks[ name ] = { + get: function( elem ) { + return elem.getAttribute( name, 4 ); + } + }; + }); +} + +if ( !jQuery.support.style ) { + jQuery.attrHooks.style = { + get: function( elem ) { + // Return undefined in the case of empty string + // Note: IE uppercases css property names, but if we were to .toLowerCase() + // .cssText, that would destroy case senstitivity in URL's, like in "background" + return elem.style.cssText || undefined; + }, + set: function( elem, value ) { + return ( elem.style.cssText = value + "" ); + } + }; +} + +// Safari mis-reports the default selected property of an option +// Accessing the parent's selectedIndex property fixes it +if ( !jQuery.support.optSelected ) { + jQuery.propHooks.selected = { + get: function( elem ) { + var parent = elem.parentNode; + + if ( parent ) { + parent.selectedIndex; + + // Make sure that it also works with optgroups, see #5701 + if ( parent.parentNode ) { + parent.parentNode.selectedIndex; + } + } + return null; + } + }; +} + +jQuery.each([ + "tabIndex", + "readOnly", + "maxLength", + "cellSpacing", + "cellPadding", + "rowSpan", + "colSpan", + "useMap", + "frameBorder", + "contentEditable" +], function() { + jQuery.propFix[ this.toLowerCase() ] = this; +}); + +// IE6/7 call enctype encoding +if ( !jQuery.support.enctype ) { + jQuery.propFix.enctype = "encoding"; +} + +// Radios and checkboxes getter/setter +jQuery.each([ "radio", "checkbox" ], function() { + jQuery.valHooks[ this ] = { + set: function( elem, value ) { + if ( jQuery.isArray( value ) ) { + return ( elem.checked = jQuery.inArray( jQuery(elem).val(), value ) >= 0 ); + } + } + }; + if ( !jQuery.support.checkOn ) { + jQuery.valHooks[ this ].get = function( elem ) { + // Support: Webkit + // "" is returned instead of "on" if a value isn't specified + return elem.getAttribute("value") === null ? "on" : elem.value; + }; + } +}); +var rformElems = /^(?:input|select|textarea)$/i, + rkeyEvent = /^key/, + rmouseEvent = /^(?:mouse|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 tmp, events, t, handleObjIn, + special, eventHandle, handleObj, + handlers, type, namespaces, origType, + elemData = jQuery._data( 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 !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? + jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : + undefined; + }; + // Add elem as a property of the handle fn to prevent a memory leak with IE non-native events + eventHandle.elem = elem; + } + + // Handle multiple events separated by a space + types = ( types || "" ).match( core_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/attachEvent if the special events handler returns false + if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { + // Bind the global event handler to the element + if ( elem.addEventListener ) { + elem.addEventListener( type, eventHandle, false ); + + } else if ( elem.attachEvent ) { + elem.attachEvent( "on" + type, eventHandle ); + } + } + } + + 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; + } + + // Nullify elem to prevent memory leaks in IE + elem = null; + }, + + // Detach an event or set of events from an element + remove: function( elem, types, handler, selector, mappedTypes ) { + var j, handleObj, tmp, + origCount, t, events, + special, handlers, type, + namespaces, origType, + elemData = jQuery.hasData( elem ) && jQuery._data( elem ); + + if ( !elemData || !(events = elemData.events) ) { + return; + } + + // Once for each type.namespace in types; type may be omitted + types = ( types || "" ).match( core_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; + + // removeData also checks for emptiness and clears the expando if empty + // so use it instead of delete + jQuery._removeData( elem, "events" ); + } + }, + + trigger: function( event, data, elem, onlyHandlers ) { + var handle, ontype, cur, + bubbleType, special, tmp, i, + eventPath = [ elem || document ], + type = core_hasOwn.call( event, "type" ) ? event.type : event, + namespaces = core_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 = ( jQuery._data( cur, "events" ) || {} )[ event.type ] && jQuery._data( cur, "handle" ); + if ( handle ) { + handle.apply( cur, data ); + } + + // Native handler + handle = ontype && cur[ ontype ]; + if ( handle && jQuery.acceptData( cur ) && handle.apply && handle.apply( cur, data ) === 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. + // Can't use an .isFunction() check here because IE6/7 fails that test. + // Don't do default actions on window, that's where global variables be (#6170) + if ( ontype && 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; + try { + elem[ type ](); + } catch ( e ) { + // IE<9 dies on focus/blur to hidden element (#1486,#12518) + // only reproducible on winXP IE8 native, not IE9 in IE8 mode + } + 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, ret, handleObj, matched, j, + handlerQueue = [], + args = core_slice.call( arguments ), + handlers = ( jQuery._data( 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 sel, handleObj, matches, i, + 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") ) { + + /* jshint eqeqeq: false */ + for ( ; cur != this; cur = cur.parentNode || this ) { + /* jshint eqeqeq: true */ + + // Don't check non-elements (#13208) + // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) + if ( cur.nodeType === 1 && (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; + }, + + 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: IE<9 + // Fix target property (#1925) + if ( !event.target ) { + event.target = originalEvent.srcElement || document; + } + + // Support: Chrome 23+, Safari? + // Target should not be a text node (#504, #13143) + if ( event.target.nodeType === 3 ) { + event.target = event.target.parentNode; + } + + // Support: IE<9 + // For mouse/key events, metaKey==false if it's undefined (#3368, #11328) + event.metaKey = !!event.metaKey; + + return fixHook.filter ? fixHook.filter( event, originalEvent ) : event; + }, + + // 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 fromElement offsetX offsetY pageX pageY screenX screenY toElement".split(" "), + filter: function( event, original ) { + var body, eventDoc, doc, + button = original.button, + fromElement = original.fromElement; + + // 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 relatedTarget, if necessary + if ( !event.relatedTarget && fromElement ) { + event.relatedTarget = fromElement === event.target ? original.toElement : fromElement; + } + + // 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; + } + }, + + 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 ) { + try { + this.focus(); + return false; + } catch ( e ) { + // Support: IE<9 + // If we error on focus to hidden element (#1486, #12518), + // let .trigger() run the handlers + } + } + }, + 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 ( jQuery.nodeName( this, "input" ) && this.type === "checkbox" && this.click ) { + 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 ) { + + // Even when returnValue equals to undefined Firefox will still show alert + if ( event.result !== undefined ) { + 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 = document.removeEventListener ? + function( elem, type, handle ) { + if ( elem.removeEventListener ) { + elem.removeEventListener( type, handle, false ); + } + } : + function( elem, type, handle ) { + var name = "on" + type; + + if ( elem.detachEvent ) { + + // #8545, #7054, preventing memory leaks for custom events in IE6-8 + // detachEvent needed property on element, by name of that event, to properly expose it to GC + if ( typeof elem[ name ] === core_strundefined ) { + elem[ name ] = null; + } + + elem.detachEvent( name, handle ); + } + }; + +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.returnValue === false || + src.getPreventDefault && src.getPreventDefault() ) ? 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 ) { + return; + } + + // If preventDefault exists, run it on the original event + if ( e.preventDefault ) { + e.preventDefault(); + + // Support: IE + // Otherwise set the returnValue property of the original event to false + } else { + e.returnValue = false; + } + }, + stopPropagation: function() { + var e = this.originalEvent; + + this.isPropagationStopped = returnTrue; + if ( !e ) { + return; + } + // If stopPropagation exists, run it on the original event + if ( e.stopPropagation ) { + e.stopPropagation(); + } + + // Support: IE + // Set the cancelBubble property of the original event to true + e.cancelBubble = true; + }, + stopImmediatePropagation: function() { + this.isImmediatePropagationStopped = returnTrue; + this.stopPropagation(); + } +}; + +// Create mouseenter/leave events using mouseover/out and event-time checks +jQuery.each({ + mouseenter: "mouseover", + mouseleave: "mouseout" +}, 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; + } + }; +}); + +// IE submit delegation +if ( !jQuery.support.submitBubbles ) { + + jQuery.event.special.submit = { + setup: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Lazy-add a submit handler when a descendant form may potentially be submitted + jQuery.event.add( this, "click._submit keypress._submit", function( e ) { + // Node name check avoids a VML-related crash in IE (#9807) + var elem = e.target, + form = jQuery.nodeName( elem, "input" ) || jQuery.nodeName( elem, "button" ) ? elem.form : undefined; + if ( form && !jQuery._data( form, "submitBubbles" ) ) { + jQuery.event.add( form, "submit._submit", function( event ) { + event._submit_bubble = true; + }); + jQuery._data( form, "submitBubbles", true ); + } + }); + // return undefined since we don't need an event listener + }, + + postDispatch: function( event ) { + // If form was submitted by the user, bubble the event up the tree + if ( event._submit_bubble ) { + delete event._submit_bubble; + if ( this.parentNode && !event.isTrigger ) { + jQuery.event.simulate( "submit", this.parentNode, event, true ); + } + } + }, + + teardown: function() { + // Only need this for delegated form submit events + if ( jQuery.nodeName( this, "form" ) ) { + return false; + } + + // Remove delegated handlers; cleanData eventually reaps submit handlers attached above + jQuery.event.remove( this, "._submit" ); + } + }; +} + +// IE change delegation and checkbox/radio fix +if ( !jQuery.support.changeBubbles ) { + + jQuery.event.special.change = { + + setup: function() { + + if ( rformElems.test( this.nodeName ) ) { + // IE doesn't fire change on a check/radio until blur; trigger it on click + // after a propertychange. Eat the blur-change in special.change.handle. + // This still fires onchange a second time for check/radio after blur. + if ( this.type === "checkbox" || this.type === "radio" ) { + jQuery.event.add( this, "propertychange._change", function( event ) { + if ( event.originalEvent.propertyName === "checked" ) { + this._just_changed = true; + } + }); + jQuery.event.add( this, "click._change", function( event ) { + if ( this._just_changed && !event.isTrigger ) { + this._just_changed = false; + } + // Allow triggered, simulated change events (#11500) + jQuery.event.simulate( "change", this, event, true ); + }); + } + return false; + } + // Delegated event; lazy-add a change handler on descendant inputs + jQuery.event.add( this, "beforeactivate._change", function( e ) { + var elem = e.target; + + if ( rformElems.test( elem.nodeName ) && !jQuery._data( elem, "changeBubbles" ) ) { + jQuery.event.add( elem, "change._change", function( event ) { + if ( this.parentNode && !event.isSimulated && !event.isTrigger ) { + jQuery.event.simulate( "change", this.parentNode, event, true ); + } + }); + jQuery._data( elem, "changeBubbles", true ); + } + }); + }, + + handle: function( event ) { + var elem = event.target; + + // Swallow native change events from checkbox/radio, we already triggered them above + if ( this !== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) { + return event.handleObj.handler.apply( this, arguments ); + } + }, + + teardown: function() { + jQuery.event.remove( this, "._change" ); + + return !rformElems.test( this.nodeName ); + } + }; +} + +// Create "bubbling" focus and blur events +if ( !jQuery.support.focusinBubbles ) { + jQuery.each({ focus: "focusin", blur: "focusout" }, function( orig, fix ) { + + // Attach a single capturing handler while someone wants focusin/focusout + var attaches = 0, + handler = function( event ) { + jQuery.event.simulate( fix, event.target, jQuery.event.fix( event ), true ); + }; + + jQuery.event.special[ fix ] = { + setup: function() { + if ( attaches++ === 0 ) { + document.addEventListener( orig, handler, true ); + } + }, + teardown: function() { + if ( --attaches === 0 ) { + document.removeEventListener( orig, handler, true ); + } + } + }; + }); +} + +jQuery.fn.extend({ + + on: function( types, selector, data, fn, /*INTERNAL*/ one ) { + var type, origFn; + + // 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 isSimple = /^.[^:#\[\.,]*$/, + rparentsprev = /^(?:parents|prev(?:Until|All))/, + rneedsContext = jQuery.expr.match.needsContext, + // methods guaranteed to produce a unique set when starting from a unique set + guaranteedUnique = { + children: true, + contents: true, + next: true, + prev: true + }; + +jQuery.fn.extend({ + find: function( selector ) { + var i, + ret = [], + self = this, + len = self.length; + + 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; + }, + + has: function( target ) { + var i, + targets = jQuery( target, this ), + len = targets.length; + + return this.filter(function() { + for ( i = 0; i < len; i++ ) { + if ( jQuery.contains( this, targets[i] ) ) { + return true; + } + } + }); + }, + + not: function( selector ) { + return this.pushStack( winnow(this, selector || [], true) ); + }, + + filter: function( selector ) { + return this.pushStack( winnow(this, selector || [], false) ); + }, + + 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; + }, + + closest: function( selectors, context ) { + var cur, + i = 0, + l = this.length, + ret = [], + 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)) ) { + + cur = ret.push( cur ); + break; + } + } + } + + return this.pushStack( ret.length > 1 ? jQuery.unique( ret ) : ret ); + }, + + // 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 jQuery.inArray( this[0], jQuery( elem ) ); + } + + // Locate the position of the desired element + return jQuery.inArray( + // If it receives a jQuery object, the first element is used + elem.jquery ? elem[0] : elem, this ); + }, + + add: function( selector, context ) { + var set = typeof selector === "string" ? + jQuery( selector, context ) : + jQuery.makeArray( selector && selector.nodeType ? [ selector ] : selector ), + all = jQuery.merge( this.get(), set ); + + return this.pushStack( jQuery.unique(all) ); + }, + + addBack: function( selector ) { + return this.add( selector == null ? + this.prevObject : this.prevObject.filter(selector) + ); + } +}); + +function sibling( cur, dir ) { + do { + cur = cur[ dir ]; + } while ( cur && 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 jQuery.nodeName( elem, "iframe" ) ? + elem.contentDocument || elem.contentWindow.document : + jQuery.merge( [], elem.childNodes ); + } +}, function( name, fn ) { + jQuery.fn[ name ] = function( until, selector ) { + var ret = jQuery.map( this, fn, until ); + + if ( name.slice( -5 ) !== "Until" ) { + selector = until; + } + + if ( selector && typeof selector === "string" ) { + ret = jQuery.filter( selector, ret ); + } + + if ( this.length > 1 ) { + // Remove duplicates + if ( !guaranteedUnique[ name ] ) { + ret = jQuery.unique( ret ); + } + + // Reverse order for parents* and prev-derivatives + if ( rparentsprev.test( name ) ) { + ret = ret.reverse(); + } + } + + return this.pushStack( ret ); + }; +}); + +jQuery.extend({ + 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; + })); + }, + + dir: function( elem, dir, until ) { + var matched = [], + cur = elem[ dir ]; + + while ( cur && cur.nodeType !== 9 && (until === undefined || cur.nodeType !== 1 || !jQuery( cur ).is( until )) ) { + if ( cur.nodeType === 1 ) { + matched.push( cur ); + } + cur = cur[dir]; + } + return matched; + }, + + sibling: function( n, elem ) { + var r = []; + + for ( ; n; n = n.nextSibling ) { + if ( n.nodeType === 1 && n !== elem ) { + r.push( n ); + } + } + + return r; + } +}); + +// 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 ( isSimple.test( qualifier ) ) { + return jQuery.filter( qualifier, elements, not ); + } + + qualifier = jQuery.filter( qualifier, elements ); + } + + return jQuery.grep( elements, function( elem ) { + return ( jQuery.inArray( elem, qualifier ) >= 0 ) !== not; + }); +} +function createSafeFragment( document ) { + var list = nodeNames.split( "|" ), + safeFrag = document.createDocumentFragment(); + + if ( safeFrag.createElement ) { + while ( list.length ) { + safeFrag.createElement( + list.pop() + ); + } + } + return safeFrag; +} + +var nodeNames = "abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|" + + "header|hgroup|mark|meter|nav|output|progress|section|summary|time|video", + rinlinejQuery = / jQuery\d+="(?:null|\d+)"/g, + rnoshimcache = new RegExp("<(?:" + nodeNames + ")[\\s/>]", "i"), + rleadingWhitespace = /^\s+/, + rxhtmlTag = /<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi, + rtagName = /<([\w:]+)/, + rtbody = /\s*$/g, + + // We have to close these tags to support XHTML (#13200) + wrapMap = { + option: [ 1, "" ], + legend: [ 1, "
    ", "
    " ], + area: [ 1, "", "" ], + param: [ 1, "", "" ], + thead: [ 1, "", "
    " ], + tr: [ 2, "", "
    " ], + col: [ 2, "", "
    " ], + td: [ 3, "", "
    " ], + + // IE6-8 can't serialize link, script, style, or any html5 (NoScope) tags, + // unless wrapped in a div with non-breaking characters in front of it. + _default: jQuery.support.htmlSerialize ? [ 0, "", "" ] : [ 1, "X
    ", "
    " ] + }, + safeFragment = createSafeFragment( document ), + fragmentDiv = safeFragment.appendChild( document.createElement("div") ); + +wrapMap.optgroup = wrapMap.option; +wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead; +wrapMap.th = wrapMap.td; + +jQuery.fn.extend({ + text: function( value ) { + return jQuery.access( this, function( value ) { + return value === undefined ? + jQuery.text( this ) : + this.empty().append( ( this[0] && this[0].ownerDocument || document ).createTextNode( 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 ); + } + }); + }, + + // keepData is for internal use only--do not document + remove: function( selector, keepData ) { + 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++ ) { + // Remove element nodes and prevent memory leaks + if ( elem.nodeType === 1 ) { + jQuery.cleanData( getAll( elem, false ) ); + } + + // Remove any remaining nodes + while ( elem.firstChild ) { + elem.removeChild( elem.firstChild ); + } + + // If this is a select, ensure that it displays empty (#12336) + // Support: IE<9 + if ( elem.options && jQuery.nodeName( elem, "select" ) ) { + elem.options.length = 0; + } + } + + 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 jQuery.access( this, function( value ) { + var elem = this[0] || {}, + i = 0, + l = this.length; + + if ( value === undefined ) { + return elem.nodeType === 1 ? + elem.innerHTML.replace( rinlinejQuery, "" ) : + undefined; + } + + // See if we can take a shortcut and just use innerHTML + if ( typeof value === "string" && !rnoInnerhtml.test( value ) && + ( jQuery.support.htmlSerialize || !rnoshimcache.test( value ) ) && + ( jQuery.support.leadingWhitespace || !rleadingWhitespace.test( value ) ) && + !wrapMap[ ( rtagName.exec( value ) || ["", ""] )[1].toLowerCase() ] ) { + + value = value.replace( rxhtmlTag, "<$1>" ); + + try { + for (; i < l; i++ ) { + // Remove element nodes and prevent memory leaks + elem = this[i] || {}; + 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 + // Snapshot the DOM in case .domManip sweeps something relevant into its fragment + args = jQuery.map( this, function( elem ) { + return [ elem.nextSibling, elem.parentNode ]; + }), + i = 0; + + // Make the changes, replacing each context element with the new content + this.domManip( arguments, function( elem ) { + var next = args[ i++ ], + parent = args[ i++ ]; + + if ( parent ) { + // Don't use the snapshot next if it has moved (#13810) + if ( next && next.parentNode !== parent ) { + next = this.nextSibling; + } + jQuery( this ).remove(); + parent.insertBefore( elem, next ); + } + // Allow new content to include elements from the context set + }, true ); + + // Force removal if there was no new content (e.g., from empty arguments) + return i ? this : this.remove(); + }, + + detach: function( selector ) { + return this.remove( selector, true ); + }, + + domManip: function( args, callback, allowIntersection ) { + + // Flatten any nested arrays + args = core_concat.apply( [], args ); + + var first, node, hasScripts, + scripts, doc, fragment, + 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" || jQuery.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, allowIntersection ); + }); + } + + if ( l ) { + fragment = jQuery.buildFragment( args, this[ 0 ].ownerDocument, false, !allowIntersection && 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 ) { + 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 || "" ) && + !jQuery._data( node, "globalEval" ) && jQuery.contains( doc, node ) ) { + + if ( node.src ) { + // Hope ajax is available... + jQuery._evalUrl( node.src ); + } else { + jQuery.globalEval( ( node.text || node.textContent || node.innerHTML || "" ).replace( rcleanScript, "" ) ); + } + } + } + } + + // Fix #11809: Avoid leaking memory + fragment = first = null; + } + } + + return this; + } +}); + +// Support: IE<8 +// Manipulating tables requires a tbody +function manipulationTarget( elem, content ) { + return jQuery.nodeName( elem, "table" ) && + jQuery.nodeName( content.nodeType === 1 ? 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 = (jQuery.find.attr( elem, "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 elem, + i = 0; + for ( ; (elem = elems[i]) != null; i++ ) { + jQuery._data( elem, "globalEval", !refElements || jQuery._data( refElements[i], "globalEval" ) ); + } +} + +function cloneCopyEvent( src, dest ) { + + if ( dest.nodeType !== 1 || !jQuery.hasData( src ) ) { + return; + } + + var type, i, l, + oldData = jQuery._data( src ), + curData = jQuery._data( dest, oldData ), + events = oldData.events; + + if ( events ) { + delete curData.handle; + curData.events = {}; + + for ( type in events ) { + for ( i = 0, l = events[ type ].length; i < l; i++ ) { + jQuery.event.add( dest, type, events[ type ][ i ] ); + } + } + } + + // make the cloned public data object a copy from the original + if ( curData.data ) { + curData.data = jQuery.extend( {}, curData.data ); + } +} + +function fixCloneNodeIssues( src, dest ) { + var nodeName, e, data; + + // We do not need to do anything for non-Elements + if ( dest.nodeType !== 1 ) { + return; + } + + nodeName = dest.nodeName.toLowerCase(); + + // IE6-8 copies events bound via attachEvent when using cloneNode. + if ( !jQuery.support.noCloneEvent && dest[ jQuery.expando ] ) { + data = jQuery._data( dest ); + + for ( e in data.events ) { + jQuery.removeEvent( dest, e, data.handle ); + } + + // Event data gets referenced instead of copied if the expando gets copied too + dest.removeAttribute( jQuery.expando ); + } + + // IE blanks contents when cloning scripts, and tries to evaluate newly-set text + if ( nodeName === "script" && dest.text !== src.text ) { + disableScript( dest ).text = src.text; + restoreScript( dest ); + + // IE6-10 improperly clones children of object elements using classid. + // IE10 throws NoModificationAllowedError if parent is null, #12132. + } else if ( nodeName === "object" ) { + if ( dest.parentNode ) { + dest.outerHTML = src.outerHTML; + } + + // This path appears unavoidable for IE9. When cloning an object + // element in IE9, the outerHTML strategy above is not sufficient. + // If the src has innerHTML and the destination does not, + // copy the src.innerHTML into the dest.innerHTML. #10324 + if ( jQuery.support.html5Clone && ( src.innerHTML && !jQuery.trim(dest.innerHTML) ) ) { + dest.innerHTML = src.innerHTML; + } + + } else if ( nodeName === "input" && manipulation_rcheckableType.test( src.type ) ) { + // IE6-8 fails to persist the checked state of a cloned checkbox + // or radio button. Worse, IE6-7 fail to give the cloned element + // a checked appearance if the defaultChecked value isn't also set + + dest.defaultChecked = dest.checked = src.checked; + + // IE6-7 get confused and end up setting the value of a cloned + // checkbox/radio button to an empty string instead of "on" + if ( dest.value !== src.value ) { + dest.value = src.value; + } + + // IE6-8 fails to return the selected option to the default selected + // state when cloning options + } else if ( nodeName === "option" ) { + dest.defaultSelected = dest.selected = src.defaultSelected; + + // IE6-8 fails to set the defaultValue to the correct value when + // cloning other types of input fields + } else if ( nodeName === "input" || nodeName === "textarea" ) { + dest.defaultValue = src.defaultValue; + } +} + +jQuery.each({ + appendTo: "append", + prependTo: "prepend", + insertBefore: "before", + insertAfter: "after", + replaceAll: "replaceWith" +}, function( name, original ) { + jQuery.fn[ name ] = function( selector ) { + var elems, + i = 0, + ret = [], + insert = jQuery( selector ), + last = insert.length - 1; + + for ( ; i <= last; i++ ) { + elems = i === last ? this : this.clone(true); + jQuery( insert[i] )[ original ]( elems ); + + // Modern browsers can apply jQuery collections as arrays, but oldIE needs a .get() + core_push.apply( ret, elems.get() ); + } + + return this.pushStack( ret ); + }; +}); + +function getAll( context, tag ) { + var elems, elem, + i = 0, + found = typeof context.getElementsByTagName !== core_strundefined ? context.getElementsByTagName( tag || "*" ) : + typeof context.querySelectorAll !== core_strundefined ? context.querySelectorAll( tag || "*" ) : + undefined; + + if ( !found ) { + for ( found = [], elems = context.childNodes || context; (elem = elems[i]) != null; i++ ) { + if ( !tag || jQuery.nodeName( elem, tag ) ) { + found.push( elem ); + } else { + jQuery.merge( found, getAll( elem, tag ) ); + } + } + } + + return tag === undefined || tag && jQuery.nodeName( context, tag ) ? + jQuery.merge( [ context ], found ) : + found; +} + +// Used in buildFragment, fixes the defaultChecked property +function fixDefaultChecked( elem ) { + if ( manipulation_rcheckableType.test( elem.type ) ) { + elem.defaultChecked = elem.checked; + } +} + +jQuery.extend({ + clone: function( elem, dataAndEvents, deepDataAndEvents ) { + var destElements, node, clone, i, srcElements, + inPage = jQuery.contains( elem.ownerDocument, elem ); + + if ( jQuery.support.html5Clone || jQuery.isXMLDoc(elem) || !rnoshimcache.test( "<" + elem.nodeName + ">" ) ) { + clone = elem.cloneNode( true ); + + // IE<=8 does not properly clone detached, unknown element nodes + } else { + fragmentDiv.innerHTML = elem.outerHTML; + fragmentDiv.removeChild( clone = fragmentDiv.firstChild ); + } + + if ( (!jQuery.support.noCloneEvent || !jQuery.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 ); + + // Fix all IE cloning issues + for ( i = 0; (node = srcElements[i]) != null; ++i ) { + // Ensure that the destination node is not null; Fixes #9587 + if ( destElements[i] ) { + fixCloneNodeIssues( node, 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; (node = srcElements[i]) != null; i++ ) { + cloneCopyEvent( node, destElements[i] ); + } + } else { + cloneCopyEvent( elem, clone ); + } + } + + // Preserve script evaluation history + destElements = getAll( clone, "script" ); + if ( destElements.length > 0 ) { + setGlobalEval( destElements, !inPage && getAll( elem, "script" ) ); + } + + destElements = srcElements = node = null; + + // Return the cloned set + return clone; + }, + + buildFragment: function( elems, context, scripts, selection ) { + var j, elem, contains, + tmp, tag, tbody, wrap, + l = elems.length, + + // Ensure a safe fragment + safe = createSafeFragment( context ), + + nodes = [], + i = 0; + + for ( ; i < l; i++ ) { + elem = elems[ i ]; + + if ( elem || elem === 0 ) { + + // Add nodes directly + if ( jQuery.type( elem ) === "object" ) { + 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 || safe.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; + } + + // Manually add leading whitespace removed by IE + if ( !jQuery.support.leadingWhitespace && rleadingWhitespace.test( elem ) ) { + nodes.push( context.createTextNode( rleadingWhitespace.exec( elem )[0] ) ); + } + + // Remove IE's autoinserted from table fragments + if ( !jQuery.support.tbody ) { + + // String was a , *may* have spurious + elem = tag === "table" && !rtbody.test( elem ) ? + tmp.firstChild : + + // String was a bare or + wrap[1] === "
    " && !rtbody.test( elem ) ? + tmp : + 0; + + j = elem && elem.childNodes.length; + while ( j-- ) { + if ( jQuery.nodeName( (tbody = elem.childNodes[j]), "tbody" ) && !tbody.childNodes.length ) { + elem.removeChild( tbody ); + } + } + } + + jQuery.merge( nodes, tmp.childNodes ); + + // Fix #12392 for WebKit and IE > 9 + tmp.textContent = ""; + + // Fix #12392 for oldIE + while ( tmp.firstChild ) { + tmp.removeChild( tmp.firstChild ); + } + + // Remember the top-level container for proper cleanup + tmp = safe.lastChild; + } + } + } + + // Fix #11356: Clear elements from fragment + if ( tmp ) { + safe.removeChild( tmp ); + } + + // Reset defaultChecked for any radios and checkboxes + // about to be appended to the DOM in IE 6/7 (#8060) + if ( !jQuery.support.appendChecked ) { + jQuery.grep( getAll( nodes, "input" ), fixDefaultChecked ); + } + + 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( safe.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 ); + } + } + } + } + + tmp = null; + + return safe; + }, + + cleanData: function( elems, /* internal */ acceptData ) { + var elem, type, id, data, + i = 0, + internalKey = jQuery.expando, + cache = jQuery.cache, + deleteExpando = jQuery.support.deleteExpando, + special = jQuery.event.special; + + for ( ; (elem = elems[i]) != null; i++ ) { + + if ( acceptData || jQuery.acceptData( elem ) ) { + + id = elem[ internalKey ]; + data = id && cache[ id ]; + + if ( data ) { + 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 ); + } + } + } + + // Remove cache only if it was not already removed by jQuery.event.remove + if ( cache[ id ] ) { + + delete cache[ id ]; + + // IE does not allow us to delete expando properties from nodes, + // nor does it have a removeAttribute function on Document nodes; + // we must handle all of these cases + if ( deleteExpando ) { + delete elem[ internalKey ]; + + } else if ( typeof elem.removeAttribute !== core_strundefined ) { + elem.removeAttribute( internalKey ); + + } else { + elem[ internalKey ] = null; + } + + core_deletedIds.push( id ); + } + } + } + } + }, + + _evalUrl: function( url ) { + return jQuery.ajax({ + url: url, + type: "GET", + dataType: "script", + async: false, + global: false, + "throws": true + }); + } +}); +jQuery.fn.extend({ + wrapAll: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapAll( html.call(this, i) ); + }); + } + + if ( this[0] ) { + // The elements to wrap the target around + var wrap = jQuery( html, this[0].ownerDocument ).eq(0).clone(true); + + if ( this[0].parentNode ) { + wrap.insertBefore( this[0] ); + } + + wrap.map(function() { + var elem = this; + + while ( elem.firstChild && elem.firstChild.nodeType === 1 ) { + elem = elem.firstChild; + } + + return elem; + }).append( this ); + } + + return this; + }, + + wrapInner: function( html ) { + if ( jQuery.isFunction( html ) ) { + return this.each(function(i) { + jQuery(this).wrapInner( html.call(this, i) ); + }); + } + + return this.each(function() { + var self = jQuery( this ), + contents = self.contents(); + + if ( contents.length ) { + contents.wrapAll( html ); + + } else { + self.append( html ); + } + }); + }, + + wrap: function( html ) { + var isFunction = jQuery.isFunction( html ); + + return this.each(function(i) { + jQuery( this ).wrapAll( isFunction ? html.call(this, i) : html ); + }); + }, + + unwrap: function() { + return this.parent().each(function() { + if ( !jQuery.nodeName( this, "body" ) ) { + jQuery( this ).replaceWith( this.childNodes ); + } + }).end(); + } +}); +var iframe, getStyles, curCSS, + ralpha = /alpha\([^)]*\)/i, + ropacity = /opacity\s*=\s*([^)]*)/, + rposition = /^(top|right|bottom|left)$/, + // swappable if display is none or starts with table except "table", "table-cell", or "table-caption" + // see here for display values: https://developer.mozilla.org/en-US/docs/CSS/display + rdisplayswap = /^(none|table(?!-c[ea]).+)/, + rmargin = /^margin/, + rnumsplit = new RegExp( "^(" + core_pnum + ")(.*)$", "i" ), + rnumnonpx = new RegExp( "^(" + core_pnum + ")(?!px)[a-z%]+$", "i" ), + rrelNum = new RegExp( "^([+-])=(" + core_pnum + ")", "i" ), + elemdisplay = { BODY: "block" }, + + cssShow = { position: "absolute", visibility: "hidden", display: "block" }, + cssNormalTransform = { + letterSpacing: 0, + fontWeight: 400 + }, + + cssExpand = [ "Top", "Right", "Bottom", "Left" ], + cssPrefixes = [ "Webkit", "O", "Moz", "ms" ]; + +// return a css property mapped to a potentially vendor prefixed property +function vendorPropName( style, name ) { + + // shortcut for names that are not vendor prefixed + if ( name in style ) { + return name; + } + + // check for vendor prefixed names + var capName = name.charAt(0).toUpperCase() + name.slice(1), + origName = name, + i = cssPrefixes.length; + + while ( i-- ) { + name = cssPrefixes[ i ] + capName; + if ( name in style ) { + return name; + } + } + + return origName; +} + +function isHidden( 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 ); +} + +function showHide( elements, show ) { + var display, elem, hidden, + values = [], + index = 0, + length = elements.length; + + for ( ; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + + values[ index ] = jQuery._data( elem, "olddisplay" ); + display = elem.style.display; + if ( show ) { + // Reset the inline display of this element to learn if it is + // being hidden by cascaded rules or not + if ( !values[ index ] && display === "none" ) { + elem.style.display = ""; + } + + // Set elements which have been overridden with display: none + // in a stylesheet to whatever the default browser style is + // for such an element + if ( elem.style.display === "" && isHidden( elem ) ) { + values[ index ] = jQuery._data( elem, "olddisplay", css_defaultDisplay(elem.nodeName) ); + } + } else { + + if ( !values[ index ] ) { + hidden = isHidden( elem ); + + if ( display && display !== "none" || !hidden ) { + jQuery._data( elem, "olddisplay", hidden ? display : jQuery.css( elem, "display" ) ); + } + } + } + } + + // Set the display of most of the elements in a second loop + // to avoid the constant reflow + for ( index = 0; index < length; index++ ) { + elem = elements[ index ]; + if ( !elem.style ) { + continue; + } + if ( !show || elem.style.display === "none" || elem.style.display === "" ) { + elem.style.display = show ? values[ index ] || "" : "none"; + } + } + + return elements; +} + +jQuery.fn.extend({ + css: function( name, value ) { + return jQuery.access( this, function( elem, name, value ) { + var len, styles, + map = {}, + i = 0; + + if ( jQuery.isArray( name ) ) { + styles = getStyles( elem ); + len = name.length; + + for ( ; i < len; i++ ) { + map[ name[ i ] ] = jQuery.css( elem, name[ i ], false, styles ); + } + + return map; + } + + return value !== undefined ? + jQuery.style( elem, name, value ) : + jQuery.css( elem, name ); + }, name, value, arguments.length > 1 ); + }, + show: function() { + return showHide( this, true ); + }, + hide: function() { + return showHide( this ); + }, + toggle: function( state ) { + if ( typeof state === "boolean" ) { + return state ? this.show() : this.hide(); + } + + return this.each(function() { + if ( isHidden( this ) ) { + jQuery( this ).show(); + } else { + jQuery( this ).hide(); + } + }); + } +}); + +jQuery.extend({ + // Add in style property hooks for overriding the default + // behavior of getting and setting a style property + cssHooks: { + opacity: { + get: function( elem, computed ) { + if ( computed ) { + // We should always get a number back from opacity + var ret = curCSS( elem, "opacity" ); + return ret === "" ? "1" : ret; + } + } + } + }, + + // Don't automatically add "px" to these possibly-unitless properties + cssNumber: { + "columnCount": true, + "fillOpacity": true, + "fontWeight": true, + "lineHeight": true, + "opacity": true, + "order": true, + "orphans": true, + "widows": true, + "zIndex": true, + "zoom": true + }, + + // Add in properties whose names you wish to fix before + // setting or getting the value + cssProps: { + // normalize float css property + "float": jQuery.support.cssFloat ? "cssFloat" : "styleFloat" + }, + + // Get and set the style property on a DOM Node + style: function( elem, name, value, extra ) { + // Don't set styles on text and comment nodes + if ( !elem || elem.nodeType === 3 || elem.nodeType === 8 || !elem.style ) { + return; + } + + // Make sure that we're working with the right name + var ret, type, hooks, + origName = jQuery.camelCase( name ), + style = elem.style; + + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // Check if we're setting a value + if ( value !== undefined ) { + type = typeof value; + + // convert relative number strings (+= or -=) to relative numbers. #7345 + if ( type === "string" && (ret = rrelNum.exec( value )) ) { + value = ( ret[1] + 1 ) * ret[2] + parseFloat( jQuery.css( elem, name ) ); + // Fixes bug #9237 + type = "number"; + } + + // Make sure that NaN and null values aren't set. See: #7116 + if ( value == null || type === "number" && isNaN( value ) ) { + return; + } + + // If a number was passed in, add 'px' to the (except for certain CSS properties) + if ( type === "number" && !jQuery.cssNumber[ origName ] ) { + value += "px"; + } + + // Fixes #8908, it can be done more correctly by specifing setters in cssHooks, + // but it would mean to define eight (for every problematic property) identical functions + if ( !jQuery.support.clearCloneStyle && value === "" && name.indexOf("background") === 0 ) { + style[ name ] = "inherit"; + } + + // If a hook was provided, use that value, otherwise just set the specified value + if ( !hooks || !("set" in hooks) || (value = hooks.set( elem, value, extra )) !== undefined ) { + + // Wrapped to prevent IE from throwing errors when 'invalid' values are provided + // Fixes bug #5509 + try { + style[ name ] = value; + } catch(e) {} + } + + } else { + // If a hook was provided get the non-computed value from there + if ( hooks && "get" in hooks && (ret = hooks.get( elem, false, extra )) !== undefined ) { + return ret; + } + + // Otherwise just get the value from the style object + return style[ name ]; + } + }, + + css: function( elem, name, extra, styles ) { + var num, val, hooks, + origName = jQuery.camelCase( name ); + + // Make sure that we're working with the right name + name = jQuery.cssProps[ origName ] || ( jQuery.cssProps[ origName ] = vendorPropName( elem.style, origName ) ); + + // gets hook for the prefixed version + // followed by the unprefixed version + hooks = jQuery.cssHooks[ name ] || jQuery.cssHooks[ origName ]; + + // If a hook was provided get the computed value from there + if ( hooks && "get" in hooks ) { + val = hooks.get( elem, true, extra ); + } + + // Otherwise, if a way to get the computed value exists, use that + if ( val === undefined ) { + val = curCSS( elem, name, styles ); + } + + //convert "normal" to computed value + if ( val === "normal" && name in cssNormalTransform ) { + val = cssNormalTransform[ name ]; + } + + // Return, converting to number if forced or a qualifier was provided and val looks numeric + if ( extra === "" || extra ) { + num = parseFloat( val ); + return extra === true || jQuery.isNumeric( num ) ? num || 0 : val; + } + return val; + } +}); + +// NOTE: we've included the "window" in window.getComputedStyle +// because jsdom on node.js will break without it. +if ( window.getComputedStyle ) { + getStyles = function( elem ) { + return window.getComputedStyle( elem, null ); + }; + + curCSS = function( elem, name, _computed ) { + var width, minWidth, maxWidth, + computed = _computed || getStyles( elem ), + + // getPropertyValue is only needed for .css('filter') in IE9, see #12537 + ret = computed ? computed.getPropertyValue( name ) || computed[ name ] : undefined, + style = elem.style; + + if ( computed ) { + + if ( ret === "" && !jQuery.contains( elem.ownerDocument, elem ) ) { + ret = jQuery.style( elem, name ); + } + + // A tribute to the "awesome hack by Dean Edwards" + // Chrome < 17 and Safari 5.0 uses "computed value" instead of "used value" for margin-right + // Safari 5.1.7 (at least) returns percentage for a larger set of values, but width seems to be reliably pixels + // this is against the CSSOM draft spec: http://dev.w3.org/csswg/cssom/#resolved-values + if ( rnumnonpx.test( ret ) && rmargin.test( name ) ) { + + // Remember the original values + width = style.width; + minWidth = style.minWidth; + maxWidth = style.maxWidth; + + // Put in the new values to get a computed value out + style.minWidth = style.maxWidth = style.width = ret; + ret = computed.width; + + // Revert the changed values + style.width = width; + style.minWidth = minWidth; + style.maxWidth = maxWidth; + } + } + + return ret; + }; +} else if ( document.documentElement.currentStyle ) { + getStyles = function( elem ) { + return elem.currentStyle; + }; + + curCSS = function( elem, name, _computed ) { + var left, rs, rsLeft, + computed = _computed || getStyles( elem ), + ret = computed ? computed[ name ] : undefined, + style = elem.style; + + // Avoid setting ret to empty string here + // so we don't default to auto + if ( ret == null && style && style[ name ] ) { + ret = style[ name ]; + } + + // From the awesome hack by Dean Edwards + // http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 + + // If we're not dealing with a regular pixel number + // but a number that has a weird ending, we need to convert it to pixels + // but not position css attributes, as those are proportional to the parent element instead + // and we can't measure the parent instead because it might trigger a "stacking dolls" problem + if ( rnumnonpx.test( ret ) && !rposition.test( name ) ) { + + // Remember the original values + left = style.left; + rs = elem.runtimeStyle; + rsLeft = rs && rs.left; + + // Put in the new values to get a computed value out + if ( rsLeft ) { + rs.left = elem.currentStyle.left; + } + style.left = name === "fontSize" ? "1em" : ret; + ret = style.pixelLeft + "px"; + + // Revert the changed values + style.left = left; + if ( rsLeft ) { + rs.left = rsLeft; + } + } + + return ret === "" ? "auto" : ret; + }; +} + +function setPositiveNumber( elem, value, subtract ) { + var matches = rnumsplit.exec( value ); + return matches ? + // Guard against undefined "subtract", e.g., when used as in cssHooks + Math.max( 0, matches[ 1 ] - ( subtract || 0 ) ) + ( matches[ 2 ] || "px" ) : + value; +} + +function augmentWidthOrHeight( elem, name, extra, isBorderBox, styles ) { + var i = extra === ( isBorderBox ? "border" : "content" ) ? + // If we already have the right measurement, avoid augmentation + 4 : + // Otherwise initialize for horizontal or vertical properties + name === "width" ? 1 : 0, + + val = 0; + + for ( ; i < 4; i += 2 ) { + // both box models exclude margin, so add it if we want it + if ( extra === "margin" ) { + val += jQuery.css( elem, extra + cssExpand[ i ], true, styles ); + } + + if ( isBorderBox ) { + // border-box includes padding, so remove it if we want content + if ( extra === "content" ) { + val -= jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + } + + // at this point, extra isn't border nor margin, so remove border + if ( extra !== "margin" ) { + val -= jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } else { + // at this point, extra isn't content, so add padding + val += jQuery.css( elem, "padding" + cssExpand[ i ], true, styles ); + + // at this point, extra isn't content nor padding, so add border + if ( extra !== "padding" ) { + val += jQuery.css( elem, "border" + cssExpand[ i ] + "Width", true, styles ); + } + } + } + + return val; +} + +function getWidthOrHeight( elem, name, extra ) { + + // Start with offset property, which is equivalent to the border-box value + var valueIsBorderBox = true, + val = name === "width" ? elem.offsetWidth : elem.offsetHeight, + styles = getStyles( elem ), + isBorderBox = jQuery.support.boxSizing && jQuery.css( elem, "boxSizing", false, styles ) === "border-box"; + + // some non-html elements return undefined for offsetWidth, so check for null/undefined + // svg - https://bugzilla.mozilla.org/show_bug.cgi?id=649285 + // MathML - https://bugzilla.mozilla.org/show_bug.cgi?id=491668 + if ( val <= 0 || val == null ) { + // Fall back to computed then uncomputed css if necessary + val = curCSS( elem, name, styles ); + if ( val < 0 || val == null ) { + val = elem.style[ name ]; + } + + // Computed unit is not pixels. Stop here and return. + if ( rnumnonpx.test(val) ) { + return val; + } + + // we need the check for style in case a browser which returns unreliable values + // for getComputedStyle silently falls back to the reliable elem.style + valueIsBorderBox = isBorderBox && ( jQuery.support.boxSizingReliable || val === elem.style[ name ] ); + + // Normalize "", auto, and prepare for extra + val = parseFloat( val ) || 0; + } + + // use the active box-sizing model to add/subtract irrelevant styles + return ( val + + augmentWidthOrHeight( + elem, + name, + extra || ( isBorderBox ? "border" : "content" ), + valueIsBorderBox, + styles + ) + ) + "px"; +} + +// Try to determine the default display value of an element +function css_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("