From cb6b3a22c78f055d03f9b529a17cb3fefdd5fc7a Mon Sep 17 00:00:00 2001 From: Sebastian Marcet Date: Thu, 7 Jun 2018 11:41:34 -0700 Subject: [PATCH] Added endpoint to create selection plans by summit POST /api/v1/summits/{id}/selection-plans Payload 'name' => 'required|string|max:255', 'is_enabled' => 'required|boolean', 'submission_begin_date' => 'nullable|date_format:U', 'submission_end_date' => 'nullable|required_with:submission_begin_date|date_format:U|after_or_equal:submission_begin_date', 'voting_begin_date' => 'nullable|date_format:U', 'voting_end_date' => 'nullable|required_with:voting_begin_date|date_format:U|after_or_equal:voting_begin_date', 'selection_begin_date' => 'nullable|date_format:U', 'selection_end_date' => 'nullable|required_with:selection_begin_date|date_format:U|after_or_equal:selection_begin_date', Change-Id: I73eb45cd5f15b79b294ef9b098f7fa749c2122ef --- ...mitSelectionPlanValidationRulesFactory.php | 49 +++++++++ ...Auth2SummitSelectionPlansApiController.php | 100 +++++++++++++++++ app/Http/Utils/DateUtils.php | 32 ++++++ app/Http/routes.php | 8 +- .../Summit/SelectionPlanSerializer.php | 6 +- .../Factories/SummitSelectionPlanFactory.php | 101 ++++++++++++++++++ .../Foundation/Summit/SelectionPlan.php | 9 +- app/Models/Foundation/Summit/Summit.php | 96 +++++++++++++++++ .../Model/ISummitSelectionPlanService.php | 30 ++++++ app/Services/Model/SummitEventTypeService.php | 4 + .../Model/SummitSelectionPlanService.php | 58 ++++++++++ app/Services/Model/SummitService.php | 8 ++ app/Services/ServicesProvider.php | 8 ++ database/seeds/ApiEndpointsSeeder.php | 34 ++++++ resources/lang/en/validation_errors.php | 4 + tests/OAuth2SelectionPlansApiTest.php | 55 ++++++++++ 16 files changed, 594 insertions(+), 8 deletions(-) create mode 100644 app/Http/Controllers/Apis/Protected/Summit/Factories/SummitSelectionPlanValidationRulesFactory.php create mode 100644 app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSelectionPlansApiController.php create mode 100644 app/Http/Utils/DateUtils.php create mode 100644 app/Models/Foundation/Summit/Factories/SummitSelectionPlanFactory.php create mode 100644 app/Services/Model/ISummitSelectionPlanService.php create mode 100644 app/Services/Model/SummitSelectionPlanService.php create mode 100644 tests/OAuth2SelectionPlansApiTest.php diff --git a/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitSelectionPlanValidationRulesFactory.php b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitSelectionPlanValidationRulesFactory.php new file mode 100644 index 00000000..f73b1e26 --- /dev/null +++ b/app/Http/Controllers/Apis/Protected/Summit/Factories/SummitSelectionPlanValidationRulesFactory.php @@ -0,0 +1,49 @@ + 'sometimes|string|max:255', + 'is_enabled' => 'sometimes|boolean', + 'submission_begin_date' => 'nullable|date_format:U', + 'submission_end_date' => 'nullable|required_with:submission_begin_date|date_format:U|after_or_equal:submission_begin_date', + 'voting_begin_date' => 'nullable|date_format:U', + 'voting_end_date' => 'nullable|required_with:voting_begin_date|date_format:U|after_or_equal:voting_begin_date', + 'selection_begin_date' => 'nullable|date_format:U', + 'selection_end_date' => 'nullable|required_with:selection_begin_date|date_format:U|after_or_equal:selection_begin_date', + ]; + } + return [ + 'name' => 'required|string|max:255', + 'is_enabled' => 'required|boolean', + 'submission_begin_date' => 'nullable|date_format:U', + 'submission_end_date' => 'nullable|required_with:submission_begin_date|date_format:U|after_or_equal:submission_begin_date', + 'voting_begin_date' => 'nullable|date_format:U', + 'voting_end_date' => 'nullable|required_with:voting_begin_date|date_format:U|after_or_equal:voting_begin_date', + 'selection_begin_date' => 'nullable|date_format:U', + 'selection_end_date' => 'nullable|required_with:selection_begin_date|date_format:U|after_or_equal:selection_begin_date', + ]; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSelectionPlansApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSelectionPlansApiController.php new file mode 100644 index 00000000..79644513 --- /dev/null +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSelectionPlansApiController.php @@ -0,0 +1,100 @@ +summit_repository = $summit_repository; + $this->selection_plan_service = $selection_plan_service; + } + + public function addSelectionPlan($summit_id){ + try { + + if(!Request::isJson()) return $this->error400(); + $payload = Input::json()->all(); + + $summit = SummitFinderStrategyFactory::build($this->summit_repository, $this->resource_server_context)->find($summit_id); + if (is_null($summit)) return $this->error404(); + + $rules = SummitSelectionPlanValidationRulesFactory::build($payload); + // Creates a Validator instance and validates the data. + $validation = Validator::make($payload, $rules); + + if ($validation->fails()) { + $messages = $validation->messages()->toArray(); + + return $this->error412 + ( + $messages + ); + } + + $template = $this->selection_plan_service->addSelectionPlan($summit, $payload); + + return $this->created(SerializerRegistry::getInstance()->getSerializer($template)->serialize()); + } + catch (ValidationException $ex1) { + Log::warning($ex1); + return $this->error412([$ex1->getMessage()]); + } + catch(EntityNotFoundException $ex2) + { + Log::warning($ex2); + return $this->error404(['message'=> $ex2->getMessage()]); + } + catch (Exception $ex) { + Log::error($ex); + return $this->error500($ex); + } + } +} \ No newline at end of file diff --git a/app/Http/Utils/DateUtils.php b/app/Http/Utils/DateUtils.php new file mode 100644 index 00000000..52493c49 --- /dev/null +++ b/app/Http/Utils/DateUtils.php @@ -0,0 +1,32 @@ += $start2; + } +} \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index 89012474..bedd5b0b 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -108,7 +108,13 @@ Route::group([ Route::group(['prefix' => '{id}'], function () { Route::put('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitApiController@updateSummit']); Route::delete('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitApiController@deleteSummit']); - // rsvp templates + + // selection plans + Route::group(['prefix' => 'selection-plans'], function () { + Route::post('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitSelectionPlansApiController@addSelectionPlan']); + }); + + // RSVP templates Route::group(['prefix' => 'rsvp-templates'], function () { Route::get('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2SummitRSVPTemplatesApiController@getAllBySummit']); diff --git a/app/ModelSerializers/Summit/SelectionPlanSerializer.php b/app/ModelSerializers/Summit/SelectionPlanSerializer.php index 09112280..83be8440 100644 --- a/app/ModelSerializers/Summit/SelectionPlanSerializer.php +++ b/app/ModelSerializers/Summit/SelectionPlanSerializer.php @@ -49,18 +49,18 @@ final class SelectionPlanSerializer extends SilverStripeSerializer foreach ($selection_plan->getCategoryGroups() as $group) { $category_groups[] = $group->getId(); } - $values['category_groups'] = $category_groups; + $values['track_groups'] = $category_groups; if (!empty($expand)) { $expand = explode(',', $expand); foreach ($expand as $relation) { switch (trim($relation)) { - case 'category_groups':{ + case 'track_groups':{ $category_groups = []; foreach ($selection_plan->getCategoryGroups() as $group) { $category_groups[] = SerializerRegistry::getInstance()->getSerializer($group)->serialize($expand); } - $values['category_groups'] = $category_groups; + $values['track_groups'] = $category_groups; } break; } diff --git a/app/Models/Foundation/Summit/Factories/SummitSelectionPlanFactory.php b/app/Models/Foundation/Summit/Factories/SummitSelectionPlanFactory.php new file mode 100644 index 00000000..edd4e18a --- /dev/null +++ b/app/Models/Foundation/Summit/Factories/SummitSelectionPlanFactory.php @@ -0,0 +1,101 @@ +setName(trim($data['name'])); + + if(isset($data['is_enabled'])) + $selection_plan->setIsEnabled(boolval($data['is_enabled'])); + + if(array_key_exists('submission_begin_date', $data) && array_key_exists('submission_end_date', $data)) { + if (isset($data['submission_begin_date']) && isset($data['submission_end_date'])) { + $start_datetime = intval($data['submission_begin_date']); + $start_datetime = new \DateTime("@$start_datetime"); + $start_datetime->setTimezone($summit->getTimeZone()); + $end_datetime = intval($data['submission_end_date']); + $end_datetime = new \DateTime("@$end_datetime"); + $end_datetime->setTimezone($summit->getTimeZone()); + + // set local time from UTC + $selection_plan->setSubmissionBeginDate($start_datetime); + $selection_plan->setSubmissionEndDate($end_datetime); + } + else{ + $selection_plan->clearSubmissionDates(); + } + } + + if(array_key_exists('voting_begin_date', $data) && array_key_exists('voting_end_date', $data)) { + if (isset($data['voting_begin_date']) && isset($data['voting_end_date'])) { + $start_datetime = intval($data['voting_begin_date']); + $start_datetime = new \DateTime("@$start_datetime"); + $start_datetime->setTimezone($summit->getTimeZone()); + $end_datetime = intval($data['voting_end_date']); + $end_datetime = new \DateTime("@$end_datetime"); + $end_datetime->setTimezone($summit->getTimeZone()); + + // set local time from UTC + $selection_plan->setVotingBeginDate($start_datetime); + $selection_plan->setVotingEndDate($end_datetime); + } + else{ + $selection_plan->clearVotingDates(); + } + } + + if(array_key_exists('selection_begin_date', $data) && array_key_exists('selection_end_date', $data)) { + if (isset($data['selection_begin_date']) && isset($data['selection_end_date'])) { + $start_datetime = intval($data['selection_begin_date']); + $start_datetime = new \DateTime("@$start_datetime"); + $start_datetime->setTimezone($summit->getTimeZone()); + $end_datetime = intval($data['selection_end_date']); + $end_datetime = new \DateTime("@$end_datetime"); + $end_datetime->setTimezone($summit->getTimeZone()); + + // set local time from UTC + $selection_plan->setSelectionBeginDate($start_datetime); + $selection_plan->setSelectionEndDate($end_datetime); + } + else{ + $selection_plan->clearSelectionDates(); + } + } + + return $selection_plan; + } +} \ No newline at end of file diff --git a/app/Models/Foundation/Summit/SelectionPlan.php b/app/Models/Foundation/Summit/SelectionPlan.php index 18198195..6d815e01 100644 --- a/app/Models/Foundation/Summit/SelectionPlan.php +++ b/app/Models/Foundation/Summit/SelectionPlan.php @@ -13,6 +13,7 @@ **/ use App\Models\Utils\TimeZoneEntity; use Doctrine\Common\Collections\ArrayCollection; +use models\summit\PresentationCategoryGroup; use models\summit\SummitOwned; use models\utils\SilverstripeBaseModel; use Doctrine\ORM\Mapping AS ORM; @@ -85,11 +86,11 @@ class SelectionPlan extends SilverstripeBaseModel */ private $selection_end_date; - /* + /** * @ORM\ManyToMany(targetEntity="models\summit\PresentationCategoryGroup") * @ORM\JoinTable(name="SelectionPlan_CategoryGroups", - * joinColumns={@JoinColumn(name="SelectionPlanID", referencedColumnName="ID")}, - * inverseJoinColumns={@JoinColumn(name="PresentationCategoryGroupID", referencedColumnName="ID")} + * joinColumns={@ORM\JoinColumn(name="SelectionPlanID", referencedColumnName="ID")}, + * inverseJoinColumns={@ORM\JoinColumn(name="PresentationCategoryGroupID", referencedColumnName="ID")} * ) * @var PresentationCategoryGroup[] */ @@ -249,7 +250,7 @@ class SelectionPlan extends SilverstripeBaseModel } /** - * @return ArrayCollection + * @return PresentationCategoryGroup[] */ public function getCategoryGroups() { diff --git a/app/Models/Foundation/Summit/Summit.php b/app/Models/Foundation/Summit/Summit.php index 3298d138..1d409965 100644 --- a/app/Models/Foundation/Summit/Summit.php +++ b/app/Models/Foundation/Summit/Summit.php @@ -12,6 +12,7 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ +use App\Http\Utils\DateUtils; use App\Models\Foundation\Summit\Events\RSVP\RSVPTemplate; use App\Models\Foundation\Summit\SelectionPlan; use App\Models\Foundation\Summit\TrackTagGroup; @@ -1964,4 +1965,99 @@ SQL; return $this->selection_plans; } + /** + * @param SelectionPlan $selection_plan + * @throws ValidationException + * @return bool + */ + public function checkSelectionPlanConflicts(SelectionPlan $selection_plan){ + foreach ($this->selection_plans as $sp){ + $start = $selection_plan->getSelectionBeginDate(); + $end = $selection_plan->getSelectionEndDate(); + + if(!is_null($start) && !is_null($end) && DateUtils::checkTimeFramesOverlap + ( + $start, + $end, + $sp->getSelectionBeginDate(), + $sp->getSelectionEndDate() + )) + throw new ValidationException(trans( + 'validation_errors.Summit.checkSelectionPlanConflicts.conflictOnSelectionWorkflow', + [ + 'selection_plan_id' => $sp->getId(), + 'summit_id' => $this->getId() + ] + )); + + $start = $selection_plan->getSubmissionBeginDate(); + $end = $selection_plan->getSubmissionEndDate(); + if(!is_null($start) && !is_null($end) && DateUtils::checkTimeFramesOverlap + ( + $start, + $end, + $sp->getSubmissionBeginDate(), + $sp->getSubmissionEndDate() + )) + throw new ValidationException(trans( + 'validation_errors.Summit.checkSelectionPlanConflicts.conflictOnSubmissionWorkflow', + [ + 'selection_plan_id' => $sp->getId(), + 'summit_id' => $this->getId() + ] + )); + + $start = $selection_plan->getVotingBeginDate(); + $end = $selection_plan->getVotingEndDate(); + + if(!is_null($start) && !is_null($end) && DateUtils::checkTimeFramesOverlap + ( + $start, + $end, + $sp->getVotingBeginDate(), + $sp->getVotingEndDate() + )) + throw new ValidationException(trans( + 'validation_errors.Summit.checkSelectionPlanConflicts.conflictOnVotingWorkflow', + [ + 'selection_plan_id' => $sp->getId(), + 'summit_id' => $this->getId() + ] + )); + } + + return true; + } + + /** + * @param string $name + * @return null|SelectionPlan + */ + public function getSelectionPlanByName($name){ + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->eq('name', intval($name))); + $selection_plan = $this->selection_plans->matching($criteria)->first(); + return $selection_plan === false ? null : $selection_plan; + } + + /** + * @param SelectionPlan $selection_plan + * @return $this + */ + public function addSelectionPlan(SelectionPlan $selection_plan){ + $this->selection_plans->add($selection_plan); + $selection_plan->setSummit($this); + return $this; + } + + /** + * @param SelectionPlan $selection_plan + * @return $this + */ + public function removeSelectionSelectionPlan(SelectionPlan $selection_plan){ + $this->selection_plans->removeElement($selection_plan); + $selection_plan->clearSummit(); + return $this; + } + } diff --git a/app/Services/Model/ISummitSelectionPlanService.php b/app/Services/Model/ISummitSelectionPlanService.php new file mode 100644 index 00000000..9ad522a5 --- /dev/null +++ b/app/Services/Model/ISummitSelectionPlanService.php @@ -0,0 +1,30 @@ +tx_service->transaction(function() use($summit, $payload){ + + $selection_plan = SummitSelectionPlanFactory::build($payload, $summit); + + $former_selection_plan = $summit->getSelectionPlanByName($selection_plan->getName()); + + if(!is_null($former_selection_plan)){ + throw new ValidationException(trans( + 'validation_errors.SummitSelectionPlanService.addSelectionPlan.alreadyExistName', + [ + 'summit_id' => $summit->getId() + ] + )); + } + + // validate selection plan + $summit->checkSelectionPlanConflicts($selection_plan); + + $summit->addSelectionPlan($selection_plan); + + return $selection_plan; + }); + } +} \ No newline at end of file diff --git a/app/Services/Model/SummitService.php b/app/Services/Model/SummitService.php index 24a5b547..fef8845f 100644 --- a/app/Services/Model/SummitService.php +++ b/app/Services/Model/SummitService.php @@ -797,6 +797,13 @@ final class SummitService extends AbstractService implements ISummitService } } + /** + * @param SummitEvent $event + * @param SummitEventType $event_type + * @param array $data + * @throws EntityNotFoundException + * @throws ValidationException + */ private function saveOrUpdatePresentationData(SummitEvent $event, SummitEventType $event_type, array $data ){ if(!$event instanceof Presentation) return; @@ -859,6 +866,7 @@ final class SummitService extends AbstractService implements ISummitService } } + /** * @param Summit $summit * @param int $event_id diff --git a/app/Services/ServicesProvider.php b/app/Services/ServicesProvider.php index df2d8ee9..281a29c4 100644 --- a/app/Services/ServicesProvider.php +++ b/app/Services/ServicesProvider.php @@ -24,6 +24,7 @@ use App\Services\Model\IPresentationCategoryGroupService; use App\Services\Model\IRSVPTemplateService; use App\Services\Model\ISummitEventTypeService; use App\Services\Model\ISummitPushNotificationService; +use App\Services\Model\ISummitSelectionPlanService; use App\Services\Model\ISummitTicketTypeService; use App\Services\Model\ISummitTrackService; use App\Services\Model\PresentationCategoryGroupService; @@ -32,6 +33,7 @@ use App\Services\Model\MemberService; use App\Services\Model\RSVPTemplateService; use App\Services\Model\SummitPromoCodeService; use App\Services\Model\SummitPushNotificationService; +use App\Services\Model\SummitSelectionPlanService; use App\Services\Model\SummitTicketTypeService; use App\Services\Model\SummitTrackService; use App\Services\SummitEventTypeService; @@ -224,5 +226,11 @@ final class ServicesProvider extends ServiceProvider Config::get("server.google_geocoding_api_key", null) ); }); + + + App::singleton( + ISummitSelectionPlanService::class, + SummitSelectionPlanService::class + ); } } \ No newline at end of file diff --git a/database/seeds/ApiEndpointsSeeder.php b/database/seeds/ApiEndpointsSeeder.php index 84ebe982..63a603a8 100644 --- a/database/seeds/ApiEndpointsSeeder.php +++ b/database/seeds/ApiEndpointsSeeder.php @@ -1603,6 +1603,40 @@ class ApiEndpointsSeeder extends Seeder sprintf(SummitScopes::WriteSummitData, $current_realm) ], ], + // selection plans + [ + 'name' => 'get-selection-plan-by-id', + 'route' => '/api/v1/summits/{id}/selection-plans/{selection_plan_id}', + 'http_method' => 'GET', + 'scopes' => [ + sprintf(SummitScopes::ReadAllSummitData, $current_realm), + sprintf(SummitScopes::ReadSummitData, $current_realm) + ], + ], + [ + 'name' => 'update-selection-plan', + 'route' => '/api/v1/summits/{id}/selection-plans/{selection_plan_id}', + 'http_method' => 'PUT', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm) + ], + ], + [ + 'name' => 'delete-selection-plan', + 'route' => '/api/v1/summits/{id}/selection-plans/{selection_plan_id}', + 'http_method' => 'DELETE', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm) + ], + ], + [ + 'name' => 'add-selection-plan', + 'route' => '/api/v1/summits/{id}/selection-plans', + 'http_method' => 'POST', + 'scopes' => [ + sprintf(SummitScopes::WriteSummitData, $current_realm) + ], + ], ]); } diff --git a/resources/lang/en/validation_errors.php b/resources/lang/en/validation_errors.php index fe06fced..dea667b2 100644 --- a/resources/lang/en/validation_errors.php +++ b/resources/lang/en/validation_errors.php @@ -73,4 +73,8 @@ return [ // SummitPushNotificationService 'SummitPushNotificationService.addPushNotification.MemberNotActive' => 'member :member_id is not active', 'SummitPushNotificationService.deleteNotification.NotificationAlreadySent' => 'notification :notification_id is already sent.', + 'Summit.checkSelectionPlanConflicts.conflictOnSelectionWorkflow' => 'there is a conflict on selection dates with selection plan :selection_plan_id on summit :summit_id', + 'Summit.checkSelectionPlanConflicts.conflictOnSubmissionWorkflow' => 'there is a conflict on submission dates with selection plan :selection_plan_id on summit :summit_id', + 'Summit.checkSelectionPlanConflicts.conflictOnVotingWorkflow' => 'there is a conflict on voting dates with selection plan :selection_plan_id on summit :summit_id', + 'SummitSelectionPlanService.addSelectionPlan.alreadyExistName' => 'there is already another selection plan with same name on summit :summit_id', ]; \ No newline at end of file diff --git a/tests/OAuth2SelectionPlansApiTest.php b/tests/OAuth2SelectionPlansApiTest.php new file mode 100644 index 00000000..53cf7349 --- /dev/null +++ b/tests/OAuth2SelectionPlansApiTest.php @@ -0,0 +1,55 @@ + $summit_id, + ]; + + $name = str_random(16).'_selection_plan'; + $data = [ + 'name' => $name, + 'is_enabled' => true + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "POST", + "OAuth2SummitSelectionPlansApiController@addSelectionPlan", + $params, + [], + [], + [], + $headers, + json_encode($data) + ); + + $content = $response->getContent(); + $this->assertResponseStatus(201); + $selection_plan = json_decode($content); + $this->assertTrue(!is_null($selection_plan)); + $this->assertEquals($name, $selection_plan->name); + return $selection_plan; + } +} \ No newline at end of file