diff --git a/app/Http/Controllers/Apis/Protected/Main/OAuth2ElectionsApiController.php b/app/Http/Controllers/Apis/Protected/Main/OAuth2ElectionsApiController.php index e42e97351..9937f5ce3 100644 --- a/app/Http/Controllers/Apis/Protected/Main/OAuth2ElectionsApiController.php +++ b/app/Http/Controllers/Apis/Protected/Main/OAuth2ElectionsApiController.php @@ -21,11 +21,14 @@ use ModelSerializers\SerializerRegistry; use utils\Filter; use utils\PagingInfo; +use OpenApi\Attributes as OA; +use Symfony\Component\HttpFoundation\Response; /** * Class OAuth2ElectionsApiController * @package App\Http\Controllers */ +#[OA\Tag(name: "Elections", description: "Elections Management Endpoints")] class OAuth2ElectionsApiController extends OAuth2ProtectedController { @@ -50,9 +53,59 @@ public function __construct $this->service = $service; } - /** - * @return mixed - */ + #[OA\Get( + path: "/api/v1/elections", + operationId: "getAllElections", + description: "Get all elections with pagination and filtering", + tags: ["Elections"], + parameters: [ + new OA\Parameter( + name: "page", + in: "query", + required: false, + schema: new OA\Schema(type: "integer", default: 1), + description: "Page number" + ), + new OA\Parameter( + name: "per_page", + in: "query", + required: false, + schema: new OA\Schema(type: "integer", default: 20), + description: "Items per page" + ), + new OA\Parameter( + name: "filter", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Filter by name, opens, or closes (epoch format). Example: name[]=Test&opens[from]=1634567890" + ), + new OA\Parameter( + name: "order", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Order by: name, id, opens, or closes. Example: name,-opens" + ), + new OA\Parameter( + name: "expand", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Expand relationships" + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: "Elections list retrieved successfully", + content: new OA\JsonContent(ref: "#/components/schemas/ElectionsList") + ), + new OA\Response(response: Response::HTTP_BAD_REQUEST, description: "Bad Request"), + new OA\Response(response: Response::HTTP_PRECONDITION_FAILED, description: "Validation Error"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + ] + )] public function getAll() { return $this->_getAll( @@ -97,9 +150,30 @@ function ($page, $per_page, $filter, $order, $applyExtraFilters) { ); } - /** - * @return \Illuminate\Http\JsonResponse|mixed - */ + #[OA\Get( + path: "/api/v1/elections/current", + operationId: "getCurrentElection", + description: "Get the current active election", + tags: ["Elections"], + parameters: [ + new OA\Parameter( + name: "expand", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Expand relationships" + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: "Current election retrieved successfully", + content: new OA\JsonContent(ref: "#/components/schemas/Election") + ), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: "No current election found"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + ] + )] public function getCurrent() { return $this->processRequest(function () { @@ -122,10 +196,37 @@ public function getCurrent() }); } - /** - * @param $election_id - * @return mixed - */ + #[OA\Get( + path: "/api/v1/elections/{election_id}", + operationId: "getElectionById", + description: "Get election by ID", + tags: ["Elections"], + parameters: [ + new OA\Parameter( + name: "election_id", + in: "path", + required: true, + schema: new OA\Schema(type: "integer", format: "int64"), + description: "Election ID" + ), + new OA\Parameter( + name: "expand", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Expand relationships" + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: "Election retrieved successfully", + content: new OA\JsonContent(ref: "#/components/schemas/Election") + ), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: "Election not found"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + ] + )] public function getById($election_id) { return $this->processRequest(function () use ($election_id) { @@ -148,9 +249,58 @@ public function getById($election_id) }); } - /** - * @return \Illuminate\Http\JsonResponse|mixed - */ + #[OA\Get( + path: "/api/v1/elections/current/candidates", + operationId: "getCurrentElectionCandidates", + description: "Get all accepted candidates for the current election", + tags: ["Elections"], + parameters: [ + new OA\Parameter( + name: "page", + in: "query", + required: false, + schema: new OA\Schema(type: "integer", default: 1), + description: "Page number" + ), + new OA\Parameter( + name: "per_page", + in: "query", + required: false, + schema: new OA\Schema(type: "integer", default: 20), + description: "Items per page" + ), + new OA\Parameter( + name: "filter", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Filter by first_name, last_name, or full_name" + ), + new OA\Parameter( + name: "order", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Order by first_name or last_name" + ), + new OA\Parameter( + name: "expand", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Expand relationships" + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: "Current election candidates retrieved successfully", + content: new OA\JsonContent(ref: "#/components/schemas/CandidatesList") + ), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: "No current election found"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + ] + )] public function getCurrentCandidates() { @@ -199,10 +349,66 @@ function ($page, $per_page, $filter, $order, $applyExtraFilters) use ($election) ); } - /** - * @return \Illuminate\Http\JsonResponse|mixed - */ - public function getElectionCandidates($election_id) + #[OA\Get( + path: "/api/v1/elections/{election_id}/candidates", + operationId: "getElectionCandidates", + description: "Get all accepted candidates for a specific election", + tags: ["Elections"], + parameters: [ + new OA\Parameter( + name: "election_id", + in: "path", + required: true, + schema: new OA\Schema(type: "integer", format: "int64"), + description: "Election ID" + ), + new OA\Parameter( + name: "page", + in: "query", + required: false, + schema: new OA\Schema(type: "integer", default: 1), + description: "Page number" + ), + new OA\Parameter( + name: "per_page", + in: "query", + required: false, + schema: new OA\Schema(type: "integer", default: 20), + description: "Items per page" + ), + new OA\Parameter( + name: "filter", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Filter by first_name, last_name, or full_name" + ), + new OA\Parameter( + name: "order", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Order by first_name or last_name" + ), + new OA\Parameter( + name: "expand", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Expand relationships" + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: "Election candidates retrieved successfully", + content: new OA\JsonContent(ref: "#/components/schemas/CandidatesList") + ), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: "Election not found"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + ] + )] + public function getElectionCandidates(int $election_id) { $election = $this->repository->getById(intval($election_id)); @@ -250,9 +456,58 @@ function ($page, $per_page, $filter, $order, $applyExtraFilters) use ($election) ); } - /** - * @return \Illuminate\Http\JsonResponse|mixed - */ + #[OA\Get( + path: "/api/v1/elections/current/candidates/gold", + operationId: "getCurrentElectionGoldCandidates", + description: "Get gold (featured/premium) candidates for the current election", + tags: ["Elections"], + parameters: [ + new OA\Parameter( + name: "page", + in: "query", + required: false, + schema: new OA\Schema(type: "integer", default: 1), + description: "Page number" + ), + new OA\Parameter( + name: "per_page", + in: "query", + required: false, + schema: new OA\Schema(type: "integer", default: 20), + description: "Items per page" + ), + new OA\Parameter( + name: "filter", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Filter by first_name, last_name, or full_name" + ), + new OA\Parameter( + name: "order", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Order by first_name or last_name" + ), + new OA\Parameter( + name: "expand", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Expand relationships" + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: "Gold candidates retrieved successfully", + content: new OA\JsonContent(ref: "#/components/schemas/CandidatesList") + ), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: "No current election found"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + ] + )] public function getCurrentGoldCandidates() { @@ -301,6 +556,65 @@ function ($page, $per_page, $filter, $order, $applyExtraFilters) use ($election) ); } + #[OA\Get( + path: "/api/v1/elections/{election_id}/candidates/gold", + operationId: "getElectionGoldCandidates", + description: "Get gold (featured/premium) candidates for a specific election", + tags: ["Elections"], + parameters: [ + new OA\Parameter( + name: "election_id", + in: "path", + required: true, + schema: new OA\Schema(type: "integer", format: "int64"), + description: "Election ID" + ), + new OA\Parameter( + name: "page", + in: "query", + required: false, + schema: new OA\Schema(type: "integer", default: 1), + description: "Page number" + ), + new OA\Parameter( + name: "per_page", + in: "query", + required: false, + schema: new OA\Schema(type: "integer", default: 20), + description: "Items per page" + ), + new OA\Parameter( + name: "filter", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Filter by first_name, last_name, or full_name" + ), + new OA\Parameter( + name: "order", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Order by first_name or last_name" + ), + new OA\Parameter( + name: "expand", + in: "query", + required: false, + schema: new OA\Schema(type: "string"), + description: "Expand relationships" + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: "Gold candidates retrieved successfully", + content: new OA\JsonContent(ref: "#/components/schemas/CandidatesList") + ), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: "Election not found"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + ] + )] public function getElectionGoldCandidates($election_id) { @@ -349,11 +663,57 @@ function ($page, $per_page, $filter, $order, $applyExtraFilters) use ($election) ); } - use GetAndValidateJsonPayload; - - /** - * @return \Illuminate\Http\JsonResponse|mixed - */ + #[OA\Put( + path: "/api/v1/elections/current/candidates/me", + operationId: "updateMyCandidateProfile", + description: "Update current user's candidate profile for the current election", + tags: ["Elections"], + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + type: "object", + properties: [ + new OA\Property( + property: "bio", + type: "string", + description: "Candidate biography" + ), + new OA\Property( + property: "relationship_to_openstack", + type: "string", + description: "Relationship to OpenStack" + ), + new OA\Property( + property: "experience", + type: "string", + description: "Professional experience" + ), + new OA\Property( + property: "boards_role", + type: "string", + description: "Board role experience" + ), + new OA\Property( + property: "top_priority", + type: "string", + description: "Top priority if elected" + ), + ] + ) + ), + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: "Candidate profile updated successfully", + content: new OA\JsonContent(ref: "#/components/schemas/Member") + ), + new OA\Response(response: Response::HTTP_BAD_REQUEST, description: "Bad Request"), + new OA\Response(response: Response::HTTP_FORBIDDEN, description: "Forbidden - User not authenticated"), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: "No current election found"), + new OA\Response(response: Response::HTTP_PRECONDITION_FAILED, description: "Validation Error"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + ] + )] public function updateMyCandidateProfile() { return $this->processRequest(function () { @@ -393,11 +753,47 @@ public function updateMyCandidateProfile() }); } - /** - * @param $candidate_id - * @return \Illuminate\Http\JsonResponse|mixed - */ - public function nominateCandidate($candidate_id) + #[OA\Post( + path: "/api/v1/elections/current/candidates/{candidate_id}", + operationId: "nominateCandidate", + description: "Nominate a candidate for the current election", + tags: ["Elections"], + parameters: [ + new OA\Parameter( + name: "candidate_id", + in: "path", + required: true, + schema: new OA\Schema(type: "integer", format: "int64"), + description: "Candidate ID to nominate" + ), + ], + requestBody: new OA\RequestBody( + required: false, + content: new OA\JsonContent( + type: "object", + properties: [ + new OA\Property( + property: "comment", + type: "string", + description: "Optional comment for the nomination" + ), + ] + ) + ), + responses: [ + new OA\Response( + response: Response::HTTP_OK, + description: "Candidate nominated successfully", + content: new OA\JsonContent(ref: "#/components/schemas/Nomination") + ), + new OA\Response(response: Response::HTTP_BAD_REQUEST, description: "Bad Request"), + new OA\Response(response: Response::HTTP_FORBIDDEN, description: "Forbidden - User not authenticated"), + new OA\Response(response: Response::HTTP_NOT_FOUND, description: "Candidate or current election not found"), + new OA\Response(response: Response::HTTP_PRECONDITION_FAILED, description: "Validation Error"), + new OA\Response(response: Response::HTTP_INTERNAL_SERVER_ERROR, description: "Server Error"), + ] + )] + public function nominateCandidate(int $candidate_id) { return $this->processRequest(function () use ($candidate_id) { $current_member = $this->resource_server_context->getCurrentUser(); diff --git a/app/Swagger/Elections/CandidateSchema.php b/app/Swagger/Elections/CandidateSchema.php new file mode 100644 index 000000000..906d1366e --- /dev/null +++ b/app/Swagger/Elections/CandidateSchema.php @@ -0,0 +1,52 @@ +