partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit c06ef811d7e81e5ba4d750ee35ebfca31f9b2223
parent f39ffce04eba9bd04c39327c0f8c2fd46dac4985
Author: Felicitus <felicitus@felicitus.org>
Date:   Thu, 23 Jul 2015 21:02:19 +0200

Added replacement and additions for 1:1 imag relations

Diffstat:
Mapp/config/config.yml | 4+++-
Msrc/PartKeepr/FootprintBundle/Entity/Footprint.php | 20+++++++++++++++-----
Msrc/PartKeepr/FootprintBundle/Entity/FootprintImage.php | 82++++++++++++++++++++++++++++++++++++++-----------------------------------------
Msrc/PartKeepr/FrontendBundle/Resources/public/css/PartKeepr.css | 6++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Footprint/FootprintEditor.js | 31++++++++++++++++++++++++++-----
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/RemoteImageField.js | 109++++++++++++++++++++++++++-----------------------------------------------------
Msrc/PartKeepr/ImageBundle/Controller/ImageController.php | 34+++++++++++++++++++++++++++++++++-
Msrc/PartKeepr/ImageBundle/Services/ImageService.php | 23++++-------------------
Asrc/PartKeepr/UploadedFileBundle/Annotation/UploadedFile.php | 14++++++++++++++
Msrc/PartKeepr/UploadedFileBundle/Entity/UploadedFile.php | 27+++++++++++++++++++++++++++
Msrc/PartKeepr/UploadedFileBundle/EventListener/TemporaryFileEventListener.php | 142++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/PartKeepr/UploadedFileBundle/Services/UploadedFileService.php | 6------
12 files changed, 317 insertions(+), 181 deletions(-)

diff --git a/app/config/config.yml b/app/config/config.yml @@ -136,9 +136,11 @@ services: class: PartKeepr\UploadedFileBundle\EventListener\TemporaryFileEventListener arguments: - "@partkeepr_uploadedfile_service" + - "@partkeepr_image_service" - "@annotation_reader" - "@property_accessor" - tags: [ { name: "kernel.event_listener", event: "api.pre_update", method: "replaceTemporaryImage" }, { name: "kernel.event_listener", event: "api.pre_create", method: "replaceTemporaryImage" } ] + - "@api.iri_converter" + tags: [ { name: "kernel.event_listener", event: "api.pre_update", method: "replaceTemporaryFile" }, { name: "kernel.event_listener", event: "api.pre_create", method: "replaceTemporaryFile" } ] resource.distributor: parent: "api.resource" diff --git a/src/PartKeepr/FootprintBundle/Entity/Footprint.php b/src/PartKeepr/FootprintBundle/Entity/Footprint.php @@ -6,6 +6,7 @@ use Doctrine\ORM\Mapping as ORM; use PartKeepr\DoctrineReflectionBundle\Annotation\TargetService; use PartKeepr\Util\BaseEntity; use PartKeepr\UploadedFileBundle\Annotation\UploadedFileCollection; +use PartKeepr\UploadedFileBundle\Annotation\UploadedFile; use Symfony\Component\Serializer\Annotation\Groups; /** @@ -47,10 +48,11 @@ class Footprint extends BaseEntity /** * Holds the footprint image * - * @ORM\OneToOne(targetEntity="FootprintImage", mappedBy="footprint", cascade={"persist", "remove"}) + * @ORM\OneToOne(targetEntity="PartKeepr\FootprintBundle\Entity\FootprintImage", + * mappedBy="footprint", cascade={"persist", "remove"}, orphanRemoval=true) * * @Groups({"default"}) - * + * @UploadedFile() * @var FootprintImage */ private $image; @@ -148,10 +150,18 @@ class Footprint extends BaseEntity * * @return void */ - public function setImage(FootprintImage $image) + public function setImage($image) { - $this->image = $image; - $image->setFootprint($this); + if ($image instanceof FootprintImage) { + $image->setFootprint($this); + $this->image = $image; + } else { + // Because this is a 1:1 relationship. only allow the temporary image to be set when no image exists. + // If an image exists, the frontend needs to deliver the old file ID with the replacement property set. + if ($this->getImage() === null) { + $this->image = $image; + } + } } /** diff --git a/src/PartKeepr/FootprintBundle/Entity/FootprintImage.php b/src/PartKeepr/FootprintBundle/Entity/FootprintImage.php @@ -1,52 +1,49 @@ <?php namespace PartKeepr\FootprintBundle\Entity; -use PartKeepr\Util\Serializable, - PartKeepr\ImageBundle\Entity\Image, - Doctrine\ORM\Mapping as ORM; +use Doctrine\ORM\Mapping as ORM; +use PartKeepr\ImageBundle\Entity\Image; /** * Holds a footprint image + * * @ORM\Entity **/ -class FootprintImage extends Image implements Serializable { - /** - * The footprint object - * @ORM\OneToOne(targetEntity="PartKeepr\FootprintBundle\Entity\Footprint",inversedBy="image") - * +class FootprintImage extends Image +{ + /** + * The footprint object + * @ORM\OneToOne(targetEntity="PartKeepr\FootprintBundle\Entity\Footprint",inversedBy="image") + * * @var Footprint - */ - private $footprint = null; - - /** - * Creates a new IC logo instance - */ - public function __construct () { - parent::__construct(Image::IMAGE_FOOTPRINT); - } + */ + private $footprint = null; - /** - * Sets the footprint - * @param Footprint $footprint The footprint to set - */ - public function setFootprint (Footprint $footprint) { - $this->footprint = $footprint; - } - - /** - * Returns the footprint - * @return Footprint the footprint - */ - public function getFootprint () { - return $this->footprint; - } - - /** - * - * Serializes this ic logo - * @return array The serialized ic logo - */ - public function serialize () { - return array("id" => $this->getId(), "footprint_id" => $this->getFootprint()->getId()); - } -}- \ No newline at end of file + /** + * Creates a new IC logo instance + */ + public function __construct() + { + parent::__construct(Image::IMAGE_FOOTPRINT); + } + + /** + * Sets the footprint + * + * @param Footprint $footprint The footprint to set + */ + public function setFootprint(Footprint $footprint) + { + $this->footprint = $footprint; + } + + /** + * Returns the footprint + * + * @return Footprint the footprint + */ + public function getFootprint() + { + return $this->footprint; + } +} diff --git a/src/PartKeepr/FrontendBundle/Resources/public/css/PartKeepr.css b/src/PartKeepr/FrontendBundle/Resources/public/css/PartKeepr.css @@ -15,4 +15,10 @@ .x-item-selected { outline: 1px solid #157fcc !important; outline-offset: -1px; +} + +.remote-image-background { + background-color: white; + border: 1px solid black; + text-align: center; } \ No newline at end of file diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Footprint/FootprintEditor.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Footprint/FootprintEditor.js @@ -3,6 +3,7 @@ Ext.define('PartKeepr.FootprintEditor', { alias: 'widget.FootprintEditor', saveText: i18n("Save Footprint"), layout: 'column', + defaultListenerScope: true, syncDirect: true, labelWidth: 75, initComponent: function () { @@ -44,23 +45,43 @@ Ext.define('PartKeepr.FootprintEditor', { },{ width: 370, height: 250, - xtype: 'remoteimagefield', - name: 'image_id', - imageType: 'footprint', - imageWidth: 256, - imageHeight: 256, + xtype: 'fieldcontainer', + items: { + xtype: 'remoteimagefield', + itemId: 'image', + imageType: 'footprint', + maxHeight: 256, + maxWidth: 256, + listeners: { + 'fileUploaded': "onFileUploaded" + } + }, labelWidth: 75, fieldLabel: i18n("Image") + }]; this.on("itemSaved", this._onItemSaved, this); this.callParent(); }, + onFileUploaded: function (data) { + var uploadedFile = Ext.create("PartKeepr.UploadedFileBundle.Entity.TempUploadedFile", data); + + if (this.record.getImage() === null) { + this.record.setImage(data); + } else { + this.record.getImage().set("replacement", uploadedFile.getId()); + } + + this.down('#image').setValue(uploadedFile); + }, _onItemSaved: function (record) { this.attachmentGrid.bindStore(record.attachments()); }, onEditStart: function () { var store = this.record.attachments(); this.attachmentGrid.bindStore(store); + this.down('#image').setValue(this.record.getImage()); + } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/RemoteImageField.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Widgets/RemoteImageField.js @@ -6,65 +6,41 @@ * */ Ext.define('PartKeepr.RemoteImageField', { - extend:'Ext.form.field.Base', + extend:'Ext.container.Container', alias: 'widget.remoteimagefield', - type: 'remoteimagefield', - - // Default width and height - imageWidth: 32, - imageHeight: 32, - - // The field template for rendering this field - fieldSubTpl: [ - '<img id="{cmpId}-imgEl" style="{size}" class="remoteimagefield"/>', - { - compiled: true, - disableFormats: true - }], - + xtype: 'remoteimagefield', + + listeners: { + 'click': "onClick" + }, + layout: 'vbox', /** * Initializes the field */ initComponent : function(){ - this.minHeight = this.imageHeight; - this.minWidth = this.imageWidth; - - this.imageId = Ext.id("remoteimagefield"); - this.callParent(); - }, - /** - * Return the template data for this field - */ - getSubTplData: function() { - return { - cmpId: this.id, - size: 'height:'+this.imageHeight+"px;width:"+this.imageWidth+"px;", - imageid: this.imageId - }; - }, - /** - * Renders this field. - */ - onRender: function() { - var me = this; - /** - * @todo what did we use this for? - */ + this.image = Ext.create("Ext.Img", { + maxHeight: this.maxHeight, + maxWidth: this.maxWidth, + autoEl: 'div', + width: this.maxWidth, + height: this.maxHeight, + flex: 1, + cls: 'remote-image-background' + }); - //me.onLabelableRender(); - //me.addChildEls('imgEl'); + this.button = Ext.create("Ext.button.Button", { + text: i18n("Change Image"), + scope: this, + handler: this.onClick + }); - me.callParent(arguments); - }, - /** - * Applies the image URL to the element after rendering - */ - afterRender: function () { - //this.imgEl.dom.src = this.getImageURL(); - - //this.imgEl.on("click", this.onClick, this); + this.items = [ this.image, this.button ]; + this.minHeight = this.imageHeight; + this.minWidth = this.imageWidth; + + this.callParent(); }, onClick: function () { var j = Ext.create("PartKeepr.FileUploadDialog", { imageUpload: true }); @@ -72,23 +48,7 @@ Ext.define('PartKeepr.RemoteImageField', { j.show(); }, onFileUploaded: function (data) { - this.setValue("TMP:"+data.id); - }, - /** - * Returns the URL for the image field. Applies the temporary image if TMP: is - * found within the value. - */ - getImageURL: function () { - var idparam; - - if (strpos(this.value, "TMP:") !== false) { - idparam = "id=0&tmpId="+str_replace("TMP:","",this.value); - } else { - idparam = "id="+this.value; - } - - return PartKeepr.getImagePath() + "?"+idparam+"&type="+this.imageType+"&w="+this.imageWidth+"&h="+this.imageHeight+"&m=fitpadding&_dc="+Ext.Date.now(); - + this.fireEvent("fileUploaded", data); }, /** * Sets a value for the field. If the value is numeric, we call the image service @@ -101,13 +61,14 @@ Ext.define('PartKeepr.RemoteImageField', { * @return {Ext.form.field.Field} this */ setValue: function(value) { - var me = this; - - this.setRawValue(value); - this.value = value; - if (this.rendered) { - this.imgEl.dom.src = this.getImageURL(); - } + this.value = value; + + if (value !== null) { + this.image.setSrc(value.getId() + "?maxWidth="+this.imageWidth+"&maxHeight="+this.imageHeight + "&ts=" + new Date().getTime()); + } else { + this.image.setSrc(""); + } + return this; } }); diff --git a/src/PartKeepr/ImageBundle/Controller/ImageController.php b/src/PartKeepr/ImageBundle/Controller/ImageController.php @@ -108,7 +108,7 @@ abstract class ImageController extends FileController $outputFile = $this->getImageCacheFilename($image, $width, $height, $mode); - if (file_exists($outputFile)) { + if ($this->hasCacheFile($image, $width, $height, $mode) && file_exists($outputFile)) { return $outputFile; } @@ -141,4 +141,36 @@ abstract class ImageController extends FileController return $outputFile; } + + /** + * Checks if the database contains the cache file. + * + * @param PartKeeprImage $image + * @param $width + * @param $height + * @param $mode + * + * @return bool + */ + protected function hasCacheFile(PartKeeprImage $image, $width, $height, $mode) + { + $cacheFilename = $this->getImageCacheFilename($image, $width, $height, $mode); + + /** + * @var $em EntityManager + */ + $em = $this->getDoctrine()->getManager(); + + $queryBuilder = $em->createQueryBuilder(); + $queryBuilder->select("COUNT(c)") + ->from('PartKeepr\ImageBundle\Entity\CachedImage', "c") + ->where("c.cacheFile = :file") + ->setParameter("file", $cacheFilename); + + if ($queryBuilder->getQuery()->getSingleScalarResult() > 0) { + return true; + } else { + return false; + } + } } diff --git a/src/PartKeepr/ImageBundle/Services/ImageService.php b/src/PartKeepr/ImageBundle/Services/ImageService.php @@ -12,26 +12,11 @@ use Symfony\Component\HttpFoundation\File\File; class ImageService extends UploadedFileService { /** - * Replaces the current image with a new image. - * - * @param UploadedFile $file The target file - * @param File $filesystemFile The source file - */ - public function replace(UploadedFile $file, File $filesystemFile) - { - parent::replace($file, $filesystemFile); - $this->invalidate($file); - } - - /** - * Replaces the file from an URL. Does some tricks to avoid 403 forbidden on some sites. - * - * @param UploadedFile $file The target file - * @param string $url The URL to replace from + * {@inheritdoc} */ - public function replaceFromURL(UploadedFile $file, $url) + public function replaceFromFilesystem(UploadedFile $file, File $filesystemFile) { - parent::replaceFromURL($file, $url); + parent::replaceFromFilesystem($file, $filesystemFile); $this->invalidate($file); } @@ -48,7 +33,7 @@ class ImageService extends UploadedFileService $entityManager = $this->container->get("doctrine")->getManager(); $queryBuilder = $entityManager->createQueryBuilder(); $queryBuilder->select(array("c")) - ->from('PartKeepr\Image\CachedImage', 'c') + ->from('PartKeepr\ImageBundle\Entity\CachedImage', 'c') ->where("c.originalId = :id") ->andWhere("c.originalType = :type") ->setParameter("id", $file->getId()) diff --git a/src/PartKeepr/UploadedFileBundle/Annotation/UploadedFile.php b/src/PartKeepr/UploadedFileBundle/Annotation/UploadedFile.php @@ -0,0 +1,14 @@ +<?php +namespace PartKeepr\UploadedFileBundle\Annotation; + +use Doctrine\ORM\Mapping\Annotation; + +/** + * @Annotation + * @Target("PROPERTY") + * + * Use this annotation on any property to replace any temporary images with their concrete implementation. + */ +final class UploadedFile implements Annotation +{ +} diff --git a/src/PartKeepr/UploadedFileBundle/Entity/UploadedFile.php b/src/PartKeepr/UploadedFileBundle/Entity/UploadedFile.php @@ -73,16 +73,43 @@ abstract class UploadedFile extends BaseEntity * The description of this attachment * @ORM\Column(type="text",nullable=true) * @Groups({"default"}) + * * @var string */ private $description; + /** + * Holds an ID of a replacement image. + * @Groups({"default"}) + */ + private $replacement = null; + public function __construct() { $this->filename = Uuid::uuid1()->toString(); } /** + * Sets a replacement image + * + * @param $replacement + */ + public function setReplacement($replacement) + { + $this->replacement = $replacement; + } + + /** + * Returns the replacement image, if set + * + * @return mixed + */ + public function getReplacement() + { + return $this->replacement; + } + + /** * Sets the type of the file. Once the type is set, * it may not be changed later. * diff --git a/src/PartKeepr/UploadedFileBundle/EventListener/TemporaryFileEventListener.php b/src/PartKeepr/UploadedFileBundle/EventListener/TemporaryFileEventListener.php @@ -2,8 +2,11 @@ namespace PartKeepr\UploadedFileBundle\EventListener; use Doctrine\Common\Annotations\Reader; +use Dunglas\ApiBundle\Api\IriConverterInterface; use Dunglas\ApiBundle\Event\DataEvent; +use PartKeepr\ImageBundle\Entity\Image; use PartKeepr\ImageBundle\Entity\TempImage; +use PartKeepr\ImageBundle\Services\ImageService; use PartKeepr\UploadedFileBundle\Entity\TempUploadedFile; use PartKeepr\UploadedFileBundle\Entity\UploadedFile; use PartKeepr\UploadedFileBundle\Services\UploadedFileService; @@ -17,6 +20,11 @@ class TemporaryFileEventListener private $uploadedFileService; /** + * @var ImageService + */ + private $imageService; + + /** * @var Reader */ private $reader; @@ -26,14 +34,23 @@ class TemporaryFileEventListener */ private $propertyAccessor; + /** + * @var IriConverterInterface + */ + private $iriConverter; + public function __construct( UploadedFileService $uploadedFileService, + ImageService $imageService, Reader $reader, - PropertyAccessorInterface $propertyAccessor + PropertyAccessorInterface $propertyAccessor, + IriConverterInterface $iriConverter ) { $this->uploadedFileService = $uploadedFileService; + $this->imageService = $imageService; $this->reader = $reader; $this->propertyAccessor = $propertyAccessor; + $this->iriConverter = $iriConverter; } /** @@ -44,60 +61,131 @@ class TemporaryFileEventListener * * @param DataEvent $event The event */ - public function replaceTemporaryImage(DataEvent $event) + public function replaceTemporaryFile(DataEvent $event) { $data = $event->getData(); $classReflection = new \ReflectionClass($data); foreach ($classReflection->getProperties() as $property) { - $propertyAnnotation = $this->reader->getPropertyAnnotation( + $propertyAnnotationCollection = $this->reader->getPropertyAnnotation( $property, 'PartKeepr\UploadedFileBundle\Annotation\UploadedFileCollection' ); + $propertyAnnotation = $this->reader->getPropertyAnnotation( + $property, + 'PartKeepr\UploadedFileBundle\Annotation\UploadedFile' + ); + + $manyToOneAnnotation = $this->reader->getPropertyAnnotation( $property, 'Doctrine\ORM\Mapping\OneToMany' ); - if ($propertyAnnotation !== null) { - $collection = $this->propertyAccessor->getValue($data, $property->getName()); + $oneToOneAnnotation = $this->reader->getPropertyAnnotation( + $property, + 'Doctrine\ORM\Mapping\OneToOne' + ); - foreach ($collection as $key => $item) { - if ($item instanceof TempImage || $item instanceof TempUploadedFile) { - $targetEntity = $manyToOneAnnotation->targetEntity; + if ($propertyAnnotationCollection !== null || $propertyAnnotation !== null) { + if ($manyToOneAnnotation !== null) { + $collection = $this->propertyAccessor->getValue($data, $property->getName()); - /** - * @var $newFile UploadedFile - */ - $newFile = new $targetEntity(); + foreach ($collection as $key => $item) { + if ($item instanceof TempUploadedFile || $item instanceof TempImage) { + $targetEntity = $manyToOneAnnotation->targetEntity; - $this->uploadedFileService->replaceFromUploadedFile($newFile, $item); - $newFile->setOriginalFilename($item->getOriginalFilename()); - $newFile->setDescription($item->getDescription()); + /** + * @var $newFile UploadedFile + */ + $newFile = new $targetEntity(); - // Find the setter for the association - $inverseSideReflection = new \ReflectionClass($newFile); + $this->replaceFile($newFile, $item); - foreach ($inverseSideReflection->getProperties() as $inverseSideProperty) { - $oneToManyAssociation = $this->reader->getPropertyAnnotation( - $inverseSideProperty, - 'Doctrine\ORM\Mapping\ManyToOne' - ); + $setterName = $this->getReferenceSetter($newFile, $data); - if ($oneToManyAssociation !== null && - $oneToManyAssociation->targetEntity == $classReflection->getName()) { - $this->propertyAccessor->setValue($newFile, $inverseSideProperty->getName(), $data); + if ($setterName !== false) { + $this->propertyAccessor->setValue($newFile, $setterName, $data); } + + $collection[$key] = $newFile; } + } + + $this->propertyAccessor->setValue($data, $property->getName(), $collection); + } + + if ($oneToOneAnnotation !== null) { + $item = $this->propertyAccessor->getValue($data, $property->getName()); + + if ($item instanceof TempUploadedFile || $item instanceof TempImage) { + $targetEntity = $oneToOneAnnotation->targetEntity; + + $newFile = new $targetEntity(); - $collection[$key] = $newFile; + $this->replaceFile($newFile, $item); + + $setterName = $this->getReferenceSetter($newFile, $data); + + if ($setterName !== false) { + $this->propertyAccessor->setValue($newFile, $setterName, $data); + } + + $this->propertyAccessor->setValue($data, $property->getName(), $newFile); + } else { + $item = $this->propertyAccessor->getValue($data, $property->getName()); + + if ($item->getReplacement() !== null) { + /** + * @var $tempImage UploadedFile + */ + $tempImage = $this->iriConverter->getItemFromIri($item->getReplacement()); + $this->replaceFile($item, $tempImage); + } } } + } + } + } + + protected function replaceFile(UploadedFile $target, UploadedFile $source) + { + if ($target instanceof Image) { + $this->imageService->replaceFromUploadedFile($target, $source); + } else { + $this->uploadedFileService->replaceFromUploadedFile($target, $source); + } + $target->setDescription($source->getDescription()); + } - $this->propertyAccessor->setValue($data, $property->getName(), $collection); + /** + * Returns the setter name for the inverse side. + * + * @param $inverseSideEntity + * @param $owningSideEntity + * + * @return bool|string + */ + protected function getReferenceSetter($inverseSideEntity, $owningSideEntity) + { + $inverseSideReflection = new \ReflectionClass($inverseSideEntity); + $owningSideReflection = new \ReflectionClass($owningSideEntity); + + foreach ($inverseSideReflection->getProperties() as $inverseSideProperty) { + $oneToManyAssociation = $this->reader->getPropertyAnnotation( + $inverseSideProperty, + 'Doctrine\ORM\Mapping\ManyToOne' + ); + + if ($oneToManyAssociation !== null && + $oneToManyAssociation->targetEntity == $owningSideReflection->getName() + ) { + return $inverseSideProperty->getName(); } } + + return false; } } diff --git a/src/PartKeepr/UploadedFileBundle/Services/UploadedFileService.php b/src/PartKeepr/UploadedFileBundle/Services/UploadedFileService.php @@ -39,18 +39,12 @@ class UploadedFileService extends ContainerAware */ public function replaceFromFilesystem(UploadedFile $file, File $filesystemFile) { - $oldFile = $this->getFullPath($file); - $file->setOriginalFilename($filesystemFile->getBasename()); $file->setExtension($filesystemFile->getExtension()); $file->setMimeType($filesystemFile->getMimeType()); $file->setSize($filesystemFile->getSize()); copy($filesystemFile->getPathname(), $this->getFullPath($file)); - - if (file_exists($oldFile)) { - unlink($oldFile); - } } public function replaceFromData(UploadedFile $file, $data, $filename)