From 0d836a1b90def0a4ef1314d2c45d3dd7610bcfc1 Mon Sep 17 00:00:00 2001 From: smarcet Date: Wed, 31 Mar 2021 18:43:24 -0300 Subject: [PATCH] Summit Presentation Actions Types Updates to Summit Serializer * added new collection presentation_action_types Presentation Action Types GET /api/v1/summits/{id}/presentation-action-types filtering 'label' => ['=@', '=='], ordering 'id', 'order', 'label', required scopes %s/summits/read %s/summits/read/all auth groups * SuperAdmins * Administrators * SummitAdministrators * TrackChairsAdmins GET /api/v1/summits/{id}/presentation-action-types/csv filtering 'name' => ['=@', '=='], 'label' => ['=@', '=='], 'is_enabled' => ['=='], ordering 'id', 'order', 'label', required scopes %s/summits/read %s/summits/read/all auth groups * SuperAdmins * Administrators * SummitAdministrators * TrackChairsAdmins POST /api/v1/summits/{id}/presentation-action-types payload 'label' => 'required|string|max:255', required scopes %s/summits/write auth groups * SuperAdmins * Administrators * SummitAdministrators * TrackChairsAdmins PUT /api/v1/summits/{id}/presentation-action-types/{action_id} payload 'label' => 'sometimes|string|max:255', 'order' => 'sometimes|integer|min:1', required scopes %s/summits/write auth groups * SuperAdmins * Administrators * SummitAdministrators * TrackChairsAdmins GET /api/v1/summits/{id}/presentation-action-types/{action_id} required scopes %s/summits/read %s/summits/read/all auth groups * SuperAdmins * Administrators * SummitAdministrators * TrackChairsAdmins DELETE /api/v1/summits/{id}/presentation-action-types/{action_id} required scopes %s/summits/write auth groups * SuperAdmins * Administrators * SummitAdministrators * TrackChairsAdmins Change-Id: Ibec7591b88470b59d9f942a553a8335a57e9db9e Signed-off-by: smarcet --- app/Events/PresentationActionTypeCreated.php | 41 +++ ...tationActionTypeValidationRulesFactory.php | 40 +++ ...mitPresentationActionTypeApiController.php | 251 ++++++++++++++++ app/Http/routes.php | 13 + app/Jobs/SynchAllPresentationActions.php | 68 +++++ app/ModelSerializers/SerializerRegistry.php | 2 + .../PresentationActionTypeSerializer.php | 60 ++++ .../Summit/SummitSerializer.php | 10 + .../Actions/PresentationAction.php | 147 +++++++++ .../Actions/PresentationActionType.php | 96 ++++++ .../Events/Presentations/Presentation.php | 75 ++++- .../PresentationActionTypeFactory.php | 41 +++ .../IPresentationActionTypeRepository.php | 23 ++ app/Models/Foundation/Summit/Summit.php | 74 ++++- app/Providers/EventServiceProvider.php | 11 + app/Repositories/RepositoriesProvider.php | 11 +- ...ctrinePresentationActionTypeRepository.php | 59 ++++ .../ISummitPresentationActionTypeService.php | 50 ++++ .../SummitPresentationActionTypeService.php | 98 ++++++ app/Services/ModelServicesProvider.php | 8 + .../model/Version20210326171114.php | 94 ++++++ .../model/Version20210326171117.php | 52 ++++ database/seeds/ApiEndpointsSeeder.php | 87 ++++++ database/seeds/TestSeeder.php | 11 +- ...th2SummitPresentationActionTypeApiTest.php | 282 ++++++++++++++++++ 25 files changed, 1692 insertions(+), 12 deletions(-) create mode 100644 app/Events/PresentationActionTypeCreated.php create mode 100644 app/Http/Controllers/Apis/Protected/Summit/Factories/SummitPresentationActionTypeValidationRulesFactory.php create mode 100644 app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitPresentationActionTypeApiController.php create mode 100644 app/Jobs/SynchAllPresentationActions.php create mode 100644 app/ModelSerializers/Summit/Presentation/PresentationActionTypeSerializer.php create mode 100644 app/Models/Foundation/Summit/Events/Presentations/Actions/PresentationAction.php create mode 100644 app/Models/Foundation/Summit/Events/Presentations/Actions/PresentationActionType.php create mode 100644 app/Models/Foundation/Summit/Factories/PresentationActionTypeFactory.php create mode 100644 app/Models/Foundation/Summit/Repositories/IPresentationActionTypeRepository.php create mode 100644 app/Repositories/Summit/DoctrinePresentationActionTypeRepository.php create mode 100644 app/Services/Model/ISummitPresentationActionTypeService.php create mode 100644 app/Services/Model/Imp/SummitPresentationActionTypeService.php create mode 100644 database/migrations/model/Version20210326171114.php create mode 100644 database/migrations/model/Version20210326171117.php create mode 100644 tests/OAuth2SummitPresentationActionTypeApiTest.php diff --git a/app/Events/PresentationActionTypeCreated.php b/app/Events/PresentationActionTypeCreated.php new file mode 100644 index 00000000..00c10fe8 --- /dev/null +++ b/app/Events/PresentationActionTypeCreated.php @@ -0,0 +1,41 @@ +action_type = $action_type; + } + + public function getActionType():PresentationActionType{ + return $this->action_type; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitPresentationActionTypeValidationRulesFactory.php b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitPresentationActionTypeValidationRulesFactory.php new file mode 100644 index 00000000..3a2e4e46 --- /dev/null +++ b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitPresentationActionTypeValidationRulesFactory.php @@ -0,0 +1,40 @@ + 'sometimes|string|max:255', + 'order' => 'sometimes|integer|min:1', + ]; + } + + return [ + 'label' => 'required|string|max:255', + ]; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitPresentationActionTypeApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitPresentationActionTypeApiController.php new file mode 100644 index 00000000..9daabb3e --- /dev/null +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitPresentationActionTypeApiController.php @@ -0,0 +1,251 @@ +repository = $repository; + $this->summit_repository = $summit_repository; + $this->service = $service; + } + + use ParametrizedGetAll; + + use GetSummitChildElementById; + + use AddSummitChildElement; + + use UpdateSummitChildElement; + + use DeleteSummitChildElement; + + /** + * @inheritDoc + */ + protected function addChild(Summit $summit, array $payload): IEntity + { + return $this->service->add($summit, $payload); + } + + /** + * @inheritDoc + */ + function getAddValidationRules(array $payload): array + { + return SummitPresentationActionTypeValidationRulesFactory::build($payload, false); + } + + /** + * @inheritDoc + */ + protected function getSummitRepository(): ISummitRepository + { + return $this->summit_repository; + } + + /** + * @inheritDoc + */ + protected function deleteChild(Summit $summit, $child_id): void + { + $this->service->delete($summit, $child_id); + } + + /** + * @inheritDoc + */ + protected function getChildFromSummit(Summit $summit, $child_id): ?IEntity + { + return $summit->getPresentationActionTypeById($child_id); + } + + /** + * @inheritDoc + */ + function getUpdateValidationRules(array $payload): array + { + return SummitPresentationActionTypeValidationRulesFactory::build($payload, true); + } + + /** + * @inheritDoc + */ + protected function updateChild(Summit $summit, int $child_id, array $payload): IEntity + { + return $this->service->update($summit, $child_id, $payload); + } + + /** + * @param $summit_id + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function getAllBySummit($summit_id) + { + $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->getResourceServerContext())->find($summit_id); + if (is_null($summit)) return $this->error404(); + + return $this->_getAll( + function () { + return [ + 'name' => ['=@', '=='], + 'label' => ['=@', '=='], + 'is_enabled' => ['=='], + ]; + }, + function () { + return [ + 'name' => 'sometimes|string', + 'label' => 'sometimes|string', + 'is_enabled' => 'sometimes|boolean', + ]; + }, + function () { + return [ + 'id', + 'name', + 'order', + 'label', + 'is_enabled' + ]; + }, + function ($filter) use ($summit) { + if ($filter instanceof Filter) { + $filter->addFilterCondition(FilterElement::makeEqual('summit_id', $summit->getId())); + } + return $filter; + }, + function () { + return SerializerRegistry::SerializerType_Public; + } + ); + } + + /** + * @param $summit_id + * @return \Illuminate\Http\JsonResponse|mixed + */ + public function getAllBySummitCSV($summit_id) + { + $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->getResourceServerContext())->find($summit_id); + if (is_null($summit)) return $this->error404(); + + return $this->_getAllCSV( + function () { + return [ + 'name' => ['=@', '=='], + 'label' => ['=@', '=='], + 'is_enabled' => ['=='], + ]; + }, + function () { + return [ + 'name' => 'sometimes|string', + 'label' => 'sometimes|string', + 'is_enabled' => 'sometimes|boolean', + ]; + }, + function () { + return [ + 'id', + 'name', + 'order', + 'label', + 'is_enabled' + ]; + }, + function ($filter) use ($summit) { + if ($filter instanceof Filter) { + $filter->addFilterCondition(FilterElement::makeEqual('summit_id', $summit->getId())); + } + return $filter; + }, + function () { + return SerializerRegistry::SerializerType_Public; + }, + function () { + return [ + 'created' => new EpochCellFormatter(), + 'last_edited' => new EpochCellFormatter(), + ]; + }, + function () use ($summit) { + $allowed_columns = [ + 'id', + 'created', + 'last_edited', + 'name', + 'label', + 'is_enabled', + 'order', + ]; + + $columns_param = Input::get("columns", ""); + $columns = []; + if (!empty($columns_param)) + $columns = explode(',', $columns_param); + $diff = array_diff($columns, $allowed_columns); + if (count($diff) > 0) { + throw new ValidationException(sprintf("columns %s are not allowed!", implode(",", $diff))); + } + if (empty($columns)) + $columns = $allowed_columns; + return $columns; + }, + sprintf('summit_presentation_action_types-%s', $summit_id) + ); + } +} \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index 2676e617..a90a368b 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -1362,6 +1362,19 @@ Route::group([ Route::delete('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitApiController@removeFeatureSpeaker']); }); }); + + // presentation action types + + Route::group(['prefix' => 'presentation-action-types'], function(){ + Route::get('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitPresentationActionTypeApiController@getAllBySummit']); + Route::get('csv', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitPresentationActionTypeApiController@getAllBySummitCSV']); + Route::post('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitPresentationActionTypeApiController@add']); + Route::group(['prefix' => '{action_id}'], function() { + Route::get('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitPresentationActionTypeApiController@get']); + Route::put('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitPresentationActionTypeApiController@update']); + Route::delete('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitPresentationActionTypeApiController@delete']); + }); + }); }); }); diff --git a/app/Jobs/SynchAllPresentationActions.php b/app/Jobs/SynchAllPresentationActions.php new file mode 100644 index 00000000..422d2aa2 --- /dev/null +++ b/app/Jobs/SynchAllPresentationActions.php @@ -0,0 +1,68 @@ +summit_id = $summit_id; + } + + /** + * @param ISummitRepository $repository + * @param ITransactionService $tx_service + * @throws \Exception + */ + public function handle( + ISummitRepository $repository, + ITransactionService $tx_service + ) + { + Log::debug(sprintf("SynchAllPresentationActions::handle summit %s", $this->summit_id)); + $tx_service->transaction(function() use($repository){ + $summit = $repository->getById($this->summit_id); + if(is_null($summit)) + throw new EntityNotFoundException(sprintf("Summit %s not found", $this->summit_id)); + + $summit->synchAllPresentationActions(); + }); + } +} \ No newline at end of file diff --git a/app/ModelSerializers/SerializerRegistry.php b/app/ModelSerializers/SerializerRegistry.php index d3d22e97..3a478dc3 100644 --- a/app/ModelSerializers/SerializerRegistry.php +++ b/app/ModelSerializers/SerializerRegistry.php @@ -203,6 +203,8 @@ final class SerializerRegistry $this->registry['SummitCategoryChange'] = SummitCategoryChangeSerializer::class; + $this->registry['PresentationActionType'] = PresentationActionTypeSerializer::class; + // track chairs $this->registry['PresentationTrackChairView'] = PresentationTrackChairViewSerializer::class; $this->registry['SummitSelectedPresentationList'] = SummitSelectedPresentationListSerializer::class; diff --git a/app/ModelSerializers/Summit/Presentation/PresentationActionTypeSerializer.php b/app/ModelSerializers/Summit/Presentation/PresentationActionTypeSerializer.php new file mode 100644 index 00000000..f7fcaca4 --- /dev/null +++ b/app/ModelSerializers/Summit/Presentation/PresentationActionTypeSerializer.php @@ -0,0 +1,60 @@ + 'name:json_string', + 'Label' => 'label:json_string', + 'SummitId' => 'summit_id:json_int', + 'Order' => 'order:json_int', + 'Active' => 'is_active:json_boolean', + ]; + + /** + * @param null $expand + * @param array $fields + * @param array $relations + * @param array $params + * @return array + */ + public function serialize($expand = null, array $fields = array(), array $relations = array(), array $params = array()) + { + $action = $this->object; + if (!$action instanceof PresentationActionType) return []; + $values = parent::serialize($expand, $fields, $relations, $params); + if (!empty($expand)) { + $exp_expand = explode(',', $expand); + foreach ($exp_expand as $relation) { + switch (trim($relation)) { + case 'summit': + { + unset($values['summit_id']); + $values['summit'] = SerializerRegistry::getInstance()->getSerializer + ( + $action->getSummit() + )->serialize(AbstractSerializer::filterExpandByPrefix($expand, $relation)); + } + break; + } + } + } + return $values; + } +} \ No newline at end of file diff --git a/app/ModelSerializers/Summit/SummitSerializer.php b/app/ModelSerializers/Summit/SummitSerializer.php index 1dd44e5f..73a79ecc 100644 --- a/app/ModelSerializers/Summit/SummitSerializer.php +++ b/app/ModelSerializers/Summit/SummitSerializer.php @@ -115,6 +115,7 @@ class SummitSerializer extends SilverStripeSerializer 'summit_documents', 'featured_speakers', 'dates_with_events', + 'presentation_action_types', ]; /** @@ -312,6 +313,15 @@ class SummitSerializer extends SilverStripeSerializer $values['featured_speakers'] = $featured_speakers; } + // presentation_action_types + if (in_array('presentation_action_types', $relations)) { + $presentation_action_types = []; + foreach ($summit->getPresentationActionTypes() as $action) { + $presentation_action_types[] = SerializerRegistry::getInstance()->getSerializer($action)->serialize(AbstractSerializer::filterExpandByPrefix($expand, 'presentation_action_types')); + } + $values['presentation_action_types'] = $presentation_action_types; + } + if (!empty($expand)) { foreach (explode(',', $expand) as $relation) { diff --git a/app/Models/Foundation/Summit/Events/Presentations/Actions/PresentationAction.php b/app/Models/Foundation/Summit/Events/Presentations/Actions/PresentationAction.php new file mode 100644 index 00000000..308a2a9f --- /dev/null +++ b/app/Models/Foundation/Summit/Events/Presentations/Actions/PresentationAction.php @@ -0,0 +1,147 @@ +is_completed = false; + } + + /** + * @ORM\ManyToOne(targetEntity="Presentation", inversedBy="actions", fetch="EXTRA_LAZY") + * @ORM\JoinColumn(name="PresentationID", referencedColumnName="ID", onDelete="CASCADE") + * @var Presentation + */ + private $presentation; + + /** + * @ORM\ManyToOne(targetEntity="PresentationActionType", fetch="EXTRA_LAZY") + * @ORM\JoinColumn(name="TypeID", referencedColumnName="ID", onDelete="CASCADE") + * @var PresentationActionType + */ + private $type; + + /** + * @ORM\ManyToOne(targetEntity="models\main\Member", fetch="EXTRA_LAZY") + * @ORM\JoinColumn(name="CreatedByID", referencedColumnName="ID", onDelete="SET NULL") + * @var Member + */ + private $created_by; + + /** + * @ORM\ManyToOne(targetEntity="models\main\Member", fetch="EXTRA_LAZY") + * @ORM\JoinColumn(name="UpdateByID", referencedColumnName="ID", onDelete="SET NULL") + * @var Member + */ + private $updated_by; + + /** + * @return bool + */ + public function isCompleted(): bool + { + return $this->is_completed; + } + + /** + * @param bool $is_completed + */ + public function setIsCompleted(bool $is_completed): void + { + $this->is_completed = $is_completed; + } + + /** + * @return Presentation + */ + public function getPresentation(): Presentation + { + return $this->presentation; + } + + /** + * @param Presentation $presentation + */ + public function setPresentation(Presentation $presentation): void + { + $this->presentation = $presentation; + } + + /** + * @return PresentationActionType + */ + public function getType(): PresentationActionType + { + return $this->type; + } + + /** + * @param PresentationActionType $type + */ + public function setType(PresentationActionType $type): void + { + $this->type = $type; + } + + /** + * @return Member + */ + public function getCreatedBy(): ?Member + { + return $this->created_by; + } + + /** + * @param Member $created_by + */ + public function setCreatedBy(Member $created_by): void + { + $this->created_by = $created_by; + } + + /** + * @return Member + */ + public function getUpdatedBy(): ?Member + { + return $this->updated_by; + } + + /** + * @param Member $updated_by + */ + public function setUpdatedBy(Member $updated_by): void + { + $this->updated_by = $updated_by; + } + + +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Events/Presentations/Actions/PresentationActionType.php b/app/Models/Foundation/Summit/Events/Presentations/Actions/PresentationActionType.php new file mode 100644 index 00000000..81a7ba6e --- /dev/null +++ b/app/Models/Foundation/Summit/Events/Presentations/Actions/PresentationActionType.php @@ -0,0 +1,96 @@ +order; + } + + /** + * @param int $order + */ + public function setOrder($order): void + { + $this->order = $order; + } + + /** + * @return string + */ + public function getLabel(): string + { + return $this->label; + } + + /** + * @param string $label + */ + public function setLabel(string $label): void + { + $this->label = $label; + } + + + public function __construct() + { + parent::__construct(); + $this->order = 1; + } + + /** + * @ORM\PostPersist + */ + public function inserted($args) + { + Event::dispatch(new PresentationActionTypeCreated($this)); + } + +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Events/Presentations/Presentation.php b/app/Models/Foundation/Summit/Events/Presentations/Presentation.php index f6777c6f..c5d0bf84 100644 --- a/app/Models/Foundation/Summit/Events/Presentations/Presentation.php +++ b/app/Models/Foundation/Summit/Events/Presentations/Presentation.php @@ -12,17 +12,16 @@ * limitations under the License. **/ -use App\Models\Foundation\Main\OrderableChilds; -use Illuminate\Support\Arr; -use models\summit\PresentationTrackChairView; -use Behat\Transliterator\Transliterator; use Doctrine\ORM\Mapping AS ORM; +use App\Models\Foundation\Main\OrderableChilds; +use Behat\Transliterator\Transliterator; use App\Models\Foundation\Summit\Events\Presentations\TrackQuestions\TrackAnswer; use App\Models\Foundation\Summit\SelectionPlan; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\ArrayCollection; use App\Models\Foundation\Summit\Events\Presentations\TrackQuestions\TrackQuestionTemplate; use Illuminate\Support\Facades\Config; +use Illuminate\Support\Facades\Log; use models\exceptions\ValidationException; use models\main\Member; @@ -131,7 +130,6 @@ class Presentation extends SummitEvent */ protected $moderator; - /** * @ORM\ManyToOne(targetEntity="models\main\Member", fetch="EXTRA_LAZY") * @ORM\JoinColumn(name="CreatorID", referencedColumnName="ID", onDelete="SET NULL") @@ -202,6 +200,12 @@ class Presentation extends SummitEvent */ private $category_changes_requests; + /** + * @ORM\OneToMany(targetEntity="models\summit\PresentationAction", mappedBy="presentation", cascade={"persist", "remove"}, orphanRemoval=true, fetch="EXTRA_LAZY") + * @var PresentationAction[] + */ + private $actions; + /** * @return bool */ @@ -241,6 +245,7 @@ class Presentation extends SummitEvent $this->votes = new ArrayCollection(); $this->category_changes_requests = new ArrayCollection(); $this->selected_presentations = new ArrayCollection(); + $this->actions = new ArrayCollection(); $this->to_record = false; $this->attending_media = false; } @@ -1425,4 +1430,64 @@ class Presentation extends SummitEvent if(is_null($list) || !$list instanceof SummitSelectedPresentationList) return 0; return $list->getAvailableSlots(); } + + /** + * @param PresentationActionType $type + * @return bool + */ + public function hasActionByType(PresentationActionType $type):bool{ + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->eq('type', $type)); + return $this->actions->matching($criteria)->count() > 0; + } + + /** + * @param PresentationActionType $type + * @return PresentationAction|null + */ + public function getActionByType(PresentationActionType $type):?PresentationAction { + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->eq('type', $type)); + $res = $this->actions->matching($criteria)->first(); + return $res === false ? null : $res; + } + + /** + * @param bool $complete + * @param PresentationActionType $type + * @return PresentationAction|null + */ + public function setCompletionByType(bool $complete, PresentationActionType $type):?PresentationAction{ + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->eq('type', $type)); + $res = $this->actions->matching($criteria)->first(); + if($res === false) return null; + $res->setIsCompleted($complete); + return $res; + } + + public function initializeActions():void { + Log::debug(sprintf("Presentation::initializeActions presentation %s", $this->id)); + foreach ($this->summit->getPresentationActionTypes() as $presentationActionType){ + if(!$this->hasActionByType($presentationActionType)){ + // create it + Log::debug + ( + sprintf + ( + "Presentation::initializeActions creating new presentation action for type %s", + $presentationActionType->getName() + ) + ); + $action = new PresentationAction(); + $action->setType($presentationActionType); + $action->setPresentation($this); + $this->actions->add($action); + } + } + } + + public function getPresentationActions(){ + return $this->actions; + } } diff --git a/app/Models/Foundation/Summit/Factories/PresentationActionTypeFactory.php b/app/Models/Foundation/Summit/Factories/PresentationActionTypeFactory.php new file mode 100644 index 00000000..c5197428 --- /dev/null +++ b/app/Models/Foundation/Summit/Factories/PresentationActionTypeFactory.php @@ -0,0 +1,41 @@ +setLabel(trim($data['label'])); + + return $action; + } +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/Repositories/IPresentationActionTypeRepository.php b/app/Models/Foundation/Summit/Repositories/IPresentationActionTypeRepository.php new file mode 100644 index 00000000..90e8bd1d --- /dev/null +++ b/app/Models/Foundation/Summit/Repositories/IPresentationActionTypeRepository.php @@ -0,0 +1,23 @@ +media_upload_types = new ArrayCollection(); $this->featured_speakers = new ArrayCollection(); $this->metrics = new ArrayCollection(); - $this->track_chairs = new ArrayCollection(); + $this->track_chairs = new ArrayCollection(); + $this->presentation_action_types = new ArrayCollection(); } /** @@ -1349,6 +1354,9 @@ class Summit extends SilverstripeBaseModel return $this->events->matching($criteria); } + /** + * @return Presentation[] + */ public function getPresentations() { $query = $this->createQuery("SELECT p from models\summit\Presentation p JOIN p.summit s WHERE s.id = :summit_id"); @@ -5192,4 +5200,64 @@ SQL; if($member->isAdmin()) return true; return $this->hasPermissionOnSummit($member) && $member->isOnGroup(IGroup::TrackChairsAdmins); } + + /** + * @param PresentationActionType $presentation_action_type + * @return $this + */ + public function addPresentationActionType(PresentationActionType $presentation_action_type) + { + if($this->presentation_action_types->contains($presentation_action_type)) return $this; + $presentation_action_type->setOrder($this->getPresentationActionTypeMaxOrder() + 1); + $this->presentation_action_types->add($presentation_action_type); + $presentation_action_type->setSummit($this); + return $this; + } + + /** + * @return int + */ + public function getPresentationActionTypeMaxOrder():int{ + $criteria = Criteria::create(); + $criteria->orderBy(['order' => 'DESC']); + $action = $this->presentation_action_types->matching($criteria)->first(); + return $action === false ? 0 : $action->getOrder(); + } + + /** + * @return ArrayCollection|\Doctrine\Common\Collections\Collection + */ + public function getPresentationActionTypes(){ + $criteria = Criteria::create(); + $criteria->orderBy(["order" => Criteria::ASC ]); + return $this->presentation_action_types->matching($criteria); + } + + /** + * @param PresentationActionType $presentation_action_type + * @return $this + */ + public function removePresentationActionType(PresentationActionType $presentation_action_type){ + if(!$this->presentation_action_types->contains($presentation_action_type)) return $this; + $this->presentation_action_types->removeElement($presentation_action_type); + $presentation_action_type->setSummit(null); + return $this; + } + + /** + * @param int $action_id + * @return PresentationActionType|null + */ + public function getPresentationActionTypeById(int $action_id):?PresentationActionType{ + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->eq('id', $action_id)); + $action = $this->presentation_action_types->matching($criteria)->first(); + return $action === false ? null: $action; + } + + public function synchAllPresentationActions():void{ + foreach ($this-$this->getPresentations() as $presentation){ + $presentation->initializeActions(); + } + } } diff --git a/app/Providers/EventServiceProvider.php b/app/Providers/EventServiceProvider.php index 6974c437..e829845a 100644 --- a/app/Providers/EventServiceProvider.php +++ b/app/Providers/EventServiceProvider.php @@ -18,6 +18,7 @@ use App\EntityPersisters\EntityEventPersister; use App\Events\NewMember; use App\Events\OrderDeleted; use App\Events\PaymentSummitRegistrationOrderConfirmed; +use App\Events\PresentationActionTypeCreated; use App\Events\RequestedSummitAttendeeTicketRefund; use App\Events\RequestedSummitOrderRefund; use App\Events\RSVPCreated; @@ -66,6 +67,7 @@ use App\Jobs\Emails\BookableRooms\BookableRoomReservationRefundRequestedOwnerEma use App\Jobs\Emails\BookableRooms\BookableRoomReservationCanceledEmail; use App\Jobs\Emails\Schedule\RSVPRegularSeatMail; use App\Jobs\Emails\Schedule\RSVPWaitListSeatMail; +use App\Jobs\SynchAllPresentationActions; use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Event; @@ -601,5 +603,14 @@ final class EventServiceProvider extends ServiceProvider } }); + + Event::listen(PresentationActionTypeCreated::class, function($event){ + + if(!$event instanceof PresentationActionTypeCreated) return; + + $summit = $event->getActionType()->getSummit(); + + SynchAllPresentationActions::dispatch($summit->getId()); + }); } } diff --git a/app/Repositories/RepositoriesProvider.php b/app/Repositories/RepositoriesProvider.php index 30f2e603..5caefbd6 100644 --- a/app/Repositories/RepositoriesProvider.php +++ b/app/Repositories/RepositoriesProvider.php @@ -28,6 +28,7 @@ use App\Models\Foundation\Summit\Locations\Banners\SummitLocationBanner; use App\Models\Foundation\Summit\Repositories\IDefaultSummitEventTypeRepository; use App\Models\Foundation\Summit\Repositories\IDefaultTrackTagGroupRepository; use App\Models\Foundation\Summit\Repositories\IPaymentGatewayProfileRepository; +use App\Models\Foundation\Summit\Repositories\IPresentationActionTypeRepository; use App\Models\Foundation\Summit\Repositories\IPresentationCategoryGroupRepository; use App\Models\Foundation\Summit\Repositories\IPresentationSpeakerSummitAssistanceConfirmationRequestRepository; use App\Models\Foundation\Summit\Repositories\IRSVPTemplateRepository; @@ -67,7 +68,6 @@ use App\Models\Foundation\Summit\SelectionPlan; use App\Models\Foundation\Summit\Speakers\SpeakerEditPermissionRequest; use App\Models\Foundation\Summit\TrackTagGroupAllowedTag; use App\Models\ResourceServer\IApiRepository; -use App\Repositories\Summit\DoctrineSummitTrackChairRepository; use Illuminate\Support\Facades\App; use Illuminate\Support\ServiceProvider; use LaravelDoctrine\ORM\Facades\EntityManager; @@ -85,6 +85,7 @@ use models\summit\ISponsorUserInfoGrantRepository; use models\summit\ISummitRegistrationPromoCodeRepository; use models\summit\ISummitTicketTypeRepository; use models\summit\PaymentGatewayProfile; +use models\summit\PresentationActionType; use models\summit\PresentationCategory; use models\summit\PresentationCategoryGroup; use models\summit\PresentationSpeakerSummitAssistanceConfirmationRequest; @@ -119,7 +120,6 @@ use models\summit\SummitTaxType; use models\summit\SummitTicketType; use models\summit\SummitTrackChair; use repositories\main\DoctrineLegalDocumentRepository; - /** * Class RepositoriesProvider * @package repositories @@ -662,5 +662,12 @@ final class RepositoriesProvider extends ServiceProvider return EntityManager::getRepository(SummitCategoryChange::class); } ); + + App::singleton( + IPresentationActionTypeRepository::class, + function(){ + return EntityManager::getRepository(PresentationActionType::class); + } + ); } } \ No newline at end of file diff --git a/app/Repositories/Summit/DoctrinePresentationActionTypeRepository.php b/app/Repositories/Summit/DoctrinePresentationActionTypeRepository.php new file mode 100644 index 00000000..1776b028 --- /dev/null +++ b/app/Repositories/Summit/DoctrinePresentationActionTypeRepository.php @@ -0,0 +1,59 @@ + 'e.label:json_string', + 'summit_id' => new DoctrineLeftJoinFilterMapping("e.summit", "s" ,"s.id :operator :value") + ]; + } + + /** + * @return array + */ + protected function getOrderMappings() + { + return [ + 'id' => 'e.id', + 'order' => 'e.order', + 'label' => 'e.label', + ]; + } +} \ No newline at end of file diff --git a/app/Services/Model/ISummitPresentationActionTypeService.php b/app/Services/Model/ISummitPresentationActionTypeService.php new file mode 100644 index 00000000..fd3ee4ab --- /dev/null +++ b/app/Services/Model/ISummitPresentationActionTypeService.php @@ -0,0 +1,50 @@ +repository = $repository; + } + + /** + * @inheritDoc + */ + public function add(Summit $summit, array $payload): PresentationActionType + { + return $this->tx_service->transaction(function() use($summit, $payload){ + $action = PresentationActionTypeFactory::build($payload); + $max_order = $summit->getPresentationActionTypeMaxOrder(); + $action->setOrder($max_order + 1); + $summit->addPresentationActionType($action); + return $action; + }); + } + + /** + * @inheritDoc + */ + public function update(Summit $summit, int $action_type_id, array $payload): ?PresentationActionType + { + return $this->tx_service->transaction(function() use($summit, $action_type_id, $payload){ + + $action = $summit->getPresentationActionTypeById($action_type_id); + if(is_null($action)){ + throw new EntityNotFoundException(sprintf("PresentationActionType %s not found.", $action_type_id)); + } + $action = PresentationActionTypeFactory::populate($action, $payload); + if (isset($payload['order']) && intval($payload['order']) != $action->getOrder()) { + // request to update order + self::recalculateOrderForCollection($summit->getPresentationActionTypes()->toArray(), $action, intval($payload['order'])); + } + return $action; + }); + } + + /** + * @inheritDoc + */ + public function delete(Summit $summit, int $action_type_id): void + { + $this->tx_service->transaction(function() use($summit, $action_type_id){ + $action = $summit->getPresentationActionTypeById($action_type_id); + if(is_null($action)){ + throw new EntityNotFoundException(sprintf("PresentationActionType %s not found.", $action_type_id)); + } + + $summit->removePresentationActionType($action); + }); + } +} \ No newline at end of file diff --git a/app/Services/ModelServicesProvider.php b/app/Services/ModelServicesProvider.php index d48aa32d..c97b4cef 100644 --- a/app/Services/ModelServicesProvider.php +++ b/app/Services/ModelServicesProvider.php @@ -43,6 +43,7 @@ use App\Services\Model\Imp\SummitEmailEventFlowService; use App\Services\Model\Imp\SummitMediaFileTypeService; use App\Services\Model\Imp\SummitMediaUploadTypeService; use App\Services\Model\Imp\SummitMetricService; +use App\Services\Model\Imp\SummitPresentationActionTypeService; use App\Services\Model\Imp\SummitRegistrationInvitationService; use App\Services\Model\Imp\SummitSelectedPresentationListService; use App\Services\Model\Imp\TrackChairService; @@ -68,6 +69,7 @@ use App\Services\Model\ISummitMediaUploadTypeService; use App\Services\Model\ISummitMetricService; use App\Services\Model\ISummitOrderExtraQuestionTypeService; use App\Services\Model\ISummitOrderService; +use App\Services\Model\ISummitPresentationActionTypeService; use App\Services\Model\ISummitPushNotificationService; use App\Services\Model\ISummitRefundPolicyTypeService; use App\Services\Model\ISummitRegistrationInvitationService; @@ -420,6 +422,11 @@ final class ModelServicesProvider extends ServiceProvider ITrackChairService::class, TrackChairService::class ); + + App::singleton( + ISummitPresentationActionTypeService::class, + SummitPresentationActionTypeService::class + ); } /** @@ -482,6 +489,7 @@ final class ModelServicesProvider extends ServiceProvider ISummitMetricService::class, ISummitSelectedPresentationListService::class, ITrackChairService::class, + ISummitPresentationActionTypeService::class, ]; } } \ No newline at end of file diff --git a/database/migrations/model/Version20210326171114.php b/database/migrations/model/Version20210326171114.php new file mode 100644 index 00000000..3eaf0432 --- /dev/null +++ b/database/migrations/model/Version20210326171114.php @@ -0,0 +1,94 @@ +hasTable("PresentationActionType")) { + $builder->create('PresentationActionType', function (Table $table) { + + $table->integer("ID", true, false); + $table->primary("ID"); + + $table->timestamp('Created'); + $table->timestamp('LastEdited'); + $table->string('ClassName')->setDefault("PresentationActionType"); + + $table->string('Label')->setNotnull(true)->setLength(255); + $table->integer('Order')->setDefault(1); + + // FK + $table->integer("SummitID", false, false)->setNotnull(false)->setDefault('NULL'); + $table->index("SummitID", "SummitID"); + $table->foreign("Summit", "SummitID", "ID", ["onDelete" => "CASCADE"]); + + $table->unique(["SummitID", "Label"]); + }); + } + + if(!$schema->hasTable("PresentationAction")) { + $builder->create('PresentationAction', function (Table $table) { + + $table->integer("ID", true, false); + $table->primary("ID"); + + $table->timestamp('Created'); + $table->timestamp('LastEdited'); + $table->string('ClassName')->setDefault("PresentationAction"); + + $table->boolean('IsCompleted')->setDefault(false); + + // FK + $table->integer("TypeID", false, false)->setNotnull(false)->setDefault('NULL'); + $table->index("TypeID", "TypeID"); + $table->foreign("PresentationActionType", "TypeID", "ID", ["onDelete" => "CASCADE"]); + + $table->integer("PresentationID", false, false)->setNotnull(false)->setDefault('NULL'); + $table->index("PresentationID", "PresentationID"); + $table->foreign("Presentation", "PresentationID", "ID", ["onDelete" => "CASCADE"]); + + $table->integer("CreatedByID", false, false)->setNotnull(false)->setDefault('NULL'); + $table->index("CreatedByID", "CreatedByID"); + $table->foreign("Member", "CreatedByID", "ID", ["onDelete" => "SET NULL"]); + + $table->integer("UpdateByID", false, false)->setNotnull(false)->setDefault('NULL'); + $table->index("UpdateByID", "UpdateByID"); + $table->foreign("Member", "UpdateByID", "ID", ["onDelete" => "SET NULL"]); + + $table->unique(["PresentationID", "TypeID"]); + }); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + + } +} diff --git a/database/migrations/model/Version20210326171117.php b/database/migrations/model/Version20210326171117.php new file mode 100644 index 00000000..97a90150 --- /dev/null +++ b/database/migrations/model/Version20210326171117.php @@ -0,0 +1,52 @@ +addSql($sql); + + $sql = <<addSql($sql); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + + } +} diff --git a/database/seeds/ApiEndpointsSeeder.php b/database/seeds/ApiEndpointsSeeder.php index 44f6b796..cac932c0 100644 --- a/database/seeds/ApiEndpointsSeeder.php +++ b/database/seeds/ApiEndpointsSeeder.php @@ -6445,6 +6445,93 @@ class ApiEndpointsSeeder extends Seeder IGroup::SummitAdministrators, ] ], + [ + 'name' => 'get-presentation-action-types', + 'route' => '/api/v1/summits/{id}/presentation-action-types', + 'http_method' => 'GET', + 'scopes' => [ + sprintf(SummitScopes::ReadAllSummitData, $current_realm), + sprintf(SummitScopes::ReadSummitData, $current_realm) + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + IGroup::TrackChairsAdmins, + ] + ], + [ + 'name' => 'get-presentation-action-types-csv', + 'route' => '/api/v1/summits/{id}/presentation-action-types/csv', + 'http_method' => 'GET', + 'scopes' => [ + sprintf(SummitScopes::ReadAllSummitData, $current_realm), + sprintf(SummitScopes::ReadSummitData, $current_realm) + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + IGroup::TrackChairsAdmins, + ] + ], + [ + 'name' => 'add-presentation-action-types', + 'route' => '/api/v1/summits/{id}/presentation-action-types', + 'http_method' => 'POST', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + IGroup::TrackChairsAdmins, + ] + ], + [ + 'name' => 'get-presentation-action-type-by-id', + 'route' => '/api/v1/summits/{id}/presentation-action-types/{action_id}', + 'http_method' => 'GET', + 'scopes' => [ + sprintf(SummitScopes::ReadAllSummitData, $current_realm), + sprintf(SummitScopes::ReadSummitData, $current_realm) + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + IGroup::TrackChairsAdmins, + ] + ], + [ + 'name' => 'delete-presentation-action-type', + 'route' => '/api/v1/summits/{id}/presentation-action-types/{action_id}', + 'http_method' => 'DELETE', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + IGroup::TrackChairsAdmins, + ] + ], + [ + 'name' => 'update-presentation-action-type', + 'route' => '/api/v1/summits/{id}/presentation-action-types/{action_id}', + 'http_method' => 'PUT', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm), + ], + 'authz_groups' => [ + IGroup::SuperAdmins, + IGroup::Administrators, + IGroup::SummitAdministrators, + IGroup::TrackChairsAdmins, + ] + ] ] ); } diff --git a/database/seeds/TestSeeder.php b/database/seeds/TestSeeder.php index a5bb8a5b..7bfd82f0 100644 --- a/database/seeds/TestSeeder.php +++ b/database/seeds/TestSeeder.php @@ -11,10 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ - use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Seeder; - +use Illuminate\Support\Facades\DB; /** * Class TestSeeder */ @@ -23,6 +22,14 @@ final class TestSeeder extends Seeder public function run() { Model::unguard(); + DB::setDefaultConnection("model"); + DB::table('Summit')->delete(); + DB::table('SummitEventType')->delete(); + DB::table('PresentationType')->delete(); + DB::table('SummitAbstractLocation')->delete(); + DB::table('SummitGeoLocatedLocation')->delete(); + DB::table('SummitVenue')->delete(); + DB::setDefaultConnection("config"); $this->call('ApiSeeder'); $this->call('ApiScopesSeeder'); $this->call('ApiEndpointsSeeder'); diff --git a/tests/OAuth2SummitPresentationActionTypeApiTest.php b/tests/OAuth2SummitPresentationActionTypeApiTest.php new file mode 100644 index 00000000..aaf4cb49 --- /dev/null +++ b/tests/OAuth2SummitPresentationActionTypeApiTest.php @@ -0,0 +1,282 @@ +setCurrentGroup(IGroup::TrackChairs); + parent::setUp(); + self::insertTestData(); + self::$summit_permission_group->addMember(self::$member); + self::$em->persist(self::$summit); + self::$em->persist(self::$summit_permission_group); + self::$em->flush(); + $track_chair = self::$summit->addTrackChair(self::$member, [ self::$defaultTrack ] ); + + self::$action1 = new PresentationActionType(); + self::$action1->setLabel("ACTION1"); + self::$action1->setOrder(1); + self::$summit->addPresentationActionType(self::$action1); + + self::$action2 = new PresentationActionType(); + self::$action2->setLabel("ACTION2"); + self::$action2->setOrder(2); + self::$summit->addPresentationActionType(self::$action2); + + self::$em->persist(self::$summit); + self::$em->flush(); + } + + protected function tearDown() + { + self::clearTestData(); + parent::tearDown(); + } + + public function testGetAllPerSummit(){ + $params = [ + 'summit_id' => self::$summit->getId(), + 'page' => 1, + 'per_page' => 10, + 'order' => '+order', + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitPresentationActionTypeApiController@getAllBySummit", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + $this->assertTrue($page->total == 2); + } + + public function testGetAllPerSummitWithFiltering(){ + $params = [ + 'summit_id' => self::$summit->getId(), + 'filter' => 'label==ACTION1', + 'page' => 1, + 'per_page' => 10, + 'order' => '+order', + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitPresentationActionTypeApiController@getAllBySummit", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $page = json_decode($content); + $this->assertTrue(!is_null($page)); + $this->assertTrue($page->total == 1); + } + + public function testGetActionTypeById(){ + $params = [ + 'summit_id' => self::$summit->getId(), + 'action_id' => self::$action1->getId(), + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "GET", + "OAuth2SummitPresentationActionTypeApiController@get", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $action = json_decode($content); + $this->assertTrue(!is_null($action)); + $this->assertTrue($action->id == self::$action1->getId()); + } + + public function testReorderAction(){ + $params = [ + 'summit_id' => self::$summit->getId(), + 'action_id' => self::$action2->getId(), + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $payload = [ + 'order' => 1, + ]; + + $response = $this->action( + "PUT", + "OAuth2SummitPresentationActionTypeApiController@update", + $params, + [], + [], + [], + $headers, + json_encode($payload) + ); + + $content = $response->getContent(); + $this->assertResponseStatus(201); + $action = json_decode($content); + $this->assertTrue(!is_null($action)); + $this->assertTrue($action->id == self::$action2->getId()); + $this->assertTrue($action->order == 1); + } + + public function testAddAction(){ + $params = [ + 'summit_id' => self::$summit->getId(), + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $payload = [ + 'label' => "ACTION3", + ]; + + $response = $this->action( + "POST", + "OAuth2SummitPresentationActionTypeApiController@add", + $params, + [], + [], + [], + $headers, + json_encode($payload) + ); + + $content = $response->getContent(); + $this->assertResponseStatus(201); + $action = json_decode($content); + $this->assertTrue(!is_null($action)); + $this->assertTrue($action->label == "ACTION3"); + $this->assertTrue($action->order == 3); + } + + + public function testUpdateAction(){ + $params = [ + 'summit_id' => self::$summit->getId(), + 'action_id' => self::$action2->getId(), + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $payload = [ + 'label' => self::$action2->getLabel()." UPDATE", + ]; + + $response = $this->action( + "PUT", + "OAuth2SummitPresentationActionTypeApiController@update", + $params, + [], + [], + [], + $headers, + json_encode($payload) + ); + + $content = $response->getContent(); + $this->assertResponseStatus(201); + $action = json_decode($content); + $this->assertTrue(!is_null($action)); + $this->assertTrue($action->id == self::$action2->getId()); + $this->assertTrue($action->label == self::$action2->getLabel()); + } + + + public function testDeleteAction(){ + $params = [ + 'summit_id' => self::$summit->getId(), + 'action_id' => self::$action2->getId(), + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "DELETE", + "OAuth2SummitPresentationActionTypeApiController@delete", + $params, + [], + [], + [], + $headers + ); + + $content = $response->getContent(); + $this->assertResponseStatus(204); + $this->assertTrue(empty($content)); + } + + +} \ No newline at end of file