From 1fd901ae2f10cd6800735f8a53cc2bed2916fe05 Mon Sep 17 00:00:00 2001 From: smarcet Date: Mon, 28 Jun 2021 17:21:00 -0300 Subject: [PATCH] Added Image to BadgeFeature Change-Id: I0ea27dea92535c0cdbfeb5aa264f36159e2f4034 Signed-off-by: smarcet --- ...th2SummitBadgeFeatureTypeApiController.php | 71 +++++++++ app/Http/routes.php | 4 + .../SummitBadgeFeatureTypeSerializer.php | 1 + .../Foundation/Summit/Events/SummitEvent.php | 49 +----- .../Registration/SummitBadgeFeatureType.php | 13 ++ app/Models/Utils/Traits/HasImageTrait.php | 68 +++++++++ .../Model/ISummitBadgeFeatureTypeService.php | 27 ++++ .../Imp/SummitBadgeFeatureTypeService.php | 84 ++++++++++- .../model/Version20210628184207.php | 55 +++++++ database/seeds/ApiEndpointsSeeder.php | 26 ++++ tests/OAuth2SummitBadgeFeatureTypeApiTest.php | 139 ++++++++++++++++-- 11 files changed, 478 insertions(+), 59 deletions(-) create mode 100644 app/Models/Utils/Traits/HasImageTrait.php create mode 100644 database/migrations/model/Version20210628184207.php diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitBadgeFeatureTypeApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitBadgeFeatureTypeApiController.php index 27078711..e2bfadae 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitBadgeFeatureTypeApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitBadgeFeatureTypeApiController.php @@ -13,11 +13,17 @@ **/ use App\Models\Foundation\Summit\Repositories\ISummitBadgeFeatureTypeRepository; use App\Services\Model\ISummitBadgeFeatureTypeService; +use Illuminate\Http\Request as LaravelRequest; +use Illuminate\Support\Facades\Log; +use models\exceptions\EntityNotFoundException; +use models\exceptions\ValidationException; use models\oauth2\IResourceServerContext; use models\summit\ISummitRepository; use models\summit\Summit; use models\utils\IBaseRepository; use models\utils\IEntity; +use ModelSerializers\SerializerRegistry; +use Exception; /** * Class OAuth2SummitBadgeFeatureTypeApiController * @package App\Http\Controllers @@ -178,4 +184,69 @@ final class OAuth2SummitBadgeFeatureTypeApiController { return $this->service->updateBadgeFeatureType($summit, $child_id, $payload); } + + /** + * @param LaravelRequest $request + * @param $summit_id + * @param $feature_id + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function addFeatureImage(LaravelRequest $request, $summit_id, $feature_id){ + try { + $summit = SummitFinderStrategyFactory::build($this->getSummitRepository(), $this->resource_server_context)->find($summit_id); + if (is_null($summit)) return $this->error404(); + + $file = $request->file('file'); + if (is_null($file)) { + return $this->error412(array('file param not set!')); + } + + $image = $this->service->addFeatureImage($summit, $feature_id, $file); + + return $this->created(SerializerRegistry::getInstance()->getSerializer($image)->serialize()); + + } + catch (ValidationException $ex1) + { + Log::warning($ex1); + return $this->error412(array($ex1->getMessage())); + } + catch(EntityNotFoundException $ex2) + { + Log::warning($ex2); + return $this->error404(array('message'=> $ex2->getMessage())); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } + + /** + * @param $summit_id + * @param $feature_id + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function deleteFeatureImage($summit_id, $feature_id) { + try { + $summit = SummitFinderStrategyFactory::build($this->getSummitRepository(), $this->resource_server_context)->find($summit_id); + if (is_null($summit)) return $this->error404(); + $this->service->removeFeatureImage($summit, $feature_id); + return $this->deleted(); + } + catch (ValidationException $ex1) + { + Log::warning($ex1); + return $this->error412(array($ex1->getMessage())); + } + catch(EntityNotFoundException $ex2) + { + Log::warning($ex2); + return $this->error404(array('message'=> $ex2->getMessage())); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } } \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index fa28f05b..48669ec0 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -858,6 +858,10 @@ Route::group([ Route::get('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitBadgeFeatureTypeApiController@get']); Route::put('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitBadgeFeatureTypeApiController@update']); Route::delete('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitBadgeFeatureTypeApiController@delete']); + Route::group(['prefix' => 'image'], function () { + Route::post('', [ 'middleware' => 'auth.user', 'uses' => 'OAuth2SummitBadgeFeatureTypeApiController@addFeatureImage']); + Route::delete('', [ 'middleware' => 'auth.user', 'uses' => 'OAuth2SummitBadgeFeatureTypeApiController@deleteFeatureImage']); + }); }); }); diff --git a/app/ModelSerializers/Summit/Registration/SummitBadgeFeatureTypeSerializer.php b/app/ModelSerializers/Summit/Registration/SummitBadgeFeatureTypeSerializer.php index f1290030..906fbcde 100644 --- a/app/ModelSerializers/Summit/Registration/SummitBadgeFeatureTypeSerializer.php +++ b/app/ModelSerializers/Summit/Registration/SummitBadgeFeatureTypeSerializer.php @@ -23,5 +23,6 @@ final class SummitBadgeFeatureTypeSerializer extends SilverStripeSerializer 'Description' => 'description:json_string', 'TemplateContent' => 'template_content:json_string', 'SummitId' => 'summit_id:json_int', + 'ImageUrl' => 'image:json_url', ]; } \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Events/SummitEvent.php b/app/Models/Foundation/Summit/Events/SummitEvent.php index e6b2a280..dc474f8a 100644 --- a/app/Models/Foundation/Summit/Events/SummitEvent.php +++ b/app/Models/Foundation/Summit/Events/SummitEvent.php @@ -16,6 +16,7 @@ use App\Models\Foundation\Summit\Events\RSVP\RSVPTemplate; use App\Events\SummitEventCreated; use App\Events\SummitEventDeleted; use App\Events\SummitEventUpdated; +use App\Models\Utils\Traits\HasImageTrait; use Doctrine\Common\Collections\Criteria; use Doctrine\ORM\Event\PreUpdateEventArgs; use models\exceptions\ValidationException; @@ -1199,53 +1200,7 @@ class SummitEvent extends SilverstripeBaseModel return $this->attendance_metrics->matching($criteria)->toArray(); } - /** - * @return bool - */ - public function hasImage(){ - return $this->getImageId() > 0; - } - - /** - * @return int - */ - public function getImageId() - { - try{ - if(is_null($this->image)) return 0; - return $this->image->getId(); - } - catch(\Exception $ex){ - return 0; - } - } - - public function getImage():?File{ - return $this->image; - } - - /** - * @return string|null - */ - public function getImageUrl():?string{ - $photoUrl = null; - if($this->hasImage() && $photo = $this->getImage()){ - $photoUrl = $photo->getUrl(); - } - return $photoUrl; - } - - /** - * @param File $image - */ - public function setImage(File $image): void - { - $this->image = $image; - } - - public function clearImage():void{ - $this->image = null; - } + use HasImageTrait; /** * @param Member|null $member diff --git a/app/Models/Foundation/Summit/Registration/SummitBadgeFeatureType.php b/app/Models/Foundation/Summit/Registration/SummitBadgeFeatureType.php index e5c85933..5a1f52d5 100644 --- a/app/Models/Foundation/Summit/Registration/SummitBadgeFeatureType.php +++ b/app/Models/Foundation/Summit/Registration/SummitBadgeFeatureType.php @@ -11,6 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use App\Models\Utils\Traits\HasImageTrait; +use models\main\File; use models\utils\SilverstripeBaseModel; use Doctrine\ORM\Mapping AS ORM; use Doctrine\Common\Collections\ArrayCollection; @@ -48,6 +51,13 @@ class SummitBadgeFeatureType extends SilverstripeBaseModel */ private $template_content; + /** + * @ORM\ManyToOne(targetEntity="models\main\File", cascade={"persist"}) + * @ORM\JoinColumn(name="ImageID", referencedColumnName="ID") + * @var File + */ + private $image; + /** * @return string */ @@ -101,4 +111,7 @@ class SummitBadgeFeatureType extends SilverstripeBaseModel parent::__construct(); $this->template_content = ''; } + + use HasImageTrait; + } \ No newline at end of file diff --git a/app/Models/Utils/Traits/HasImageTrait.php b/app/Models/Utils/Traits/HasImageTrait.php new file mode 100644 index 00000000..41f8b5f5 --- /dev/null +++ b/app/Models/Utils/Traits/HasImageTrait.php @@ -0,0 +1,68 @@ +image = $image; + } + + public function clearImage():void{ + $this->image = null; + } + + /** + * @return bool + */ + public function hasImage():bool{ + return $this->getImageId() > 0; + } + + /** + * @return int + */ + public function getImageId():int + { + try{ + if(is_null($this->image)) return 0; + return $this->image->getId(); + } + catch(\Exception $ex){ + return 0; + } + } + + public function getImage():?File{ + return $this->image; + } + + /** + * @return string|null + */ + public function getImageUrl():?string{ + $photoUrl = null; + if($this->hasImage() && $photo = $this->getImage()){ + $photoUrl = $photo->getUrl(); + } + return $photoUrl; + } +} \ No newline at end of file diff --git a/app/Services/Model/ISummitBadgeFeatureTypeService.php b/app/Services/Model/ISummitBadgeFeatureTypeService.php index a5e2be99..cd9fedac 100644 --- a/app/Services/Model/ISummitBadgeFeatureTypeService.php +++ b/app/Services/Model/ISummitBadgeFeatureTypeService.php @@ -11,8 +11,11 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use Illuminate\Http\UploadedFile; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; +use models\main\File; use models\summit\Summit; use models\summit\SummitBadgeFeatureType; /** @@ -47,4 +50,28 @@ interface ISummitBadgeFeatureTypeService * @throws EntityNotFoundException */ public function deleteBadgeFeatureType(Summit $summit, int $feature_id):void; + + /** + * @param Summit $summit + * @param int $feature_id + * @param UploadedFile $file + * @param int $max_file_size + * @return File + * @throws EntityNotFoundException + * @throws ValidationException + */ + public function addFeatureImage + ( + Summit $summit, + int $feature_id, + UploadedFile $file, + int $max_file_size = 10485760 + ):File; + + /** + * @param Summit $summit + * @param int $feature_id + * @throws EntityNotFoundException + */ + public function removeFeatureImage(Summit $summit, int $feature_id): void; } \ No newline at end of file diff --git a/app/Services/Model/Imp/SummitBadgeFeatureTypeService.php b/app/Services/Model/Imp/SummitBadgeFeatureTypeService.php index 1167c0d4..92125487 100644 --- a/app/Services/Model/Imp/SummitBadgeFeatureTypeService.php +++ b/app/Services/Model/Imp/SummitBadgeFeatureTypeService.php @@ -11,12 +11,16 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + +use App\Http\Utils\IFileUploader; use App\Models\Foundation\Summit\Factories\SummitBadgeFeatureTypeFactory; use libs\utils\ITransactionService; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; +use models\main\File; use models\summit\Summit; use models\summit\SummitBadgeFeatureType; +use Illuminate\Http\UploadedFile; /** * Class SummitBadgeFeatureTypeService * @package App\Services\Model @@ -25,9 +29,24 @@ final class SummitBadgeFeatureTypeService extends AbstractService implements ISummitBadgeFeatureTypeService { - public function __construct(ITransactionService $tx_service) + /** + * @var IFileUploader + */ + private $file_uploader; + + /** + * SummitBadgeFeatureTypeService constructor. + * @param IFileUploader $file_uploader + * @param ITransactionService $tx_service + */ + public function __construct + ( + IFileUploader $file_uploader, + ITransactionService $tx_service + ) { parent::__construct($tx_service); + $this->file_uploader = $file_uploader; } /** @@ -101,4 +120,67 @@ implements ISummitBadgeFeatureTypeService }); } + + /** + * @param Summit $summit + * @param int $feature_id + * @param UploadedFile $file + * @param int $max_file_size + * @return File + * @throws EntityNotFoundException + * @throws ValidationException + */ + public function addFeatureImage + ( + Summit $summit, + int $feature_id, + UploadedFile $file, + int $max_file_size = 10485760 + ):File + { + return $this->tx_service->transaction(function () use ($summit, $feature_id, $file, $max_file_size) { + + $allowed_extensions = ['png', 'jpg', 'jpeg', 'gif', 'svg']; + + $feature = $summit->getFeatureTypeById($feature_id); + + if (is_null($feature) || !$feature instanceof SummitBadgeFeatureType) { + throw new EntityNotFoundException('feature type not found on summit!'); + } + + if (!in_array($file->extension(), $allowed_extensions)) { + throw new ValidationException("file does not has a valid extension ('png','jpg','jpeg','gif','pdf')."); + } + + if ($file->getSize() > $max_file_size) { + throw new ValidationException(sprintf("file exceeds max_file_size (%s MB).", ($max_file_size / 1024) / 1024)); + } + + $file = $this->file_uploader->build($file, 'summit-event-images', true); + $feature->setImage($file); + + return $file; + }); + } + + /** + * @param Summit $summit + * @param int $feature_id + * @throws EntityNotFoundException + */ + public function removeFeatureImage(Summit $summit, int $feature_id): void + { + $this->tx_service->transaction(function () use ($summit, $feature_id) { + + $feature = $summit->getFeatureTypeById($feature_id); + + if (is_null($feature) || !$feature instanceof SummitBadgeFeatureType) { + throw new EntityNotFoundException('feature type not found on summit!'); + } + + $feature->clearImage(); + + }); + } + } \ No newline at end of file diff --git a/database/migrations/model/Version20210628184207.php b/database/migrations/model/Version20210628184207.php new file mode 100644 index 00000000..905e2596 --- /dev/null +++ b/database/migrations/model/Version20210628184207.php @@ -0,0 +1,55 @@ +hasTable("SummitBadgeFeatureType") && !$builder->hasColumn("SummitBadgeFeatureType", "ImageID")) { + $builder->table('SummitBadgeFeatureType', function (Table $table) { + + $table->integer("ImageID", false, false)->setNotnull(false)->setDefault('NULL'); + $table->index("ImageID", "ImageID"); + $table->foreign("File", "ImageID", "ID", ["onDelete" => "CASCADE"]); + + }); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema):Void + { + $builder = new Builder($schema); + + if($schema->hasTable("SummitBadgeFeatureType") && $builder->hasColumn("SummitBadgeFeatureType", "ImageID")) { + $builder->table('SummitBadgeFeatureType', function (Table $table) { + $table->dropColumn("ImageID"); + }); + } + } +} diff --git a/database/seeds/ApiEndpointsSeeder.php b/database/seeds/ApiEndpointsSeeder.php index 11d7acd3..b4d3ee61 100644 --- a/database/seeds/ApiEndpointsSeeder.php +++ b/database/seeds/ApiEndpointsSeeder.php @@ -1346,6 +1346,32 @@ class ApiEndpointsSeeder extends Seeder IGroup::SummitAdministrators, ] ], + [ + 'name' => 'add-feature-type-image', + 'route' => '/api/v1/summits/{id}/badge-feature-types/{feature_id}/image', + 'http_method' => 'POST', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + ] + ], + [ + 'name' => 'delete-feature-type-image', + 'route' => '/api/v1/summits/{id}/badge-feature-types/{feature_id}/image', + 'http_method' => 'DELETE', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + ] + ], // refund-policies [ 'name' => 'get-refund-policies', diff --git a/tests/OAuth2SummitBadgeFeatureTypeApiTest.php b/tests/OAuth2SummitBadgeFeatureTypeApiTest.php index b7200951..74e4414c 100644 --- a/tests/OAuth2SummitBadgeFeatureTypeApiTest.php +++ b/tests/OAuth2SummitBadgeFeatureTypeApiTest.php @@ -11,18 +11,53 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ +use Illuminate\Http\UploadedFile; - +/** + * Class OAuth2SummitBadgeFeatureTypeApiTest + */ final class OAuth2SummitBadgeFeatureTypeApiTest extends ProtectedApiTest { + use InsertSummitTestData; + + public function createApplication() + { + $app = parent::createApplication(); + + $fileUploaderMock = Mockery::mock(\App\Http\Utils\IFileUploader::class) + ->shouldIgnoreMissing(); + + $fileUploaderMock->shouldReceive('build')->andReturn(new \models\main\File()); + + $app->instance(\App\Http\Utils\IFileUploader::class, $fileUploaderMock); + + return $app; + } + + protected function setUp():void + { + parent::setUp(); + self::insertTestData(); + } + + protected function tearDown():void + { + self::clearTestData(); + parent::tearDown(); + } + + public function testAddBadgeFeatureType(){ + return $this->_testAddBadgeFeatureType(); + } + /** * @param int $summit_id * @return mixed */ - public function testAddBadgeFeatureType($summit_id = 27){ + protected function _testAddBadgeFeatureType(){ $params = [ - 'id' => $summit_id, + 'id' => self::$summit->getId(), ]; $name = str_random(16).'_feature_type'; @@ -96,11 +131,11 @@ HTML; return $feature; } - public function testUpdateBadgeFeatureType($summit_id = 27){ + public function testUpdateBadgeFeatureType(){ - $feature_old = $this->testAddBadgeFeatureType(); + $feature_old = $this->_testAddBadgeFeatureType(); $params = [ - 'id' => $summit_id, + 'id' => self::$summit->getId(), "feature_id" => $feature_old->id ]; @@ -133,10 +168,14 @@ HTML; return $feature; } + public function testGetAllBySummit(){ + + $this->_testAddBadgeFeatureType(); + $this->_testAddBadgeFeatureType(); + $this->_testAddBadgeFeatureType(); - public function testGetAllBySummit($summit_id=27){ $params = [ - 'id' => $summit_id, + 'id' => self::$summit->getId(), ]; $headers = [ @@ -158,16 +197,17 @@ HTML; $this->assertResponseStatus(200); $data = json_decode($content); $this->assertTrue(!is_null($data)); + $this->assertTrue($data->total == 3); return $data; } /** * @param int $summit_id */ - public function testDeleteAccessLevel($summit_id=27){ - $feature_old = $this->testAddBadgeFeatureType(); + public function testDeleteFeature(){ + $feature_old = $this->_testAddBadgeFeatureType(); $params = [ - 'id' => $summit_id, + 'id' => self::$summit->getId(), "feature_id" => $feature_old->id ]; @@ -189,4 +229,81 @@ HTML; $content = $response->getContent(); $this->assertResponseStatus(204); } + + public function testAddFeatureImage(){ + + $feature_old = $this->_testAddBadgeFeatureType(); + + $params = [ + 'id' => self::$summit->getId(), + "feature_id" => $feature_old->id + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "POST", + "OAuth2SummitBadgeFeatureTypeApiController@addFeatureImage", + $params, + [], + [], + [ + 'file' => UploadedFile::fake()->image('feat.svg'), + ], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(201); + $file = json_decode($content); + $this->assertTrue(!is_null($file)); + } + + public function testDeleteFeatureImage(){ + + $feature_old = $this->_testAddBadgeFeatureType(); + + $params = [ + 'id' => self::$summit->getId(), + "feature_id" => $feature_old->id + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "POST", + "OAuth2SummitBadgeFeatureTypeApiController@addFeatureImage", + $params, + [], + [], + [ + 'file' => UploadedFile::fake()->image('feat.svg'), + ], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(201); + $file = json_decode($content); + $this->assertTrue(!is_null($file)); + + $response = $this->action( + "DELETE", + "OAuth2SummitBadgeFeatureTypeApiController@deleteFeatureImage", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(204); + } } \ No newline at end of file