diff --git a/app/Http/Controllers/Apis/Protected/Main/OAuth2MembersApiController.php b/app/Http/Controllers/Apis/Protected/Main/OAuth2MembersApiController.php index 49f5e3a2..297026e9 100644 --- a/app/Http/Controllers/Apis/Protected/Main/OAuth2MembersApiController.php +++ b/app/Http/Controllers/Apis/Protected/Main/OAuth2MembersApiController.php @@ -11,6 +11,7 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ +use App\Services\Model\IMemberService; use models\exceptions\EntityNotFoundException; use models\exceptions\ValidationException; use models\main\IMemberRepository; @@ -31,19 +32,27 @@ use utils\PagingInfo; */ final class OAuth2MembersApiController extends OAuth2ProtectedController { + /** + * @var IMemberService + */ + private $member_service; + /** * OAuth2MembersApiController constructor. * @param IMemberRepository $member_repository + * @param IMemberService $member_service * @param IResourceServerContext $resource_server_context */ public function __construct ( IMemberRepository $member_repository, + IMemberService $member_service, IResourceServerContext $resource_server_context ) { parent::__construct($resource_server_context); - $this->repository = $member_repository; + $this->repository = $member_repository; + $this->member_service = $member_service; } public function getAll(){ @@ -163,5 +172,87 @@ final class OAuth2MembersApiController extends OAuth2ProtectedController ); } + /** + * @param int $member_id + * @param int $affiliation_id + * @return mixed + */ + public function updateAffiliation($member_id, $affiliation_id){ + try { + if(!Request::isJson()) return $this->error403(); + $data = Input::json(); + + $member = $this->repository->getById($member_id); + if(is_null($member)) return $this->error404(); + + $rules = [ + 'is_current' => 'sometimes|boolean', + 'start_date' => 'sometimes|date_format:U|valid_epoch', + 'end_date' => 'sometimes|date_format:U|after_or_null_epoch:start_date', + 'organization_id' => 'sometimes|integer', + 'job_title' => 'sometimes|string|max:255' + ]; + + // Creates a Validator instance and validates the data. + $validation = Validator::make($data->all(), $rules); + + if ($validation->fails()) { + $messages = $validation->messages()->toArray(); + + return $this->error412 + ( + $messages + ); + } + + $affiliation = $this->member_service->updateAffiliation($member, $affiliation_id, $data->all()); + + return $this->ok(SerializerRegistry::getInstance()->getSerializer($affiliation)->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 $member_id + * @param $affiliation_id + * @return mixed + */ + public function deleteAffiliation($member_id, $affiliation_id){ + try{ + + $member = $this->repository->getById($member_id); + if(is_null($member)) return $this->error404(); + + $this->member_service->deleteAffiliation($member, $affiliation_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 78c829f1..df0643a6 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -98,6 +98,17 @@ Route::group([ }); }); }); + + Route::group(['prefix'=>'{member_id}'], function(){ + + Route::group(['prefix' => 'affiliations'], function(){ + + Route::group(['prefix' => '{affiliation_id}'], function(){ + Route::put('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2MembersApiController@updateAffiliation']); + Route::delete('', [ 'middleware' => 'auth.user:administrators|summit-front-end-administrators', 'uses' => 'OAuth2MembersApiController@deleteAffiliation']); + }); + }); + }); }); // tags diff --git a/app/ModelSerializers/AffiliationSerializer.php b/app/ModelSerializers/AffiliationSerializer.php index cacfc9ca..0c30d061 100644 --- a/app/ModelSerializers/AffiliationSerializer.php +++ b/app/ModelSerializers/AffiliationSerializer.php @@ -22,9 +22,9 @@ use models\main\Affiliation; final class AffiliationSerializer extends SilverStripeSerializer { protected static $array_mappings = [ - 'StartDate' => 'start_date:datetime_epoch', 'EndDate' => 'end_date:datetime_epoch', + 'JobTitle' => 'job_title:json_string', 'OwnerId' => 'owner_id:json_int', 'IsCurrent' => 'is_current:json_boolean', 'OrganizationId' => 'organization_id:json_int' diff --git a/app/Models/Foundation/Main/Affiliation.php b/app/Models/Foundation/Main/Affiliation.php index 1c85d3fa..54e47719 100644 --- a/app/Models/Foundation/Main/Affiliation.php +++ b/app/Models/Foundation/Main/Affiliation.php @@ -11,10 +11,8 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ - use Doctrine\ORM\Mapping as ORM; use models\utils\SilverstripeBaseModel; - /** * @ORM\Entity * @ORM\Table(name="Affiliation") @@ -41,6 +39,12 @@ class Affiliation extends SilverstripeBaseModel */ private $is_current; + /** + * @ORM\Column(name="JobTitle", type="string") + * @var string + */ + private $job_title; + /** * @ORM\ManyToOne(targetEntity="models\main\Member", inversedBy="affiliations") * @ORM\JoinColumn(name="MemberID", referencedColumnName="ID") @@ -170,4 +174,19 @@ class Affiliation extends SilverstripeBaseModel return $this->getOrganizationId() > 0; } + /** + * @return mixed + */ + public function getJobTitle() + { + return $this->job_title; + } + + /** + * @param mixed $job_title + */ + public function setJobTitle($job_title) + { + $this->job_title = $job_title; + } } \ No newline at end of file diff --git a/app/Models/Foundation/Main/Company.php b/app/Models/Foundation/Main/Company.php index 8c5f913f..a02ead8f 100644 --- a/app/Models/Foundation/Main/Company.php +++ b/app/Models/Foundation/Main/Company.php @@ -11,11 +11,9 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ - use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping AS ORM; use models\utils\SilverstripeBaseModel; - /** * @ORM\Entity(repositoryClass="repositories\main\DoctrineCompanyRepository") * @ORM\Table(name="Company") diff --git a/app/Models/Foundation/Main/Member.php b/app/Models/Foundation/Main/Member.php index 24fc4a93..0f52ded2 100644 --- a/app/Models/Foundation/Main/Member.php +++ b/app/Models/Foundation/Main/Member.php @@ -70,6 +70,7 @@ class Member extends SilverstripeBaseModel /** * @ORM\OneToMany(targetEntity="Affiliation", mappedBy="owner", cascade={"persist"}) + * @var Affiliation[] */ private $affiliations; @@ -994,4 +995,25 @@ SQL; $calendar_sync_info->clearOwner(); } + /** + * @param int $affiliation_id + * @return Affiliation|null + */ + public function getAffiliationById($affiliation_id){ + $criteria = Criteria::create(); + $criteria->where(Criteria::expr()->eq('id', $affiliation_id)); + + $affiliation = $this->affiliations->matching($criteria)->first(); + + return $affiliation ? $affiliation : null; + } + + /** + * @param Affiliation $affiliation + * @return $this + */ + public function removeAffiliation(Affiliation $affiliation){ + $this->affiliations->removeElement($affiliation); + return $this; + } } \ No newline at end of file diff --git a/app/Models/Foundation/Main/Organization.php b/app/Models/Foundation/Main/Organization.php index 044607b0..b9182284 100644 --- a/app/Models/Foundation/Main/Organization.php +++ b/app/Models/Foundation/Main/Organization.php @@ -11,12 +11,10 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ - use Doctrine\ORM\Mapping as ORM; use models\utils\SilverstripeBaseModel; - /** - * @ORM\Entity + * @ORM\Entity(repositoryClass="repositories\main\DoctrineOrganizationRepository") * @ORM\Table(name="Org") * Class Organization * @package models\main diff --git a/app/Models/Foundation/Main/Repositories/IOrganizationRepository.php b/app/Models/Foundation/Main/Repositories/IOrganizationRepository.php new file mode 100644 index 00000000..a26864bf --- /dev/null +++ b/app/Models/Foundation/Main/Repositories/IOrganizationRepository.php @@ -0,0 +1,22 @@ +addReplacer('after_or_null_epoch', function($message, $attribute, $rule, $parameters) use ($validator) { + return sprintf("%s should be zero or after %s", $attribute, $parameters[0]); + }); + $data = $validator->getData(); + if(is_null($value) || intval($value) == 0 ) return true; + if(isset($data[$parameters[0]])){ + $compare_to = $data[$parameters[0]]; + return intval($compare_to) < intval($value); + } + return true; + }); + + Validator::extend('valid_epoch', function($attribute, $value, $parameters, $validator) + { + $validator->addReplacer('valid_epoch', function($message, $attribute, $rule, $parameters) use ($validator) { + return sprintf("%s should be a valid epoch value", $attribute); + }); + return intval($value) > 0; + }); } /** diff --git a/app/Repositories/Main/DoctrineOrganizationRepository.php b/app/Repositories/Main/DoctrineOrganizationRepository.php new file mode 100644 index 00000000..14f49ebd --- /dev/null +++ b/app/Repositories/Main/DoctrineOrganizationRepository.php @@ -0,0 +1,33 @@ +organization_repository = $organization_repository; + $this->tx_service = $tx_service; + } + + /** + * @param Member $member + * @param int $affiliation_id + * @param array $data + * @return Affiliation + */ + public function updateAffiliation(Member $member, $affiliation_id, array $data) + { + return $this->tx_service->transaction(function() use($member, $affiliation_id, $data){ + $affiliation = $member->getAffiliationById($affiliation_id); + if(is_null($affiliation)) + throw new EntityNotFoundException(sprintf("affiliation id %s does not belongs to member id %s", $affiliation_id, $member->getId())); + + if(isset($data['is_current'])) + $affiliation->setIsCurrent(boolval($data['is_current'])); + if(isset($data['start_date'])) { + $start_date = intval($data['start_date']); + $affiliation->setEndDate(new DateTime("@$start_date")); + } + if(isset($data['end_date'])) { + $end_date = intval($data['end_date']); + $affiliation->setEndDate($end_date > 0 ? new DateTime("@$end_date") : null); + } + if(isset($data['organization_id'])) { + $org = $this->organization_repository->getById(intval($data['organization_id'])); + if(is_null($org)) + throw new EntityNotFoundException(sprintf("organization id %s not found", $data['organization_id'])); + $affiliation->setOrganization($org); + } + + if(isset($data['job_title'])) { + $affiliation->setJobTitle(trim($data['job_title'])); + } + + if($affiliation->isCurrent() && $affiliation->getEndDate() != null) + throw new ValidationException + ( + sprintf + ( + "in order to set affiliation id %s as current end_date should be null", + $affiliation_id + ) + ); + + return $affiliation; + }); + } + + /** + * @param Member $member + * @param $affiliation_id + * @return void + */ + public function deleteAffiliation(Member $member, $affiliation_id) + { + return $this->tx_service->transaction(function() use($member, $affiliation_id){ + $affiliation = $member->getAffiliationById($affiliation_id); + if(is_null($affiliation)) + throw new EntityNotFoundException(sprintf("affiliation id %s does not belongs to member id %s", $affiliation_id, $member->getId())); + + $member->removeAffiliation($affiliation); + }); + } +} \ No newline at end of file diff --git a/app/Services/ServicesProvider.php b/app/Services/ServicesProvider.php index 0c08cce9..10de307b 100644 --- a/app/Services/ServicesProvider.php +++ b/app/Services/ServicesProvider.php @@ -13,6 +13,8 @@ **/ use App\Services\Model\AttendeeService; use App\Services\Model\IAttendeeService; +use App\Services\Model\IMemberService; +use App\Services\Model\MemberService; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Config; use Illuminate\Support\ServiceProvider; @@ -120,5 +122,10 @@ class ServicesProvider extends ServiceProvider 'App\Services\Model\IAdminActionsCalendarSyncProcessingService', 'App\Services\Model\AdminActionsCalendarSyncProcessingService' ); + + App::singleton( + IMemberService::class, + MemberService::class + ); } } \ No newline at end of file diff --git a/database/seeds/ApiEndpointsSeeder.php b/database/seeds/ApiEndpointsSeeder.php index faa060a7..0bba9446 100644 --- a/database/seeds/ApiEndpointsSeeder.php +++ b/database/seeds/ApiEndpointsSeeder.php @@ -666,23 +666,34 @@ class ApiEndpointsSeeder extends Seeder $this->seedApiEndpoints('members', [ // members - array( + [ 'name' => 'get-members', 'route' => '/api/v1/members', 'http_method' => 'GET', 'scopes' => [sprintf('%s/members/read', $current_realm)], - ) - ] - ); - - $this->seedApiEndpoints('members', [ - // members - array( + ], + [ 'name' => 'get-my-member', 'route' => '/api/v1/members/me', 'http_method' => 'GET', 'scopes' => [sprintf('%s/members/read/me', $current_realm)], - ) + ], + [ + 'name' => 'update-member-affiliation', + 'route' => '/api/v1/members/{member_id}/affiliations/{affiliation_id}', + 'http_method' => 'PUT', + 'scopes' => [ + sprintf(SummitScopes::WriteMemberData, $current_realm) + ], + ], + [ + 'name' => 'delete-member-affiliation', + 'route' => '/api/v1/members/{member_id}/affiliations/{affiliation_id}', + 'http_method' => 'DELETE', + 'scopes' => [ + sprintf(SummitScopes::WriteMemberData, $current_realm) + ], + ] ] ); } diff --git a/database/seeds/ApiScopesSeeder.php b/database/seeds/ApiScopesSeeder.php index 73ff0f97..a85cfd0e 100644 --- a/database/seeds/ApiScopesSeeder.php +++ b/database/seeds/ApiScopesSeeder.php @@ -160,6 +160,11 @@ final class ApiScopesSeeder extends Seeder 'short_description' => 'Allows write only access to invitations', 'description' => 'Allows write only access to invitations', ), + array( + 'name' => sprintf(SummitScopes::WriteMemberData, $current_realm), + 'short_description' => 'Allows write only access to members', + 'description' => 'Allows write only access to memberss', + ), ]; foreach ($scopes as $scope_info) { diff --git a/tests/OAuth2MembersApiTest.php b/tests/OAuth2MembersApiTest.php index b39292cc..1623686e 100644 --- a/tests/OAuth2MembersApiTest.php +++ b/tests/OAuth2MembersApiTest.php @@ -136,4 +136,39 @@ final class OAuth2MembersApiTest extends ProtectedApiTest $this->assertTrue(!is_null($members)); $this->assertResponseStatus(200); } + + public function testUpdateMemberAffiliation(){ + $params = [ + 'member_id' => 11624, + 'affiliation_id' => 61749, + ]; + + $data = [ + 'is_current' => true, + 'end_date' => 0, + 'job_title' => 'test update' + ]; + + $headers = [ + "HTTP_Authorization" => " Bearer " . $this->access_token, + "CONTENT_TYPE" => "application/json" + ]; + + $response = $this->action( + "PUT", + "OAuth2MembersApiController@updateAffiliation", + $params, + [], + [], + [], + $headers, + json_encode($data) + ); + + $content = $response->getContent(); + $this->assertResponseStatus(200); + $affiliation = json_decode($content); + $this->assertTrue(!is_null($affiliation)); + return $affiliation; + } } \ No newline at end of file diff --git a/tests/ProtectedApiTest.php b/tests/ProtectedApiTest.php index 13751a26..7525a9fb 100644 --- a/tests/ProtectedApiTest.php +++ b/tests/ProtectedApiTest.php @@ -58,6 +58,7 @@ class AccessTokenServiceStub implements IAccessTokenService $url . '/me/summits/events/favorites/delete', sprintf(SummitScopes::WriteSpeakersData, $url), sprintf(SummitScopes::WriteAttendeesData, $url), + sprintf(SummitScopes::WriteMemberData, $url), ); return AccessToken::createFromParams('123456789', implode(' ', $scopes), '1', $realm, '1','11624', 3600, 'WEB_APPLICATION', '', ''); @@ -104,6 +105,7 @@ class AccessTokenServiceStub2 implements IAccessTokenService $url . '/me/summits/events/favorites/delete', sprintf(SummitScopes::WriteSpeakersData, $url), sprintf(SummitScopes::WriteAttendeesData, $url), + sprintf(SummitScopes::WriteMemberData, $url), ); return AccessToken::createFromParams('123456789', implode(' ', $scopes), '1', $realm, null,null, 3600, 'SERVICE', '', '');