diff --git a/app/Classes/Repositories/OrganisationRepository.php b/app/Classes/Repositories/OrganisationRepository.php index 0a53976..79a7258 100644 --- a/app/Classes/Repositories/OrganisationRepository.php +++ b/app/Classes/Repositories/OrganisationRepository.php @@ -3,6 +3,7 @@ namespace App\Classes\Repositories; use App\Models\Organisation; +use App\Models\Contributor; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; @@ -87,9 +88,9 @@ public function updateDetailsWithInput(Organisation $org, array $input) if (count($input['translations'])) { DB::transaction(function () use ($org, $input) { $org->details()->delete(); - + foreach ($input['translations'] as $lang => $data) { - $org->details()->updateOrCreate([ + $currentDetail = $org->details()->updateOrCreate([ 'language_code' => strtolower($lang), ], [ 'language_code' => strtolower($lang), @@ -97,6 +98,25 @@ public function updateDetailsWithInput(Organisation $org, array $input) 'attribution_message' => $data['attributionMessage'] ?? '', 'published' => $data['published'] ?? false, ]); + + if (array_key_exists('contributors', $data)) { + $currentDetail->contributors()->delete(); + + foreach ($data['contributors'] as $contributor) { + $contributor = new Contributor([ + 'name' => $contributor['name'], + 'logo' => $contributor['logo'], + ]); + + $isValid = $contributor->validate($contributor->toArray()); + + if (!$isValid) { + Log::error('Contributor validation failed', ['errors' => $contributor->errors()->toArray()]); + } + + $currentDetail->contributors()->save($contributor); + } + } } }); } diff --git a/app/Classes/Transformers/OrganisationTransformer.php b/app/Classes/Transformers/OrganisationTransformer.php index 81af0d0..4d4bb20 100644 --- a/app/Classes/Transformers/OrganisationTransformer.php +++ b/app/Classes/Transformers/OrganisationTransformer.php @@ -37,7 +37,7 @@ public function transform(Organisation $model) 'name' => $model->org_name, 'url' => $model->attribution_url, 'imageUrl' => $model->attribution_file_name ? $model->getAttributionImageUrl() : null, - 'translations' => null + 'translations' => null, ]; if ($model->details->count()) { @@ -45,6 +45,10 @@ public function transform(Organisation $model) foreach ($model->details as $detail) { + $model->details->each(function ($detail) { + $detail->load('contributors'); + }); + if($this->unpublished || $detail->published) { $response['translations'][$detail->language_code] = [ 'languageCode' => $detail->language_code, @@ -52,6 +56,18 @@ public function transform(Organisation $model) 'attributionMessage' => $detail->attribution_message, ]; + $response['translations'][$detail->language_code]['contributors'] = []; + if ($detail->contributors->count()) { + + $response['translations'][$detail->language_code]['contributors'] = $detail->contributors->map(function ($contributor) { + return [ + 'id' => $contributor->id, + 'name' => $contributor->name, + 'logo' => $contributor->logo, + ]; + }); + } + $response['translations'][$detail->language_code]['published'] = (bool) $detail->published; } } diff --git a/app/Classes/Transformers/WhatNowEntityTransformer.php b/app/Classes/Transformers/WhatNowEntityTransformer.php index 53b57c6..e97c265 100644 --- a/app/Classes/Transformers/WhatNowEntityTransformer.php +++ b/app/Classes/Transformers/WhatNowEntityTransformer.php @@ -70,7 +70,7 @@ public function transform(WhatNowEntity $model) 'countryCode' => $model->organisation->country_code, 'url' => $model->organisation->attribution_url, 'imageUrl' => $model->organisation->attribution_file_name ? $model->organisation->getAttributionImageUrl() : null, - 'translations' => null + 'translations' => null, ], ]; @@ -78,12 +78,26 @@ public function transform(WhatNowEntity $model) $response['attribution']['translations'] = []; foreach ($model->organisation->details as $detail) { + $model->organisation->details->each(function ($detail) { + $detail->load('contributors'); + }); $response['attribution']['translations'][$detail->language_code] = [ 'languageCode' => $detail->language_code, 'name' => $detail->org_name, 'attributionMessage' => $detail->attribution_message, + 'contributors' => [] ]; + if ($detail->contributors->count()) { + $response['attribution']['translations'][$detail->language_code]['contributors'] = $detail->contributors->map(function ($contributor) { + return [ + 'id' => $contributor->id, + 'name' => $contributor->name, + 'logo' => $contributor->logo, + ]; + }); + } + $response['attribution']['translations'][$detail->language_code]['published'] = (bool) $detail->published; } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 03e02a2..b888c41 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -7,6 +7,13 @@ use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +/** + * @OA\Info( + * title="Whatnow API", + * description="Whatnow API documentation", + * version="1.0.0", + * ) + */ class Controller extends BaseController { use AuthorizesRequests, DispatchesJobs, ValidatesRequests; diff --git a/app/Http/Controllers/OrganisationController.php b/app/Http/Controllers/OrganisationController.php index 988e1e1..e33191d 100644 --- a/app/Http/Controllers/OrganisationController.php +++ b/app/Http/Controllers/OrganisationController.php @@ -11,6 +11,12 @@ use Illuminate\Support\Facades\Storage; use League\Fractal\Manager; +/** + * @OA\Tag( + * name="Organisation", + * description="Operations about Organisations" + * ) + */ class OrganisationController extends Controller { /** @@ -101,6 +107,77 @@ public function getById($code, Request $request) /** * @param $code * @return \Symfony\Component\HttpFoundation\Response + */ + /** + * @OA\Put( + * path="/organisation/{code}", + * summary="Update organisation by country code", + * tags={"Organisation"}, + * @OA\Parameter( + * name="code", + * in="path", + * description="Country code of the organisation", + * required=true, + * @OA\Schema(type="string") + * ), + * @OA\RequestBody( + * required=true, + * @OA\JsonContent( + * type="object", + * @OA\Property(property="countryCode", type="string", example="USA"), + * @OA\Property(property="name", type="string", example="American Red Cross"), + * @OA\Property(property="url", type="string", nullable=true, example=null), + * @OA\Property( + * property="translations", + * type="array", + * @OA\Items( + * type="object", + * @OA\Property(property="languageCode", type="string", example="en"), + * @OA\Property(property="name", type="string", example="Organization name"), + * @OA\Property(property="attributionMessage", type="string", example="Attribution Message"), + * @OA\Property(property="published", type="boolean", example=true), + * @OA\Property( + * property="contributors", + * type="array", + * @OA\Items( + * type="object", + * @OA\Property(property="name", type="string", example="Contributor name"), + * @OA\Property(property="logo", type="string", example="logo.png") + * ) + * ) + * ) + * ) + * ) + * ), + * @OA\Response( + * response=200, + * description="Successful response", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="data", type="object") + * ) + * ), + * @OA\Response( + * response=404, + * description="Organisation not found", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="status", type="integer", example=404), + * @OA\Property(property="error_message", type="string", example="Organisation does not exist"), + * @OA\Property(property="errors", type="array", @OA\Items(type="string")) + * ) + * ), + * @OA\Response( + * response=500, + * description="Organisation could not be updated", + * @OA\JsonContent( + * type="object", + * @OA\Property(property="status", type="integer", example=500), + * @OA\Property(property="error_message", type="string", example="Organisation could not be updated"), + * @OA\Property(property="errors", type="array", @OA\Items(type="string")) + * ) + * ) + * ) */ public function putById($code) { diff --git a/app/Models/Contributor.php b/app/Models/Contributor.php new file mode 100644 index 0000000..5b3eb1c --- /dev/null +++ b/app/Models/Contributor.php @@ -0,0 +1,75 @@ + 'required|string|between:2,100', + 'logo' => 'string|between:2,2048', + ]; + + + /** + * @param $data + * @return bool + */ + public function validate($data) + { + $v = Validator::make($data, $this->rules); + + if ($v->fails()) { + $this->errors = $v->errors(); + + return false; + } + + return true; + } + + /** + * @return mixed + */ + public function errors() + { + return $this->errors; + } + + public function organisation() + { + return $this->belongsTo(OrganisationDetails::class, 'org_detail_id'); + } + +} diff --git a/app/Models/OrganisationDetails.php b/app/Models/OrganisationDetails.php index 12e2051..44949a2 100644 --- a/app/Models/OrganisationDetails.php +++ b/app/Models/OrganisationDetails.php @@ -34,5 +34,10 @@ public function organisation() { return $this->belongsTo('App\Models\Details', 'org_id', 'id'); } + + public function contributors() + { + return $this->hasMany(Contributor::class, 'org_detail_id'); + } } diff --git a/composer.json b/composer.json index 522c4c3..e506f58 100644 --- a/composer.json +++ b/composer.json @@ -9,6 +9,7 @@ "license": "MIT", "require": { "php": "^7.4", + "darkaonline/l5-swagger": "^8.6", "fideloper/proxy": "^4.0", "laravel/framework": "^7.0", "laravel/helpers": "^1.6", diff --git a/config/l5-swagger.php b/config/l5-swagger.php new file mode 100644 index 0000000..a3ce546 --- /dev/null +++ b/config/l5-swagger.php @@ -0,0 +1,318 @@ + 'default', + 'documentations' => [ + 'default' => [ + 'api' => [ + 'title' => 'L5 Swagger UI', + ], + + 'routes' => [ + /* + * Route for accessing api documentation interface + */ + 'api' => 'api/documentation', + ], + 'paths' => [ + /* + * Edit to include full URL in ui for assets + */ + 'use_absolute_path' => env('L5_SWAGGER_USE_ABSOLUTE_PATH', true), + + /* + * Edit to set path where swagger ui assets should be stored + */ + 'swagger_ui_assets_path' => env('L5_SWAGGER_UI_ASSETS_PATH', 'vendor/swagger-api/swagger-ui/dist/'), + + /* + * File name of the generated json documentation file + */ + 'docs_json' => 'api-docs.json', + + /* + * File name of the generated YAML documentation file + */ + 'docs_yaml' => 'api-docs.yaml', + + /* + * Set this to `json` or `yaml` to determine which documentation file to use in UI + */ + 'format_to_use_for_docs' => env('L5_FORMAT_TO_USE_FOR_DOCS', 'json'), + + /* + * Absolute paths to directory containing the swagger annotations are stored. + */ + 'annotations' => [ + base_path('app'), + ], + ], + ], + ], + 'defaults' => [ + 'routes' => [ + /* + * Route for accessing parsed swagger annotations. + */ + 'docs' => 'docs', + + /* + * Route for Oauth2 authentication callback. + */ + 'oauth2_callback' => 'api/oauth2-callback', + + /* + * Middleware allows to prevent unexpected access to API documentation + */ + 'middleware' => [ + 'api' => [], + 'asset' => [], + 'docs' => [], + 'oauth2_callback' => [], + ], + + /* + * Route Group options + */ + 'group_options' => [], + ], + + 'paths' => [ + /* + * Absolute path to location where parsed annotations will be stored + */ + 'docs' => storage_path('api-docs'), + + /* + * Absolute path to directory where to export views + */ + 'views' => base_path('resources/views/vendor/l5-swagger'), + + /* + * Edit to set the api's base path + */ + 'base' => env('L5_SWAGGER_BASE_PATH', null), + + /* + * Absolute path to directories that should be excluded from scanning + * @deprecated Please use `scanOptions.exclude` + * `scanOptions.exclude` overwrites this + */ + 'excludes' => [], + ], + + 'scanOptions' => [ + /** + * Configuration for default processors. Allows to pass processors configuration to swagger-php. + * + * @link https://zircote.github.io/swagger-php/reference/processors.html + */ + 'default_processors_configuration' => [ + /** Example */ + /** + * 'operationId.hash' => true, + * 'pathFilter' => [ + * 'tags' => [ + * '/pets/', + * '/store/', + * ], + * ],. + */ + ], + + /** + * analyser: defaults to \OpenApi\StaticAnalyser . + * + * @see \OpenApi\scan + */ + 'analyser' => null, + + /** + * analysis: defaults to a new \OpenApi\Analysis . + * + * @see \OpenApi\scan + */ + 'analysis' => null, + + /** + * Custom query path processors classes. + * + * @link https://github.com/zircote/swagger-php/tree/master/Examples/processors/schema-query-parameter + * @see \OpenApi\scan + */ + 'processors' => [ + // new \App\SwaggerProcessors\SchemaQueryParameter(), + ], + + /** + * pattern: string $pattern File pattern(s) to scan (default: *.php) . + * + * @see \OpenApi\scan + */ + 'pattern' => null, + + /* + * Absolute path to directories that should be excluded from scanning + * @note This option overwrites `paths.excludes` + * @see \OpenApi\scan + */ + 'exclude' => [], + + /* + * Allows to generate specs either for OpenAPI 3.0.0 or OpenAPI 3.1.0. + * By default the spec will be in version 3.0.0 + */ + 'open_api_spec_version' => env('L5_SWAGGER_OPEN_API_SPEC_VERSION', \L5Swagger\Generator::OPEN_API_DEFAULT_SPEC_VERSION), + ], + + /* + * API security definitions. Will be generated into documentation file. + */ + 'securityDefinitions' => [ + 'securitySchemes' => [ + /* + * Examples of Security schemes + */ + /* + 'api_key_security_example' => [ // Unique name of security + 'type' => 'apiKey', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". + 'description' => 'A short description for security scheme', + 'name' => 'api_key', // The name of the header or query parameter to be used. + 'in' => 'header', // The location of the API key. Valid values are "query" or "header". + ], + 'oauth2_security_example' => [ // Unique name of security + 'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". + 'description' => 'A short description for oauth2 security scheme.', + 'flow' => 'implicit', // The flow used by the OAuth2 security scheme. Valid values are "implicit", "password", "application" or "accessCode". + 'authorizationUrl' => 'http://example.com/auth', // The authorization URL to be used for (implicit/accessCode) + //'tokenUrl' => 'http://example.com/auth' // The authorization URL to be used for (password/application/accessCode) + 'scopes' => [ + 'read:projects' => 'read your projects', + 'write:projects' => 'modify projects in your account', + ] + ], + */ + + /* Open API 3.0 support + 'passport' => [ // Unique name of security + 'type' => 'oauth2', // The type of the security scheme. Valid values are "basic", "apiKey" or "oauth2". + 'description' => 'Laravel passport oauth2 security.', + 'in' => 'header', + 'scheme' => 'https', + 'flows' => [ + "password" => [ + "authorizationUrl" => config('app.url') . '/oauth/authorize', + "tokenUrl" => config('app.url') . '/oauth/token', + "refreshUrl" => config('app.url') . '/token/refresh', + "scopes" => [] + ], + ], + ], + 'sanctum' => [ // Unique name of security + 'type' => 'apiKey', // Valid values are "basic", "apiKey" or "oauth2". + 'description' => 'Enter token in format (Bearer )', + 'name' => 'Authorization', // The name of the header or query parameter to be used. + 'in' => 'header', // The location of the API key. Valid values are "query" or "header". + ], + */ + ], + 'security' => [ + /* + * Examples of Securities + */ + [ + /* + 'oauth2_security_example' => [ + 'read', + 'write' + ], + + 'passport' => [] + */ + ], + ], + ], + + /* + * Set this to `true` in development mode so that docs would be regenerated on each request + * Set this to `false` to disable swagger generation on production + */ + 'generate_always' => env('L5_SWAGGER_GENERATE_ALWAYS', false), + + /* + * Set this to `true` to generate a copy of documentation in yaml format + */ + 'generate_yaml_copy' => env('L5_SWAGGER_GENERATE_YAML_COPY', false), + + /* + * Edit to trust the proxy's ip address - needed for AWS Load Balancer + * string[] + */ + 'proxy' => false, + + /* + * Configs plugin allows to fetch external configs instead of passing them to SwaggerUIBundle. + * See more at: https://github.com/swagger-api/swagger-ui#configs-plugin + */ + 'additional_config_url' => null, + + /* + * Apply a sort to the operation list of each API. It can be 'alpha' (sort by paths alphanumerically), + * 'method' (sort by HTTP method). + * Default is the order returned by the server unchanged. + */ + 'operations_sort' => env('L5_SWAGGER_OPERATIONS_SORT', null), + + /* + * Pass the validatorUrl parameter to SwaggerUi init on the JS side. + * A null value here disables validation. + */ + 'validator_url' => null, + + /* + * Swagger UI configuration parameters + */ + 'ui' => [ + 'display' => [ + 'dark_mode' => env('L5_SWAGGER_UI_DARK_MODE', false), + /* + * Controls the default expansion setting for the operations and tags. It can be : + * 'list' (expands only the tags), + * 'full' (expands the tags and operations), + * 'none' (expands nothing). + */ + 'doc_expansion' => env('L5_SWAGGER_UI_DOC_EXPANSION', 'none'), + + /** + * If set, enables filtering. The top bar will show an edit box that + * you can use to filter the tagged operations that are shown. Can be + * Boolean to enable or disable, or a string, in which case filtering + * will be enabled using that string as the filter expression. Filtering + * is case-sensitive matching the filter expression anywhere inside + * the tag. + */ + 'filter' => env('L5_SWAGGER_UI_FILTERS', true), // true | false + ], + + 'authorization' => [ + /* + * If set to true, it persists authorization data, and it would not be lost on browser close/refresh + */ + 'persist_authorization' => env('L5_SWAGGER_UI_PERSIST_AUTHORIZATION', false), + + 'oauth2' => [ + /* + * If set to true, adds PKCE to AuthorizationCodeGrant flow + */ + 'use_pkce_with_authorization_code_grant' => false, + ], + ], + ], + /* + * Constants which can be used in annotations + */ + 'constants' => [ + 'L5_SWAGGER_CONST_HOST' => env('L5_SWAGGER_CONST_HOST', 'http://my-default-host.com'), + ], + ], +]; diff --git a/database/migrations/2025_02_18_165714_create_contributors_table.php b/database/migrations/2025_02_18_165714_create_contributors_table.php new file mode 100644 index 0000000..d86050f --- /dev/null +++ b/database/migrations/2025_02_18_165714_create_contributors_table.php @@ -0,0 +1,35 @@ +increments('id'); + $table->string('name', 100); + $table->string('logo', 2048)->nullable(); + $table->integer('org_detail_id')->unsigned(); + $table->foreign('org_detail_id')->references('id')->on('organisation_details')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('contributors'); + } +} diff --git a/resources/views/vendor/l5-swagger/.gitkeep b/resources/views/vendor/l5-swagger/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/resources/views/vendor/l5-swagger/index.blade.php b/resources/views/vendor/l5-swagger/index.blade.php new file mode 100644 index 0000000..7d346a7 --- /dev/null +++ b/resources/views/vendor/l5-swagger/index.blade.php @@ -0,0 +1,167 @@ + + + + + {{config('l5-swagger.documentations.'.$documentation.'.api.title')}} + + + + + @if(config('l5-swagger.defaults.ui.display.dark_mode')) + + @endif + + + +
+ + + + + +