Added member affilition endpoints

update member affiliation

PUT /api/v1/members/{member_id}/affiliations/{affiliation_id}

Payload

{
  start_date: int|epoch
  end_date: int|epoch
  is_current: bool
  job_title: string|100
  organization_id: int
}

delete member affiliation

DELETE /api/v1/members/{member_id}/affiliations/{affiliation_id}

Change-Id: I0a80f0d0478c816f12e8d6ea6b93f9c6f98e4eb9
This commit is contained in:
Sebastian Marcet 2018-01-19 15:10:39 -03:00
parent a763661ad0
commit 885b9bca22
19 changed files with 454 additions and 19 deletions

View File

@ -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);
}
}
}

View File

@ -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

View File

@ -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'

View File

@ -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;
}
}

View File

@ -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")

View File

@ -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;
}
}

View File

@ -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

View File

@ -0,0 +1,22 @@
<?php namespace models\main;
/**
* Copyright 2018 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use models\utils\IBaseRepository;
/**
* Interface IOrganizationRepository
* @package models\main
*/
interface IOrganizationRepository extends IBaseRepository
{
}

View File

@ -200,6 +200,29 @@ class AppServiceProvider extends ServiceProvider
});
return in_array($value, [ PushNotificationMessagePriority::Normal, PushNotificationMessagePriority::High]);
});
Validator::extend('after_or_null_epoch', function($attribute, $value, $parameters, $validator)
{
$validator->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;
});
}
/**

View File

@ -0,0 +1,33 @@
<?php namespace repositories\main;
/**
* Copyright 2018 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use App\Repositories\SilverStripeDoctrineRepository;
use models\main\IOrganizationRepository;
use models\main\Organization;
/**
* Class DoctrineOrganizationRepository
* @package repositories\main
*/
final class DoctrineOrganizationRepository
extends SilverStripeDoctrineRepository
implements IOrganizationRepository
{
/**
* @return string
*/
protected function getBaseEntity()
{
return Organization::class;
}
}

View File

@ -19,11 +19,11 @@ use models\main\Company;
use models\main\EmailCreationRequest;
use models\main\File;
use models\main\Group;
use models\main\Organization;
use models\summit\ISummitTicketTypeRepository;
use models\summit\SpeakerRegistrationRequest;
use models\summit\SpeakerSummitRegistrationPromoCode;
use models\summit\SummitTicketType;
/**
* Class RepositoriesProvider
* @package repositories
@ -239,5 +239,11 @@ final class RepositoriesProvider extends ServiceProvider
function(){
return EntityManager::getRepository(SummitTicketType::class);
});
App::singleton(
'models\main\IOrganizationRepository',
function(){
return EntityManager::getRepository(Organization::class);
});
}
}

View File

@ -29,4 +29,6 @@ final class SummitScopes
const WriteVideoData = '%s/summits/write-videos';
const WriteAttendeesData = '%s/attendees/write';
const WriteMemberData = '%s/members/write';
}

View File

@ -0,0 +1,36 @@
<?php namespace App\Services\Model;
/**
* Copyright 2018 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use models\main\Affiliation;
use models\main\Member;
/**
* Interface IMemberService
* @package App\Services\Model
*/
interface IMemberService
{
/**
* @param Member $member
* @param int $affiliation_id
* @param array $data
* @return Affiliation
*/
public function updateAffiliation(Member $member, $affiliation_id, array $data);
/**
* @param Member $member
* @param $affiliation_id
* @return void
*/
public function deleteAffiliation(Member $member, $affiliation_id);
}

View File

@ -0,0 +1,114 @@
<?php namespace App\Services\Model;
/**
* Copyright 2018 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use libs\utils\ITransactionService;
use models\exceptions\EntityNotFoundException;
use models\exceptions\ValidationException;
use models\main\Affiliation;
use models\main\IOrganizationRepository;
use models\main\Member;
use DateTime;
/**
* Class MemberService
* @package App\Services\Model
*/
final class MemberService implements IMemberService
{
/**
* @var IOrganizationRepository
*/
private $organization_repository;
/**
* @var ITransactionService
*/
private $tx_service;
/**
* MemberService constructor.
* @param IOrganizationRepository $organization_repository
* @param ITransactionService $tx_service
*/
public function __construct
(
IOrganizationRepository $organization_repository,
ITransactionService $tx_service
)
{
$this->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);
});
}
}

View File

@ -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
);
}
}

View File

@ -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)
],
]
]
);
}

View File

@ -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) {

View File

@ -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;
}
}

View File

@ -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', '', '');