From 6c6412c9ddcbfbe7c2ce125b91144c9ce52d6ed7 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 14 Nov 2024 09:57:59 -0800 Subject: [PATCH 01/21] [TM-1469] Add is_test column to projects and orgs. --- .../Organisation/OrganisationLiteResource.php | 1 + .../V2/Projects/ProjectLiteResource.php | 1 + app/Models/V2/Organisation.php | 1 + app/Models/V2/Projects/Project.php | 2 ++ ...2024_11_14_173836_add_is_test_proj_org.php | 33 +++++++++++++++++++ routes/api_v2.php | 2 +- 6 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 database/migrations/2024_11_14_173836_add_is_test_proj_org.php diff --git a/app/Http/Resources/V2/Organisation/OrganisationLiteResource.php b/app/Http/Resources/V2/Organisation/OrganisationLiteResource.php index 0236cf03a..3c77f9238 100644 --- a/app/Http/Resources/V2/Organisation/OrganisationLiteResource.php +++ b/app/Http/Resources/V2/Organisation/OrganisationLiteResource.php @@ -12,6 +12,7 @@ public function toArray($request) 'uuid' => $this->uuid, 'name' => $this->name, 'status' => $this->status, + 'is_test' => $this->is_test, 'readable_status' => $this->readable_status, 'type' => $this->type, 'updated_at' => $this->updated_at, diff --git a/app/Http/Resources/V2/Projects/ProjectLiteResource.php b/app/Http/Resources/V2/Projects/ProjectLiteResource.php index 23fd777be..e786e05f2 100644 --- a/app/Http/Resources/V2/Projects/ProjectLiteResource.php +++ b/app/Http/Resources/V2/Projects/ProjectLiteResource.php @@ -11,6 +11,7 @@ public function toArray($request) { $data = [ 'uuid' => $this->uuid, + 'is_test' => $this->is_test, 'ppc_external_id' => $this->ppc_external_id ?? $this->id, 'framework_key' => $this->framework_key, 'framework_uuid' => $this->framework_uuid, diff --git a/app/Models/V2/Organisation.php b/app/Models/V2/Organisation.php index 32647e301..6bd2593c0 100644 --- a/app/Models/V2/Organisation.php +++ b/app/Models/V2/Organisation.php @@ -158,6 +158,7 @@ class Organisation extends Model implements MediaModel ]; public $casts = [ + 'is_test' => 'boolean', 'private' => 'boolean', 'founding_date' => 'date', 'fin_start_month' => 'integer', diff --git a/app/Models/V2/Projects/Project.php b/app/Models/V2/Projects/Project.php index 26cfb53c7..a00ae3af1 100644 --- a/app/Models/V2/Projects/Project.php +++ b/app/Models/V2/Projects/Project.php @@ -74,6 +74,7 @@ class Project extends Model implements MediaModel, AuditableContract, EntityMode protected $fillable = [ 'name', 'status', + 'is_test', 'update_request_status', 'project_status', 'framework_key', @@ -185,6 +186,7 @@ class Project extends Model implements MediaModel, AuditableContract, EntityMode ]; public $casts = [ + 'is_test' => 'boolean', 'land_tenures' => 'array', 'land_tenure_project_area' => 'array', 'land_use_types' => 'array', diff --git a/database/migrations/2024_11_14_173836_add_is_test_proj_org.php b/database/migrations/2024_11_14_173836_add_is_test_proj_org.php new file mode 100644 index 000000000..c810c430a --- /dev/null +++ b/database/migrations/2024_11_14_173836_add_is_test_proj_org.php @@ -0,0 +1,33 @@ +boolean('is_test')->default(false); + }); + Schema::table('v2_projects', function (Blueprint $table) { + $table->boolean('is_test')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('organisations', function (Blueprint $table) { + $table->dropColumn('is_test'); + }); + Schema::table('v2_projects', function (Blueprint $table) { + $table->dropColumn('is_test'); + }); + } +}; diff --git a/routes/api_v2.php b/routes/api_v2.php index 8c668b1af..f3a2b2fdd 100644 --- a/routes/api_v2.php +++ b/routes/api_v2.php @@ -286,6 +286,7 @@ Route::put('{framework}', AdminUpdateReportingFrameworkController::class); }); + Route::resource('organisations', AdminOrganisationController::class)->except('create'); Route::prefix('organisations')->group(function () { Route::get('multi', AdminOrganisationMultiController::class); Route::put('approve', AdminApproveOrganisationController::class); @@ -343,7 +344,6 @@ Route::get('site-reports', AdminIndexSiteReportsController::class); Route::get('project-reports', AdminIndexProjectReportsController::class); - Route::resource('organisations', AdminOrganisationController::class)->except('create'); Route::prefix('funding-programme/stage')->group(function () { Route::post('/', StoreStageController::class); Route::patch('/{stage}', UpdateStageController::class); From eae8a4ce875480ee29bb0c94bb4c0d39d43f697e Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 14 Nov 2024 10:19:39 -0800 Subject: [PATCH 02/21] [TM-1469] Include is_test in the full resource as well. --- app/Http/Resources/V2/Organisation/OrganisationResource.php | 1 + app/Http/Resources/V2/Projects/ProjectResource.php | 1 + 2 files changed, 2 insertions(+) diff --git a/app/Http/Resources/V2/Organisation/OrganisationResource.php b/app/Http/Resources/V2/Organisation/OrganisationResource.php index 7491298ce..fa1ac03a7 100644 --- a/app/Http/Resources/V2/Organisation/OrganisationResource.php +++ b/app/Http/Resources/V2/Organisation/OrganisationResource.php @@ -17,6 +17,7 @@ public function toArray($request) { $data = [ 'uuid' => $this->uuid, + 'is_test' => $this->is_test, 'status' => $this->status, 'readable_status' => $this->readable_status, 'type' => $this->type, diff --git a/app/Http/Resources/V2/Projects/ProjectResource.php b/app/Http/Resources/V2/Projects/ProjectResource.php index 4aaafef8d..55b4eb861 100644 --- a/app/Http/Resources/V2/Projects/ProjectResource.php +++ b/app/Http/Resources/V2/Projects/ProjectResource.php @@ -15,6 +15,7 @@ public function toArray($request) 'ppc_external_id' => $this->ppc_external_id ?? $this->id, 'name' => $this->name, 'status' => $this->status, + 'is_test' => $this->is_test, 'readable_status' => $this->readable_status, 'project_status' => $this->project_status, 'update_request_status' => $this->update_request_status, From 88dbf57a44b25a641aedef749c1add5fb0a36039 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 14 Nov 2024 13:50:21 -0800 Subject: [PATCH 03/21] [TM-1469] Accept is_test as an org update param. --- app/Http/Requests/V2/Organisations/UpdateOrganisationRequest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Requests/V2/Organisations/UpdateOrganisationRequest.php b/app/Http/Requests/V2/Organisations/UpdateOrganisationRequest.php index 425358b41..9bb1a96d6 100644 --- a/app/Http/Requests/V2/Organisations/UpdateOrganisationRequest.php +++ b/app/Http/Requests/V2/Organisations/UpdateOrganisationRequest.php @@ -15,6 +15,7 @@ public function rules() 'type' => 'sometimes|nullable|string', 'subtype' => 'sometimes|nullable|string', 'private' => 'sometimes|boolean', + 'is_test' => 'sometimes|boolean', 'name' => 'sometimes|nullable|string', 'phone' => 'sometimes|nullable|string', 'hq_address' => 'sometimes|nullable|string', From 3b3265636534477bff2a42d8b3688441e57986ea Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 14 Nov 2024 15:00:54 -0800 Subject: [PATCH 04/21] [TM-1469] Add controller to update is_test on projects from admin. --- .../Projects/AdminUpdateProjectController.php | 20 + .../V2/Projects/AdminUpdateProjectRequest.php | 15 + .../V2/definitions/ProjectFullRead.yml | 2 + .../V2/definitions/ProjectLiteRead.yml | 2 + .../definitions/V2AdminOrganisationRead.yml | 2 + .../definitions/V2AdminOrganisationUpdate.yml | 4 +- .../V2/definitions/V2AdminProjectUpdate.yml | 5 + openapi-src/V2/definitions/_index.yml | 2 + .../paths/Projects/put-v2-admin-project.yml | 20 + openapi-src/V2/paths/_index.yml | 2 + resources/docs/swagger-v2.yml | 2704 +++++++++++++++++ routes/api_v2.php | 2 + 12 files changed, 2779 insertions(+), 1 deletion(-) create mode 100644 app/Http/Controllers/V2/Projects/AdminUpdateProjectController.php create mode 100644 app/Http/Requests/V2/Projects/AdminUpdateProjectRequest.php create mode 100644 openapi-src/V2/definitions/V2AdminProjectUpdate.yml create mode 100644 openapi-src/V2/paths/Projects/put-v2-admin-project.yml diff --git a/app/Http/Controllers/V2/Projects/AdminUpdateProjectController.php b/app/Http/Controllers/V2/Projects/AdminUpdateProjectController.php new file mode 100644 index 000000000..0cd695e10 --- /dev/null +++ b/app/Http/Controllers/V2/Projects/AdminUpdateProjectController.php @@ -0,0 +1,20 @@ +authorize('update', $project); + + $project->update($request->validated()); + + return new ProjectResource($project); + } +} diff --git a/app/Http/Requests/V2/Projects/AdminUpdateProjectRequest.php b/app/Http/Requests/V2/Projects/AdminUpdateProjectRequest.php new file mode 100644 index 000000000..e8ba4a3e0 --- /dev/null +++ b/app/Http/Requests/V2/Projects/AdminUpdateProjectRequest.php @@ -0,0 +1,15 @@ + 'sometimes|boolean', + ]; + } +} diff --git a/openapi-src/V2/definitions/ProjectFullRead.yml b/openapi-src/V2/definitions/ProjectFullRead.yml index a0b1bfc8f..703ef1311 100644 --- a/openapi-src/V2/definitions/ProjectFullRead.yml +++ b/openapi-src/V2/definitions/ProjectFullRead.yml @@ -5,6 +5,8 @@ properties: type: string uuid: type: string + is_test: + type: boolean status: type: string organisation: diff --git a/openapi-src/V2/definitions/ProjectLiteRead.yml b/openapi-src/V2/definitions/ProjectLiteRead.yml index 01f46f1cf..5c4162c86 100644 --- a/openapi-src/V2/definitions/ProjectLiteRead.yml +++ b/openapi-src/V2/definitions/ProjectLiteRead.yml @@ -5,6 +5,8 @@ properties: type: string uuid: type: string + is_test: + type: boolean status: type: string name: diff --git a/openapi-src/V2/definitions/V2AdminOrganisationRead.yml b/openapi-src/V2/definitions/V2AdminOrganisationRead.yml index 71d295f39..4317e11c7 100644 --- a/openapi-src/V2/definitions/V2AdminOrganisationRead.yml +++ b/openapi-src/V2/definitions/V2AdminOrganisationRead.yml @@ -9,6 +9,8 @@ properties: type: string type: type: string + is_test: + type: boolean private: type: boolean name: diff --git a/openapi-src/V2/definitions/V2AdminOrganisationUpdate.yml b/openapi-src/V2/definitions/V2AdminOrganisationUpdate.yml index f50bc52a3..1f2f41a09 100644 --- a/openapi-src/V2/definitions/V2AdminOrganisationUpdate.yml +++ b/openapi-src/V2/definitions/V2AdminOrganisationUpdate.yml @@ -5,6 +5,8 @@ properties: type: string private: type: boolean + is_test: + type: boolean name: type: string phone: @@ -148,4 +150,4 @@ properties: tags: type: array items: - type: string \ No newline at end of file + type: string diff --git a/openapi-src/V2/definitions/V2AdminProjectUpdate.yml b/openapi-src/V2/definitions/V2AdminProjectUpdate.yml new file mode 100644 index 000000000..9cc9cddad --- /dev/null +++ b/openapi-src/V2/definitions/V2AdminProjectUpdate.yml @@ -0,0 +1,5 @@ +title: V2AdminProjectUpdate +type: object +properties: + is_test: + type: boolean diff --git a/openapi-src/V2/definitions/_index.yml b/openapi-src/V2/definitions/_index.yml index 4e5cf1a93..371955c1c 100644 --- a/openapi-src/V2/definitions/_index.yml +++ b/openapi-src/V2/definitions/_index.yml @@ -390,3 +390,5 @@ DashboardIndicatorHectaresRestorationData: $ref: './DashboardIndicatorHectaresRestorationData.yml' UserCreateComplete: $ref: './UserCreateComplete.yml' +V2AdminProjectUpdate: + $ref: './V2AdminProjectUpdate.yml' diff --git a/openapi-src/V2/paths/Projects/put-v2-admin-project.yml b/openapi-src/V2/paths/Projects/put-v2-admin-project.yml new file mode 100644 index 000000000..23f92e7af --- /dev/null +++ b/openapi-src/V2/paths/Projects/put-v2-admin-project.yml @@ -0,0 +1,20 @@ +summary: Updates a specific project +operationId: put-v2-admin-project +tags: + - V2 Projects +parameters: + - in: body + name: body + schema: + $ref: '../../definitions/_index.yml#/V2AdminProjectUpdate' + - type: string + in: path + name: UUID + required: true +responses: + '200': + description: OK + schema: + type: array + items: + $ref: '../../definitions/_index.yml#/ProjectFullRead' diff --git a/openapi-src/V2/paths/_index.yml b/openapi-src/V2/paths/_index.yml index ccc430654..9ee3560b2 100644 --- a/openapi-src/V2/paths/_index.yml +++ b/openapi-src/V2/paths/_index.yml @@ -2422,6 +2422,8 @@ delete: $ref: './Sites/Monitoring/delete-v2-admin-site-monitorings-uuid.yml' '/v2/admin/projects/{UUID}': + put: + $ref: './Projects/put-v2-admin-project.yml' delete: summary: Delete a project operationId: delete-v2-admin-projects-UUID diff --git a/resources/docs/swagger-v2.yml b/resources/docs/swagger-v2.yml index fba493d25..0022ab99b 100644 --- a/resources/docs/swagger-v2.yml +++ b/resources/docs/swagger-v2.yml @@ -228,6 +228,8 @@ definitions: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -1141,6 +1143,8 @@ definitions: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -3614,6 +3618,8 @@ definitions: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -4874,6 +4880,8 @@ definitions: type: string private: type: boolean + is_test: + type: boolean name: type: string phone: @@ -7595,6 +7603,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -8817,6 +8827,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -9758,6 +9770,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string organisation: @@ -11446,6 +11460,8 @@ definitions: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -16306,6 +16322,8 @@ definitions: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -18216,6 +18234,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -19165,6 +19185,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -21062,6 +21084,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -22011,6 +22035,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -23925,6 +23951,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -24874,6 +24902,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -26769,6 +26799,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -27718,6 +27750,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -30648,6 +30682,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -31605,6 +31641,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -32574,6 +32612,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -33564,6 +33604,8 @@ definitions: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -34475,6 +34517,8 @@ definitions: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -36304,6 +36348,8 @@ definitions: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -37286,6 +37332,8 @@ definitions: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -38197,6 +38245,8 @@ definitions: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -41368,6 +41418,8 @@ definitions: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -44239,6 +44291,12 @@ definitions: type: string role: type: string + V2AdminProjectUpdate: + title: V2AdminProjectUpdate + type: object + properties: + is_test: + type: boolean paths: '/v2/tree-species/{entity}/{UUID}': get: @@ -44325,6 +44383,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -45291,6 +45351,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -46265,6 +46327,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -47292,6 +47356,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -48241,6 +48307,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -50370,6 +50438,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -51319,6 +51389,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -53267,6 +53339,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -54216,6 +54290,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -59670,6 +59746,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -60601,6 +60679,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -61487,6 +61567,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -62363,6 +62445,8 @@ paths: type: string private: type: boolean + is_test: + type: boolean name: type: string phone: @@ -62556,6 +62640,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -63457,6 +63543,8 @@ paths: type: string private: type: boolean + is_test: + type: boolean name: type: string phone: @@ -63659,6 +63747,8 @@ paths: type: string private: type: boolean + is_test: + type: boolean name: type: string phone: @@ -66282,6 +66372,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -67210,6 +67302,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -70424,6 +70518,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -71407,6 +71503,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string organisation: @@ -73095,6 +73193,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -74143,6 +74243,8 @@ paths: type: string uuid: type: string + is_test: + type: boolean status: type: string name: @@ -84239,6 +84341,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -86480,6 +86584,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -88709,6 +88815,8 @@ paths: type: string type: type: string + is_test: + type: boolean private: type: boolean name: @@ -89785,6 +89893,2602 @@ paths: '200': description: OK '/v2/admin/projects/{UUID}': + put: + summary: Updates a specific project + operationId: put-v2-admin-project + tags: + - V2 Projects + parameters: + - in: body + name: body + schema: + title: V2AdminProjectUpdate + type: object + properties: + is_test: + type: boolean + - type: string + in: path + name: UUID + required: true + responses: + '200': + description: OK + schema: + type: array + items: + title: ProjectLiteRead + type: object + properties: + id: + type: string + uuid: + type: string + is_test: + type: boolean + status: + type: string + organisation: + title: OrganisationRead + type: object + properties: + uuid: + type: string + type: + type: string + private: + type: boolean + name: + type: string + phone: + type: string + currency: + type: string + states: + type: array + items: + type: string + loan_status_types: + type: array + items: + type: string + land_systems: + type: array + items: + type: string + fund_utilisation: + type: array + items: + type: string + detailed_intervention_types: + type: array + items: + type: string + account_number_1: + type: string + account_number_2: + type: string + approach_of_marginalized_communities: + type: string + community_engagement_numbers_marginalized: + type: string + founding_date: + type: string + description: + type: string + leadership_team: + type: string + countries: + type: array + items: + type: string + languages: + type: array + items: + type: string + project_pitches: + type: array + items: + title: ProjectPitchRead + x-stoplight: + id: rsuskf5on1pya + type: object + properties: + id: + type: string + uuid: + type: string + status: + type: string + readable_status: + type: string + organisation_id: + type: string + funding_programmes: + title: LimitedFundingProgrammeRead + x-stoplight: + id: uh5oce75en4za + type: object + properties: + id: + type: integer + uuid: + type: string + name: + type: string + description: + type: string + read_more_url: + type: string + organisation_types: + type: array + items: + type: string + location: + type: string + status: + type: string + tree_species: + type: array + items: + title: V2TreeSpeciesRead + x-stoplight: + id: 04vvsvyndvy8n + type: object + properties: + uuid: + type: string + name: + type: string + amount: + type: integer + type: + type: string + collection: + type: string + project_name: + type: string + how_discovered: + type: string + project_objectives: + type: string + project_country: + type: array + items: + type: string + project_county_district: + type: string + restoration_intervention_types: + type: array + items: + type: string + land_systems: + type: array + items: + type: string + tree_restoration_practices: + type: array + items: + type: string + total_hectares: + type: integer + project_budget: + type: integer + total_trees: + type: integer + capacity_building_needs: + type: array + items: + type: string + additional: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + restoration_photos: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + cover: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + proof_of_land_tenure_mou: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + detailed_project_budget: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + expected_active_restoration_start_date: + type: string + expected_active_restoration_end_date: + type: string + description_of_project_timeline: + type: string + proj_partner_info: + type: string + land_tenure_proj_area: + type: array + items: + type: string + landholder_comm_engage: + type: string + proj_success_risks: + type: string + monitor_eval_plan: + type: string + proj_boundary: + type: string + sustainable_dev_goals: + type: array + items: + type: string + proj_area_description: + type: string + proposed_num_sites: + type: integer + minimum: 0 + maximum: 4294967295 + environmental_goals: + type: string + main_degradation_causes: + type: string + seedlings_source: + type: string + proposed_num_nurseries: + type: integer + minimum: 0 + maximum: 4294967295 + curr_land_degradation: + type: string + proj_impact_socieconom: + type: string + proj_impact_foodsec: + type: string + proj_impact_watersec: + type: string + proj_impact_jobtypes: + type: string + num_jobs_created: + type: integer + minimum: 0 + maximum: 4294967295 + pct_employees_men: + type: integer + minimum: 0 + maximum: 100 + pct_employees_women: + type: integer + minimum: 0 + maximum: 100 + pct_employees_18to35: + type: integer + minimum: 0 + maximum: 100 + pct_employees_older35: + type: integer + minimum: 0 + maximum: 100 + proj_beneficiaries: + type: integer + pct_beneficiaries_women: + type: integer + minimum: 0 + maximum: 100 + pct_beneficiaries_small: + type: integer + minimum: 0 + maximum: 100 + pct_beneficiaries_large: + type: integer + minimum: 0 + maximum: 100 + pct_beneficiaries_youth: + type: integer + minimum: 0 + maximum: 100 + pct_beneficiaries_scheduled_classes: + type: integer + minimum: 0 + maximum: 100 + pct_beneficiaries_scheduled_tribes: + type: integer + minimum: 0 + maximum: 100 + monitoring_evaluation_plan: + type: string + main_causes_of_degradation: + type: string + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + tree_species: + type: array + items: + title: V2TreeSpeciesRead + x-stoplight: + id: 04vvsvyndvy8n + type: object + properties: + uuid: + type: string + name: + type: string + amount: + type: integer + type: + type: string + collection: + type: string + web_url: + type: string + facebook_url: + type: string + instagram_url: + type: string + linkedin_url: + type: string + twitter_url: + type: string + hq_street_1: + type: string + hq_street_2: + type: string + hq_city: + type: string + hq_state: + type: string + hq_zipcode: + type: string + hq_country: + type: string + fin_start_month: + type: integer + fin_budget_3year: + type: number + format: float + fin_budget_2year: + type: number + format: float + fin_budget_1year: + type: number + format: float + fin_budget_current_year: + type: number + format: float + ha_restored_total: + type: number + format: float + ha_restored_3year: + type: number + format: float + relevant_experience_years: + type: integer + trees_grown_total: + type: integer + trees_grown_3year: + type: integer + tree_care_approach: + type: string + ft_permanent_employees: + type: integer + pt_permanent_employees: + type: integer + temp_employees: + type: integer + female_employees: + type: integer + male_employees: + type: integer + young_employees: + type: integer + additional_funding_details: + type: string + community_experience: + type: string + total_engaged_community_members_3yr: + type: integer + percent_engaged_women_3yr: + type: integer + percent_engaged_men_3yr: + type: integer + percent_engaged_under_35_3yr: + type: integer + percent_engaged_over_35_3yr: + type: integer + percent_engaged_smallholder_3yr: + type: integer + total_trees_grown: + type: integer + avg_tree_survival_rate: + type: integer + tree_maintenance_aftercare_approach: + type: string + restored_areas_description: + type: string + monitoring_evaluation_experience: + type: string + funding_history: + type: string + engagement_farmers: + type: array + items: + type: string + engagement_women: + type: array + items: + type: string + engagement_youth: + type: array + items: + type: string + engagement_non_youth: + type: array + items: + type: string + tree_restoration_practices: + type: array + items: + type: string + business_model: + type: string + subtype: + type: string + organisation_revenue_this_year: + type: number + shapefiles: + type: array + items: + title: ShapefileRead + type: object + properties: + uuid: + type: string + shapefileable_type: + type: string + shapefileable_id: + type: integer + geojson: + type: string + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + bank_statements: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + previous_annual_reports: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + logo: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + cover: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + reference: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + additional: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + op_budget_2year: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + op_budget_last_year: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + op_budget_this_year: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + op_budget_next_year: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + legal_registration: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + tags: + type: array + description: 'this is a list of key value pairs eg slug: name ' + items: + type: string + application: + title: ApplicationLiteRead + type: object + properties: + uuid: + type: string + form_submissions: + type: array + items: + type: object + title: FormSubmissionRead + x-stoplight: + id: u7ul6m0rkvj6m + properties: + id: + type: string + uuid: + type: string + name: + type: string + form: + title: FormRead + x-stoplight: + id: e0u0etkn01z17 + type: object + properties: + id: + type: integer + uuid: + type: string + type: + type: string + version: + type: integer + title: + type: string + subtitle: + type: string + description: + type: string + framework_key: + type: string + duration: + type: string + deadline_at: + type: string + documentation: + type: string + documentation_label: + type: string + submission_message: + type: string + published: + type: boolean + stage_id: + type: string + options_other: + type: boolean + form_sections: + type: array + items: + title: FormSectionRead + x-stoplight: + id: scpnof2zuz3q3 + type: object + properties: + order: + type: integer + form_id: + type: integer + form_questions: + type: array + items: + title: FormQuestionRead + type: object + properties: + id: + type: integer + uuid: + type: string + form_section_id: + type: integer + label: + type: string + validation: + type: array + items: + type: string + parent_id: + type: string + linked_field_key: + type: string + children: + type: array + items: + type: object + multichoice: + type: boolean + order: + type: integer + options: + type: array + items: + title: FormQuestionOptionRead + x-stoplight: + id: ihpffrpq780e4 + type: object + properties: + id: + type: integer + uuid: + type: string + form_question_id: + type: integer + label: + type: string + order: + type: integer + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + table_headers: + type: array + items: + title: FormTableHeaderRead + type: object + properties: + id: + type: integer + uuid: + type: string + form_question_id: + type: integer + label: + type: string + order: + type: integer + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + additional_text: + type: string + additional_url: + type: string + show_on_parent_condition: + type: boolean + input_type: + type: string + enum: + - date + - text + - long-text + - select + - checkboxes + - radio + - number + - image + - file + - conditional + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + tags: + type: array + description: 'this is a list of key value pairs eg. slug: name' + items: + type: string + updated_by: + type: integer + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + stage: + title: StageLiteRead + type: object + properties: + uuid: + type: string + name: + type: string + status: + type: string + readable_status: + type: string + answers: + type: string + status: + type: string + readable_status: + type: string + audits: + type: array + items: + title: AuditRead + type: object + properties: + id: + type: integer + event: + type: string + user_id: + type: integer + user_uuid: + type: string + old_values: + type: object + new_values: + type: object + created_at: + type: string + updated_at: + type: string + tags: + type: array + description: 'this is a list of key value pairs eg slug: name ' + items: + type: string + project_pitch_uuid: + type: string + updated_by: + type: string + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + current_submission: + type: object + title: FormSubmissionRead + x-stoplight: + id: u7ul6m0rkvj6m + properties: + id: + type: string + uuid: + type: string + name: + type: string + form: + title: FormRead + x-stoplight: + id: e0u0etkn01z17 + type: object + properties: + id: + type: integer + uuid: + type: string + type: + type: string + version: + type: integer + title: + type: string + subtitle: + type: string + description: + type: string + framework_key: + type: string + duration: + type: string + deadline_at: + type: string + documentation: + type: string + documentation_label: + type: string + submission_message: + type: string + published: + type: boolean + stage_id: + type: string + options_other: + type: boolean + form_sections: + type: array + items: + title: FormSectionRead + x-stoplight: + id: scpnof2zuz3q3 + type: object + properties: + order: + type: integer + form_id: + type: integer + form_questions: + type: array + items: + title: FormQuestionRead + type: object + properties: + id: + type: integer + uuid: + type: string + form_section_id: + type: integer + label: + type: string + validation: + type: array + items: + type: string + parent_id: + type: string + linked_field_key: + type: string + children: + type: array + items: + type: object + multichoice: + type: boolean + order: + type: integer + options: + type: array + items: + title: FormQuestionOptionRead + x-stoplight: + id: ihpffrpq780e4 + type: object + properties: + id: + type: integer + uuid: + type: string + form_question_id: + type: integer + label: + type: string + order: + type: integer + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + table_headers: + type: array + items: + title: FormTableHeaderRead + type: object + properties: + id: + type: integer + uuid: + type: string + form_question_id: + type: integer + label: + type: string + order: + type: integer + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + additional_text: + type: string + additional_url: + type: string + show_on_parent_condition: + type: boolean + input_type: + type: string + enum: + - date + - text + - long-text + - select + - checkboxes + - radio + - number + - image + - file + - conditional + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + tags: + type: array + description: 'this is a list of key value pairs eg. slug: name' + items: + type: string + updated_by: + type: integer + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + stage: + title: StageLiteRead + type: object + properties: + uuid: + type: string + name: + type: string + status: + type: string + readable_status: + type: string + answers: + type: string + status: + type: string + readable_status: + type: string + audits: + type: array + items: + title: AuditRead + type: object + properties: + id: + type: integer + event: + type: string + user_id: + type: integer + user_uuid: + type: string + old_values: + type: object + new_values: + type: object + created_at: + type: string + updated_at: + type: string + tags: + type: array + description: 'this is a list of key value pairs eg slug: name ' + items: + type: string + project_pitch_uuid: + type: string + updated_by: + type: string + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + funding_programme: + type: object + title: FundingProgrammeRead + x-stoplight: + id: uh5oce75en4za + properties: + id: + type: integer + uuid: + type: string + name: + type: string + description: + type: string + location: + type: string + read_more_url: + type: string + framework_key: + type: string + status: + type: string + organisation_types: + type: array + items: + type: string + stages: + type: array + items: + title: StageRead + x-stoplight: + id: qtkhrj889wvwc + type: object + properties: + id: + type: integer + uuid: + type: string + status: + type: string + deadline_at: + type: string + readable_status: + type: string + funding_programme_id: + type: integer + name: + type: string + order: + type: integer + forms: + title: FormRead + x-stoplight: + id: e0u0etkn01z17 + type: object + properties: + id: + type: integer + uuid: + type: string + type: + type: string + version: + type: integer + title: + type: string + subtitle: + type: string + description: + type: string + framework_key: + type: string + duration: + type: string + deadline_at: + type: string + documentation: + type: string + documentation_label: + type: string + submission_message: + type: string + published: + type: boolean + stage_id: + type: string + options_other: + type: boolean + form_sections: + type: array + items: + title: FormSectionRead + x-stoplight: + id: scpnof2zuz3q3 + type: object + properties: + order: + type: integer + form_id: + type: integer + form_questions: + type: array + items: + title: FormQuestionRead + type: object + properties: + id: + type: integer + uuid: + type: string + form_section_id: + type: integer + label: + type: string + validation: + type: array + items: + type: string + parent_id: + type: string + linked_field_key: + type: string + children: + type: array + items: + type: object + multichoice: + type: boolean + order: + type: integer + options: + type: array + items: + title: FormQuestionOptionRead + x-stoplight: + id: ihpffrpq780e4 + type: object + properties: + id: + type: integer + uuid: + type: string + form_question_id: + type: integer + label: + type: string + order: + type: integer + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + table_headers: + type: array + items: + title: FormTableHeaderRead + type: object + properties: + id: + type: integer + uuid: + type: string + form_question_id: + type: integer + label: + type: string + order: + type: integer + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + additional_text: + type: string + additional_url: + type: string + show_on_parent_condition: + type: boolean + input_type: + type: string + enum: + - date + - text + - long-text + - select + - checkboxes + - radio + - number + - image + - file + - conditional + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + tags: + type: array + description: 'this is a list of key value pairs eg. slug: name' + items: + type: string + updated_by: + type: integer + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + organisations: + type: array + items: + type: object + properties: + uuid: + type: string + name: + type: string + cover: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + organisation: + type: object + title: AdminOrganisationRead + properties: + uuid: + type: string + status: + type: string + readable_status: + type: string + type: + type: string + is_test: + type: boolean + private: + type: boolean + name: + type: string + phone: + type: string + founding_date: + type: string + description: + type: string + countries: + type: array + items: + type: string + languages: + type: array + items: + type: string + tree_species: + type: array + items: + title: V2TreeSpeciesRead + x-stoplight: + id: 04vvsvyndvy8n + type: object + properties: + uuid: + type: string + name: + type: string + amount: + type: integer + type: + type: string + collection: + type: string + project_pitches: + type: array + items: + title: ProjectPitchRead + x-stoplight: + id: rsuskf5on1pya + type: object + properties: + id: + type: string + uuid: + type: string + status: + type: string + readable_status: + type: string + organisation_id: + type: string + funding_programmes: + title: LimitedFundingProgrammeRead + x-stoplight: + id: uh5oce75en4za + type: object + properties: + id: + type: integer + uuid: + type: string + name: + type: string + description: + type: string + read_more_url: + type: string + organisation_types: + type: array + items: + type: string + location: + type: string + status: + type: string + tree_species: + type: array + items: + title: V2TreeSpeciesRead + x-stoplight: + id: 04vvsvyndvy8n + type: object + properties: + uuid: + type: string + name: + type: string + amount: + type: integer + type: + type: string + collection: + type: string + project_name: + type: string + how_discovered: + type: string + project_objectives: + type: string + project_country: + type: array + items: + type: string + project_county_district: + type: string + restoration_intervention_types: + type: array + items: + type: string + land_systems: + type: array + items: + type: string + tree_restoration_practices: + type: array + items: + type: string + total_hectares: + type: integer + project_budget: + type: integer + total_trees: + type: integer + capacity_building_needs: + type: array + items: + type: string + additional: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + restoration_photos: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + cover: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + proof_of_land_tenure_mou: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + detailed_project_budget: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + expected_active_restoration_start_date: + type: string + expected_active_restoration_end_date: + type: string + description_of_project_timeline: + type: string + proj_partner_info: + type: string + land_tenure_proj_area: + type: array + items: + type: string + landholder_comm_engage: + type: string + proj_success_risks: + type: string + monitor_eval_plan: + type: string + proj_boundary: + type: string + sustainable_dev_goals: + type: array + items: + type: string + proj_area_description: + type: string + proposed_num_sites: + type: integer + minimum: 0 + maximum: 4294967295 + environmental_goals: + type: string + main_degradation_causes: + type: string + seedlings_source: + type: string + proposed_num_nurseries: + type: integer + minimum: 0 + maximum: 4294967295 + curr_land_degradation: + type: string + proj_impact_socieconom: + type: string + proj_impact_foodsec: + type: string + proj_impact_watersec: + type: string + proj_impact_jobtypes: + type: string + num_jobs_created: + type: integer + minimum: 0 + maximum: 4294967295 + pct_employees_men: + type: integer + minimum: 0 + maximum: 100 + pct_employees_women: + type: integer + minimum: 0 + maximum: 100 + pct_employees_18to35: + type: integer + minimum: 0 + maximum: 100 + pct_employees_older35: + type: integer + minimum: 0 + maximum: 100 + proj_beneficiaries: + type: integer + pct_beneficiaries_women: + type: integer + minimum: 0 + maximum: 100 + pct_beneficiaries_small: + type: integer + minimum: 0 + maximum: 100 + pct_beneficiaries_large: + type: integer + minimum: 0 + maximum: 100 + pct_beneficiaries_youth: + type: integer + minimum: 0 + maximum: 100 + pct_beneficiaries_scheduled_classes: + type: integer + minimum: 0 + maximum: 100 + pct_beneficiaries_scheduled_tribes: + type: integer + minimum: 0 + maximum: 100 + monitoring_evaluation_plan: + type: string + main_causes_of_degradation: + type: string + deleted_at: + type: string + created_at: + type: string + updated_at: + type: string + web_url: + type: string + facebook_url: + type: string + instagram_url: + type: string + linkedin_url: + type: string + twitter_url: + type: string + hq_street_1: + type: string + hq_street_2: + type: string + hq_city: + type: string + hq_state: + type: string + hq_zipcode: + type: string + hq_country: + type: string + fin_start_month: + type: integer + fin_budget_3year: + type: number + format: float + fin_budget_2year: + type: number + format: float + fin_budget_1year: + type: number + format: float + fin_budget_current_year: + type: number + format: float + ha_restored_total: + type: number + format: float + ha_restored_3year: + type: number + format: float + relevant_experience_years: + type: integer + trees_grown_total: + type: integer + trees_grown_3year: + type: integer + tree_care_approach: + type: string + ft_permanent_employees: + type: integer + pt_permanent_employees: + type: integer + temp_employees: + type: integer + female_employees: + type: integer + male_employees: + type: integer + young_employees: + type: integer + additional_funding_details: + type: string + community_experience: + type: string + total_engaged_community_members_3yr: + type: integer + percent_engaged_women_3yr: + type: integer + percent_engaged_men_3yr: + type: integer + percent_engaged_under_35_3yr: + type: integer + percent_engaged_over_35_3yr: + type: integer + percent_engaged_smallholder_3yr: + type: integer + total_trees_grown: + type: integer + avg_tree_survival_rate: + type: integer + tree_maintenance_aftercare_approach: + type: string + restored_areas_description: + type: string + monitoring_evaluation_experience: + type: string + funding_history: + type: string + shapefiles: + type: array + items: + title: ShapefileRead + type: object + properties: + uuid: + type: string + shapefileable_type: + type: string + shapefileable_id: + type: integer + geojson: + type: string + created_at: + type: string + updated_at: + type: string + deleted_at: + type: string + bank_statements: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + previous_annual_reports: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + logo: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + cover: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + reference: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + additional: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + op_budget_2year: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + op_budget_last_year: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + op_budget_this_year: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + op_budget_next_year: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + legal_registration: + type: array + items: + title: V2FileRead + type: object + properties: + uuid: + type: string + url: + type: string + thumb_url: + type: string + collection_name: + type: string + title: + type: string + file_name: + type: string + mime_type: + type: string + size: + type: integer + lat: + type: integer + lng: + type: integer + is_public: + type: boolean + is_cover: + type: boolean + created_at: + type: string + tags: + type: array + description: 'this is a list of key value pairs eg. slug: name' + items: + type: string + created_at: + type: string + updated_at: + type: string + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + planting_start_date: + type: string + framework_key: + type: string + framework_uuid: + type: string + has_monitoring_data: + type: boolean delete: summary: Delete a project operationId: delete-v2-admin-projects-UUID diff --git a/routes/api_v2.php b/routes/api_v2.php index f3a2b2fdd..c327f07ef 100644 --- a/routes/api_v2.php +++ b/routes/api_v2.php @@ -149,6 +149,7 @@ use App\Http\Controllers\V2\ProjectReports\ProjectReportsViaProjectController; use App\Http\Controllers\V2\Projects\AdminIndexProjectsController; use App\Http\Controllers\V2\Projects\AdminProjectMultiController; +use App\Http\Controllers\V2\Projects\AdminUpdateProjectController; use App\Http\Controllers\V2\Projects\CreateBlankProjectWithFormController; use App\Http\Controllers\V2\Projects\CreateProjectInviteController; use App\Http\Controllers\V2\Projects\CreateProjectWithFormController; @@ -306,6 +307,7 @@ Route::prefix('projects')->group(function () { Route::get('', AdminIndexProjectsController::class); + Route::put('/{project}', AdminUpdateProjectController::class); Route::get('/multi', AdminProjectMultiController::class); }); From 4617a4b2586bcbb1f9af48bb86b0a4b5b0f71478 Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 14 Nov 2024 15:04:52 -0800 Subject: [PATCH 05/21] [TM-1469] Automatically make projects test projects if they're in a test org. --- .../Controllers/V2/Projects/CreateProjectWithFormController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/V2/Projects/CreateProjectWithFormController.php b/app/Http/Controllers/V2/Projects/CreateProjectWithFormController.php index 00af8ff4a..2f3d37c98 100644 --- a/app/Http/Controllers/V2/Projects/CreateProjectWithFormController.php +++ b/app/Http/Controllers/V2/Projects/CreateProjectWithFormController.php @@ -35,6 +35,7 @@ public function __invoke(Request $request): EntityWithSchemaResource 'organisation_id' => $application->organisation->id, 'application_id' => $application->id, 'status' => EntityStatusStateMachine::STARTED, + 'is_test' => $application->organisation->is_test, 'project_status' => null, 'name' => $projectPitch->project_name, 'boundary_geojson' => $projectPitch->proj_boundary, From e569efcc8699d0b8871e8b5c03dd55e6c18c97fe Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 14 Nov 2024 15:26:52 -0800 Subject: [PATCH 06/21] [TM-1469] Backfill test projects and orgs. --- .../OneOff/BackfillTestProjectsOrgs.php | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 app/Console/Commands/OneOff/BackfillTestProjectsOrgs.php diff --git a/app/Console/Commands/OneOff/BackfillTestProjectsOrgs.php b/app/Console/Commands/OneOff/BackfillTestProjectsOrgs.php new file mode 100644 index 000000000..7a1f63891 --- /dev/null +++ b/app/Console/Commands/OneOff/BackfillTestProjectsOrgs.php @@ -0,0 +1,56 @@ +get() as $organisation) { + $organisation->update(['is_test' => true]); + + foreach ($organisation->projects as $project) { + $project->update(['is_test' => true]); + } + } + + foreach (Project::whereIn('uuid', self::TEST_PROJECTS)->get() as $project) { + $project->update(['is_test' => true]); + } + } +} From f32a790c15ebb7d93b1679e864dc51f5a037f20f Mon Sep 17 00:00:00 2001 From: Nathan Curtis Date: Thu, 14 Nov 2024 16:22:39 -0800 Subject: [PATCH 07/21] [TM-1469] Apparently the resource has to be defined after the prefix. --- routes/api_v2.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/api_v2.php b/routes/api_v2.php index c327f07ef..2a252fb6e 100644 --- a/routes/api_v2.php +++ b/routes/api_v2.php @@ -287,13 +287,13 @@ Route::put('{framework}', AdminUpdateReportingFrameworkController::class); }); - Route::resource('organisations', AdminOrganisationController::class)->except('create'); Route::prefix('organisations')->group(function () { Route::get('multi', AdminOrganisationMultiController::class); Route::put('approve', AdminApproveOrganisationController::class); Route::put('reject', AdminRejectOrganisationController::class); Route::get('export', AdminExportOrganisationsController::class); }); + Route::resource('organisations', AdminOrganisationController::class)->except('create'); Route::prefix('update-requests')->group(function () { Route::get('', AdminIndexUpdateRequestsController::class); From 83c5d8e652eac455a7f1a67636ead0953fdde11b Mon Sep 17 00:00:00 2001 From: Limber Mamani <154026979+LimberHope@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:10:28 -0400 Subject: [PATCH 08/21] [TM-1457] Optimized query with select to retrieve only necessary fields (#570) --- app/Exports/V2/OrganisationsExport.php | 40 ++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/app/Exports/V2/OrganisationsExport.php b/app/Exports/V2/OrganisationsExport.php index 934fff1cc..7bf937e58 100644 --- a/app/Exports/V2/OrganisationsExport.php +++ b/app/Exports/V2/OrganisationsExport.php @@ -17,7 +17,41 @@ public function collection(): Collection { ini_set('max_execution_time', 60); - return Organisation::all(); + return Organisation::query()->select([ + 'uuid', + 'status', + 'type', + 'name', + 'phone', + 'hq_street_1', + 'hq_street_2', + 'hq_city', + 'hq_state', + 'hq_zipcode', + 'hq_country', + 'countries', + 'languages', + 'founding_date', + 'description', + 'web_url', + 'facebook_url', + 'instagram_url', + 'linkedin_url', + 'twitter_url', + 'fin_start_month', + 'fin_budget_3year', + 'fin_budget_2year', + 'fin_budget_1year', + 'fin_budget_current_year', + 'ha_restored_total', + 'ha_restored_3year', + 'trees_grown_total', + 'trees_grown_3year', + 'tree_care_approach', + 'relevant_experience_years', + 'updated_at', + 'created_at', + ])->get(); } public function headings(): array @@ -108,6 +142,7 @@ public function map($organisation): array private function addFileCollectionValues(Organisation $organisation, array $mapped): array { + $organisation = Organisation::where('uuid', $organisation->uuid)->first(); foreach ($organisation->fileConfiguration as $key => $config) { if ($config['multiple'] == true) { $medias = $organisation->getMedia($key); @@ -141,7 +176,8 @@ private function addFileCollectionHeadings(array $headings): array private function buildTreeSpecies(Organisation $organisation): string { $list = []; - foreach ($organisation->treeSpecies as $treeSpecies) { + $treeSpecies = $organisation->treeSpecies()->select('name', 'amount')->get(); + foreach ($treeSpecies as $treeSpecies) { $list[] = $treeSpecies->name . '(' . $treeSpecies->amount . ')'; } From 29afd4d71f8fc231cb7a9cf2127cfa021ed3aa55 Mon Sep 17 00:00:00 2001 From: Jorge Monroy Date: Fri, 15 Nov 2024 12:21:50 -0400 Subject: [PATCH 09/21] [TM-1484] add command for export (#573) * [TM-1484] add command for export * [TM-1484] remove useless variables --- .../ExportApprovedDashboardDataCommand.php | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 app/Console/Commands/ExportApprovedDashboardDataCommand.php diff --git a/app/Console/Commands/ExportApprovedDashboardDataCommand.php b/app/Console/Commands/ExportApprovedDashboardDataCommand.php new file mode 100644 index 000000000..7fb6544d3 --- /dev/null +++ b/app/Console/Commands/ExportApprovedDashboardDataCommand.php @@ -0,0 +1,53 @@ + []]); + $projects = TerrafundDashboardQueryHelper::buildQueryFromRequest($request) + ->with(['organisation:id,type,name']) + ->select([ + 'v2_projects.uuid', + 'v2_projects.id', + ]) + ->get(); + $csvFile = fopen('projects_report.csv', 'w'); + fputcsv($csvFile, ['Project UUID', 'Trees Planted to Date', 'Hectares Under Restoration', 'Jobs Created']); + + foreach ($projects as $project) { + fputcsv($csvFile, [ + $project->uuid, + $project->approved_trees_planted_count, + $project->total_hectares_restored_sum, + $project->total_approved_jobs_created, + ]); + } + fclose($csvFile); + + echo "CSV file 'projects_report.csv' created successfully."; + } +} From 880a64ce255d13afb69c914e25aad74e6f64a03d Mon Sep 17 00:00:00 2001 From: Jorge Monroy Date: Fri, 15 Nov 2024 16:41:46 -0400 Subject: [PATCH 10/21] [Tm 1484] generate export dashboard (#574) * [TM-1484] add command for export * [TM-1484] lint * [TM-1484] remove useless variables From e1913a8c46b5f657a234d856ce8dac90c80b7c11 Mon Sep 17 00:00:00 2001 From: Limber Mamani <154026979+LimberHope@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:00:29 -0400 Subject: [PATCH 11/21] [TM-1352] add logic to get new fields results (#566) * [TM-1352] add logic to get new fields results * test lint * lint fix * [TM-1352] change non-tree to TreeSpecies * [TM-1352] remove regenerating function * [TM-1352] collections added removed * [TM-1352] improve code to total community partners * [TM-1352] fix lint * [TM-1352] remove unnecessary functions --- .../ProjectReports/ProjectReportResource.php | 7 +++++ .../V2/SiteReports/SiteReportResource.php | 2 ++ app/Models/Traits/HasWorkdays.php | 10 +++++++ app/Models/V2/Projects/ProjectReport.php | 28 +++++++++++++++++++ app/Models/V2/Sites/SiteReport.php | 5 ++++ app/Models/V2/Workdays/Workday.php | 2 ++ 6 files changed, 54 insertions(+) diff --git a/app/Http/Resources/V2/ProjectReports/ProjectReportResource.php b/app/Http/Resources/V2/ProjectReports/ProjectReportResource.php index 542cef76f..8d63ce409 100644 --- a/app/Http/Resources/V2/ProjectReports/ProjectReportResource.php +++ b/app/Http/Resources/V2/ProjectReports/ProjectReportResource.php @@ -115,6 +115,13 @@ public function toArray($request) 'volunteer_scstobc' => $this->volunteer_scstobc, 'beneficiaries_scstobc_farmers' => $this->beneficiaries_scstobc_farmers, 'beneficiaries_scstobc' => $this->beneficiaries_scstobc, + 'people_knowledge_skills_increased' => $this->people_knowledge_skills_increased, + 'indirect_beneficiaries' => $this->indirect_beneficiaries, + 'indirect_beneficiaries_description' => $this->indirect_beneficiaries_description, + 'workdays_direct_total' => $this->workdays_direct, + 'workdays_convergence_total' => $this->workdays_convergence, + 'non_tree_total' => $this->non_tree_total, + 'total_community_partners' => $this->total_community_partners, ]; return $this->appendFilesToResource($data); diff --git a/app/Http/Resources/V2/SiteReports/SiteReportResource.php b/app/Http/Resources/V2/SiteReports/SiteReportResource.php index e04d30970..e79590041 100644 --- a/app/Http/Resources/V2/SiteReports/SiteReportResource.php +++ b/app/Http/Resources/V2/SiteReports/SiteReportResource.php @@ -63,6 +63,8 @@ public function toArray($request) 'migrated' => ! empty($this->old_model), 'approved_by' => new UserLiteResource($this->approvedBy), 'created_by' => $this->handleCreatedBy(), + 'regeneration_description' => $this->regeneration_description, + 'total_non_tree_species_planted_count' => $this->total_non_tree_species_planted_count, ]; return $this->appendFilesToResource($data); diff --git a/app/Models/Traits/HasWorkdays.php b/app/Models/Traits/HasWorkdays.php index f80615373..ec97f8b45 100644 --- a/app/Models/Traits/HasWorkdays.php +++ b/app/Models/Traits/HasWorkdays.php @@ -42,6 +42,16 @@ public function getWorkdaysVolunteerAttribute(): int return $this->sumTotalWorkdaysAmounts(self::WORKDAY_COLLECTIONS['volunteer']); } + public function getWorkdaysDirectAttribute(): int + { + return $this->sumTotalWorkdaysAmounts(self::WORKDAY_COLLECTIONS['direct']); + } + + public function getWorkdaysConvergenceAttribute(): int + { + return $this->sumTotalWorkdaysAmounts(self::WORKDAY_COLLECTIONS['convergence']); + } + public function getOtherWorkdaysDescriptionAttribute(): ?string { return $this diff --git a/app/Models/V2/Projects/ProjectReport.php b/app/Models/V2/Projects/ProjectReport.php index 9f46afe19..aa44ec361 100644 --- a/app/Models/V2/Projects/ProjectReport.php +++ b/app/Models/V2/Projects/ProjectReport.php @@ -217,6 +217,12 @@ class ProjectReport extends Model implements MediaModel, AuditableContract, Repo Workday::COLLECTION_PROJECT_DIRECT, Workday::COLLECTION_PROJECT_CONVERGENCE, ], + 'direct' => [ + Workday::COLLECTION_PROJECT_DIRECT, + ], + 'convergence' => [ + Workday::COLLECTION_PROJECT_CONVERGENCE, + ], ]; public const RESTORATION_PARTNER_COLLECTIONS = [ @@ -439,11 +445,33 @@ public function getWorkdaysTotalAttribute(): int return $projectReportTotal + $sumTotals('paid') + $sumTotals('volunteer'); } + public function getNonTreeTotalAttribute(): int + { + if (empty($this->task_id)) { + return 0; + } + + return TreeSpecies::where('speciesable_type', SiteReport::class) + ->whereIn('speciesable_id', $this->task->siteReports()->hasBeenSubmitted()->select('id')) + ->where('collection', TreeSpecies::COLLECTION_NON_TREE) + ->visible() + ->sum('amount'); + } + public function getSiteReportsCountAttribute(): int { return $this->task?->siteReports()->count() ?? 0; } + public function getTotalCommunityPartnersAttribute(): int + { + $beneficiaries = ['men', 'women', 'youth', 'scstobc', 'scstobc_farmers', 'smallholder', 'large_scale']; + + return collect($beneficiaries)->reduce(function ($sum, $beneficiary) { + return $sum + ($this->{"beneficiaries_$beneficiary"} ?? 0); + }, 0); + } + public function getNurseryReportsCountAttribute(): ?int { return $this->task?->nurseryReports()->count() ?? 0; diff --git a/app/Models/V2/Sites/SiteReport.php b/app/Models/V2/Sites/SiteReport.php index 2902a3aca..1e37b196b 100644 --- a/app/Models/V2/Sites/SiteReport.php +++ b/app/Models/V2/Sites/SiteReport.php @@ -293,6 +293,11 @@ public function getTotalTreesPlantedCountAttribute(): int return $this->treeSpecies()->visible()->sum('amount'); } + public function getTotalNonTreeSpeciesPlantedCountAttribute(): int + { + return $this->nonTreeSpecies()->visible()->sum('amount'); + } + public function getTotalSeedsPlantedCountAttribute(): int { return $this->seedings()->visible()->sum('amount'); diff --git a/app/Models/V2/Workdays/Workday.php b/app/Models/V2/Workdays/Workday.php index 9484de371..d7ef78db8 100644 --- a/app/Models/V2/Workdays/Workday.php +++ b/app/Models/V2/Workdays/Workday.php @@ -62,6 +62,8 @@ class Workday extends Model implements HandlesLinkedFieldSync self::COLLECTION_PROJECT_VOLUNTEER_NURSERY_OPERATIONS => 'Volunteer Nursery Operations', self::COLLECTION_PROJECT_VOLUNTEER_PROJECT_MANAGEMENT => 'Volunteer Project Management', self::COLLECTION_PROJECT_VOLUNTEER_OTHER => 'Volunteer Other Activities', + self::COLLECTION_PROJECT_DIRECT => 'Direct Workdays', + self::COLLECTION_PROJECT_CONVERGENCE => 'Convergence Workdays', ]; public const COLLECTION_SITE_PAID_SITE_ESTABLISHMENT = 'paid-site-establishment'; From f4be18ce1bee07707827d8b0d9f4f22a4baa0d8a Mon Sep 17 00:00:00 2001 From: Limber Mamani <154026979+LimberHope@users.noreply.github.com> Date: Thu, 21 Nov 2024 09:57:25 -0400 Subject: [PATCH 12/21] [TM-1472] add direct seeding survival rate to project stablishment (#580) * [TM-1472] add direct seeding survival rate to project stablishment * fix lint --- .../CreateProjectWithFormController.php | 1 + .../Resources/V2/Projects/ProjectResource.php | 1 + app/Models/V2/Projects/Project.php | 1 + config/wri/linked-fields.php | 2 ++ ...153800_add_field_project_establishment.php | 33 +++++++++++++++++++ 5 files changed, 38 insertions(+) create mode 100644 database/migrations/2024_11_19_153800_add_field_project_establishment.php diff --git a/app/Http/Controllers/V2/Projects/CreateProjectWithFormController.php b/app/Http/Controllers/V2/Projects/CreateProjectWithFormController.php index 2f3d37c98..ca6495a3a 100644 --- a/app/Http/Controllers/V2/Projects/CreateProjectWithFormController.php +++ b/app/Http/Controllers/V2/Projects/CreateProjectWithFormController.php @@ -96,6 +96,7 @@ public function __invoke(Request $request): EntityWithSchemaResource 'goal_trees_restored_planting' => $projectPitch->goal_trees_restored_planting, 'goal_trees_restored_anr' => $projectPitch->goal_trees_restored_anr, 'goal_trees_restored_direct_seeding' => $projectPitch->goal_trees_restored_direct_seeding, + 'direct_seeding_survival_rate' => $projectPitch->direct_seeding_survival_rate, ]); foreach ($projectPitch->treeSpecies()->get() as $treeSpecies) { diff --git a/app/Http/Resources/V2/Projects/ProjectResource.php b/app/Http/Resources/V2/Projects/ProjectResource.php index 55b4eb861..8d6393c04 100644 --- a/app/Http/Resources/V2/Projects/ProjectResource.php +++ b/app/Http/Resources/V2/Projects/ProjectResource.php @@ -97,6 +97,7 @@ public function toArray($request) 'updated_at' => $this->updated_at, 'trees_restored_ppc' => $this->getTreesGrowingThroughAnr($this->sites) + (($this->trees_planted_count + $this->seeds_planted_count) * ($this->survival_rate / 100)), + 'direct_seeding_survival_rate' => $this->direct_seeding_survival_rate, ]; return $this->appendFilesToResource($data); diff --git a/app/Models/V2/Projects/Project.php b/app/Models/V2/Projects/Project.php index a00ae3af1..6d109d999 100644 --- a/app/Models/V2/Projects/Project.php +++ b/app/Models/V2/Projects/Project.php @@ -144,6 +144,7 @@ class Project extends Model implements MediaModel, AuditableContract, EntityMode 'goal_trees_restored_anr', 'goal_trees_restored_direct_seeding', 'landscape', + 'direct_seeding_survival_rate', ]; public $fileConfiguration = [ diff --git a/config/wri/linked-fields.php b/config/wri/linked-fields.php index c684d8d89..56c75c231 100644 --- a/config/wri/linked-fields.php +++ b/config/wri/linked-fields.php @@ -262,6 +262,7 @@ 'pro-pit-goal-trees-restored-planting' => ['property' => 'goal_trees_restored_planting', 'label' => 'Trees Restored Goal - Planting', 'input_type' => 'number'], 'pro-pit-goal-trees-restored-anr' => ['property' => 'goal_trees_restored_anr', 'label' => 'Trees Restored Goal - ANR', 'input_type' => 'number'], 'pro-pit-goal-trees-restored-direct-seeding' => ['property' => 'goal_trees_restored_direct_seeding', 'label' => 'Trees Restored Goal - Direct Seeding', 'input_type' => 'number'], + 'pro-pit-direct-seeding-survival-rate' => ['property' => 'direct_seeding_survival_rate', 'label' => 'Direct Seeding Survival Rate', 'input_type' => 'number-percentage'], ], 'file-collections' => [ 'pro-pit-fcol-cover' => ['property' => 'cover', 'label' => 'Cover Image', 'input_type' => 'file', 'multichoice' => false], @@ -343,6 +344,7 @@ 'pro-goal-trees-restored-planting' => ['property' => 'goal_trees_restored_planting', 'label' => 'Trees Restored Goal - Planting', 'input_type' => 'number'], 'pro-goal-trees-restored-anr' => ['property' => 'goal_trees_restored_anr', 'label' => 'Trees Restored Goal - ANR', 'input_type' => 'number'], 'pro-goal-trees-restored-direct-seeding' => ['property' => 'goal_trees_restored_direct_seeding', 'label' => 'Trees Restored Goal - Direct Seeding', 'input_type' => 'number'], + 'pro-direct-seeding-survival-rate' => ['property' => 'direct_seeding_survival_rate', 'label' => 'Direct Seeding Survival Rate', 'input_type' => 'number-percentage'], ], 'file-collections' => [ 'pro-col-media' => ['property' => 'media', 'label' => 'Media', 'input_type' => 'file', 'multichoice' => true], diff --git a/database/migrations/2024_11_19_153800_add_field_project_establishment.php b/database/migrations/2024_11_19_153800_add_field_project_establishment.php new file mode 100644 index 000000000..a6ba2f2f7 --- /dev/null +++ b/database/migrations/2024_11_19_153800_add_field_project_establishment.php @@ -0,0 +1,33 @@ +unsignedInteger('direct_seeding_survival_rate')->nullable(); + }); + Schema::table('project_pitches', function (Blueprint $table) { + $table->unsignedInteger('direct_seeding_survival_rate')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('v2_projects', function (Blueprint $table) { + $table->dropColumn('direct_seeding_survival_rate'); + }); + Schema::table('project_pitches', function (Blueprint $table) { + $table->dropColumn('direct_seeding_survival_rate'); + }); + } +}; From 49ca423096d9938cc35f5bc45be55fc17dfecd6c Mon Sep 17 00:00:00 2001 From: Limber Mamani <154026979+LimberHope@users.noreply.github.com> Date: Thu, 21 Nov 2024 10:22:58 -0400 Subject: [PATCH 13/21] [TM-1457] add delayed job to organisations export (#576) * [TM-1457] add delayed job to organisations export * [TM-1457] add delayed job functionality to export organisations * remove comment --- app/Exports/V2/OrganisationsExport.php | 5 ++ .../AdminExportOrganisationsController.php | 36 ++++++++-- app/Jobs/ExportAllOrganisationsJob.php | 66 +++++++++++++++++++ .../ExportAllOrganisationsService.php | 13 ++++ 4 files changed, 114 insertions(+), 6 deletions(-) create mode 100644 app/Jobs/ExportAllOrganisationsJob.php create mode 100644 app/Services/ExportAllOrganisationsService.php diff --git a/app/Exports/V2/OrganisationsExport.php b/app/Exports/V2/OrganisationsExport.php index 7bf937e58..ea5f44982 100644 --- a/app/Exports/V2/OrganisationsExport.php +++ b/app/Exports/V2/OrganisationsExport.php @@ -54,6 +54,11 @@ public function collection(): Collection ])->get(); } + public function chunkSize(): int + { + return 1000; + } + public function headings(): array { $headings = [ diff --git a/app/Http/Controllers/V2/Organisations/AdminExportOrganisationsController.php b/app/Http/Controllers/V2/Organisations/AdminExportOrganisationsController.php index 2e8628c4f..c3eced806 100644 --- a/app/Http/Controllers/V2/Organisations/AdminExportOrganisationsController.php +++ b/app/Http/Controllers/V2/Organisations/AdminExportOrganisationsController.php @@ -2,21 +2,45 @@ namespace App\Http\Controllers\V2\Organisations; -use App\Exports\V2\OrganisationsExport; use App\Http\Controllers\Controller; +use App\Http\Resources\DelayedJobResource; +use App\Jobs\ExportAllOrganisationsJob; +use App\Models\DelayedJob; use App\Models\V2\Organisation; use Illuminate\Http\Request; -use Maatwebsite\Excel\Excel; -use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Redis; class AdminExportOrganisationsController extends Controller { - public function __invoke(Request $request): BinaryFileResponse + public function __invoke(Request $request) { $this->authorize('export', Organisation::class); - $filename = 'organisations(' . now()->format('d-m-Y-H-i'). ').csv'; + $filename = 'organisations(' . now()->format('d-m-Y'). ').csv'; + $relativePath = 'exports/' . $filename; + $absolutePath = storage_path('app/' . $relativePath); - return (new OrganisationsExport())->download($filename, Excel::CSV)->deleteFileAfterSend(true); + try { + $binary_data = Redis::get('exports:organisations:'.$filename); + if (! $binary_data) { + $delayedJob = DelayedJob::create(); + $job = new ExportAllOrganisationsJob( + $delayedJob->id, + $filename + ); + dispatch($job); + + return (new DelayedJobResource($delayedJob))->additional(['message' => "Export for organisations $filename is being processed"]); + } else { + file_put_contents($absolutePath, $binary_data); + + return response()->download($absolutePath, $filename)->deleteFileAfterSend(true); + } + } catch (\Exception $e) { + Log::error('Error during export for organisations : ' . $e->getMessage()); + + return response()->json(['error' => 'An error occurred during organisations export'], 500); + } } } diff --git a/app/Jobs/ExportAllOrganisationsJob.php b/app/Jobs/ExportAllOrganisationsJob.php new file mode 100644 index 000000000..d10285b7d --- /dev/null +++ b/app/Jobs/ExportAllOrganisationsJob.php @@ -0,0 +1,66 @@ +file_name = $file_name; + $this->delayed_job_id = $delayed_job_id; + } + + public function handle(ExportAllOrganisationsService $exportAllOrganisationsService) + { + try { + $delayedJob = DelayedJob::findOrFail($this->delayed_job_id); + $relativePath = 'exports/' . $this->file_name; + + Excel::store($exportAllOrganisationsService->run(), $relativePath); + + $absolutePath = storage_path('app/' . $relativePath); + $binary_data = file_get_contents($absolutePath); + + Redis::set('exports:organisations:'.$this->file_name, $binary_data, 'EX', 7200); + $delayedJob->update([ + 'status' => DelayedJob::STATUS_SUCCEEDED, + 'payload' => ['message' => 'All Organisations Export completed'], + 'status_code' => Response::HTTP_OK, + ]); + } catch (Exception $e) { + Log::error('Error in ExportAllOrganisationsJob: ' . $e->getMessage()); + + DelayedJob::where('id', $this->delayed_job_id)->update([ + 'status' => DelayedJob::STATUS_FAILED, + 'payload' => json_encode(['error' => $e->getMessage()]), + 'status_code' => Response::HTTP_INTERNAL_SERVER_ERROR, + ]); + } + } +} diff --git a/app/Services/ExportAllOrganisationsService.php b/app/Services/ExportAllOrganisationsService.php new file mode 100644 index 000000000..f76901b18 --- /dev/null +++ b/app/Services/ExportAllOrganisationsService.php @@ -0,0 +1,13 @@ + Date: Thu, 21 Nov 2024 12:16:53 -0400 Subject: [PATCH 14/21] [TM-1461] waiting load for data polygons (#582) * add basic endpoints * move endpoints outside admin * fix duplicate * [TM-1461] add entity for site and project endpoint * [TM-1461] fix distinct responses * [TM-1461] lint --------- Co-authored-by: Jose Carlos Laura Ramirez --- .../V2/Sites/AdminSitesPolygonController.php | 45 +++ .../AdminSitesPolygonCountController.php | 41 +++ app/Services/PolygonService.php | 22 ++ .../V2/definitions/EntityPolygonResponse.yml | 13 + openapi-src/V2/definitions/_index.yml | 2 + .../V2/paths/Entity/get-v2-entity-count.yml | 34 ++ .../V2/paths/Entity/get-v2-entity-polygon.yml | 39 ++ ...get-v2-admin-sites-uuid-polygons-count.yml | 17 + .../get-v2-admin-sites-uuid-polygons.yml | 26 ++ openapi-src/V2/paths/_index.yml | 16 +- resources/docs/swagger-v2.yml | 336 ++++++++++++++++++ routes/api_v2.php | 6 + 12 files changed, 595 insertions(+), 2 deletions(-) create mode 100644 app/Http/Controllers/V2/Sites/AdminSitesPolygonController.php create mode 100644 app/Http/Controllers/V2/Sites/AdminSitesPolygonCountController.php create mode 100644 openapi-src/V2/definitions/EntityPolygonResponse.yml create mode 100644 openapi-src/V2/paths/Entity/get-v2-entity-count.yml create mode 100644 openapi-src/V2/paths/Entity/get-v2-entity-polygon.yml create mode 100644 openapi-src/V2/paths/Sites/get-v2-admin-sites-uuid-polygons-count.yml create mode 100644 openapi-src/V2/paths/Sites/get-v2-admin-sites-uuid-polygons.yml diff --git a/app/Http/Controllers/V2/Sites/AdminSitesPolygonController.php b/app/Http/Controllers/V2/Sites/AdminSitesPolygonController.php new file mode 100644 index 000000000..ff01c95d1 --- /dev/null +++ b/app/Http/Controllers/V2/Sites/AdminSitesPolygonController.php @@ -0,0 +1,45 @@ +input('uuid'); + $type = $request->input('type'); + $offset = $request->input('offset', 0); + $limit = $request->input('limit', 10); + $request = request(); + + + if ($type === 'projects') { + $project = Project::where('uuid', $uuid)->firstOrFail(); + $finalEntityQuery = App::make(PolygonService::class)->getSitePolygonsWithFiltersAndSorts($project->sitePolygons(), $request); + } elseif ($type === 'sites') { + $sitePolygonsQuery = SitePolygon::active()->where('site_id', $uuid); + $finalEntityQuery = App::make(PolygonService::class)->getSitePolygonsWithFiltersAndSorts($sitePolygonsQuery, $request); + } + $sitePolygons = $finalEntityQuery + ->offset($offset) + ->limit($limit) + ->get(); + + return response()->json($sitePolygons); + } catch (\Exception $e) { + Log::error($e->getMessage()); + + return response()->json(['error' => 'An error occurred while fetching site polygons'], 500); + } + } +} diff --git a/app/Http/Controllers/V2/Sites/AdminSitesPolygonCountController.php b/app/Http/Controllers/V2/Sites/AdminSitesPolygonCountController.php new file mode 100644 index 000000000..cb1f990b9 --- /dev/null +++ b/app/Http/Controllers/V2/Sites/AdminSitesPolygonCountController.php @@ -0,0 +1,41 @@ +input('uuid'); + $type = $request->input('type'); + $request = request(); + if ($type === 'projects') { + $project = Project::where('uuid', $uuid)->firstOrFail(); + $countSitePolygons = App::make(PolygonService::class)->getSitePolygonsWithFiltersAndSorts($project->sitePolygons(), $request); + } elseif ($type === 'sites') { + $sitePolygonsQuery = SitePolygon::active()->where('site_id', $uuid); + $countSitePolygons = App::make(PolygonService::class)->getSitePolygonsWithFiltersAndSorts($sitePolygonsQuery, $request); + } + + $totalCount = $countSitePolygons->count(); + + return response()->json([ + 'count' => $totalCount, + ]); + } catch (\Exception $e) { + Log::error($e->getMessage()); + + return response()->json(['error' => 'An error occurred while fetching site polygons'], 500); + } + } +} diff --git a/app/Services/PolygonService.php b/app/Services/PolygonService.php index ac80dd04d..eaee1393f 100755 --- a/app/Services/PolygonService.php +++ b/app/Services/PolygonService.php @@ -17,6 +17,7 @@ use App\Validators\SitePolygonValidator; use DateTime; use Exception; +use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\App; @@ -582,4 +583,25 @@ public function processClippedPolygons(array $polygonUuids) return $updatedPolygons; } + + public function getSitePolygonsWithFiltersAndSorts($sitePolygonsQuery, Request $request) + { + if ($request->has('status') && $request->input('status')) { + $statusValues = explode(',', $request->input('status')); + $sitePolygonsQuery->whereIn('site_polygon.status', $statusValues); + } + + $sortFields = $request->input('sort', []); + foreach ($sortFields as $field => $direction) { + if ($field === 'status') { + $sitePolygonsQuery->orderByRaw('FIELD(site_polygon.status, "draft", "submitted", "needs-more-information", "approved") ' . $direction); + } elseif ($field === 'poly_name') { + $sitePolygonsQuery->orderByRaw('site_polygon.poly_name IS NULL, site_polygon.poly_name ' . $direction); + } else { + $sitePolygonsQuery->orderBy($field, $direction); + } + } + + return $sitePolygonsQuery; + } } diff --git a/openapi-src/V2/definitions/EntityPolygonResponse.yml b/openapi-src/V2/definitions/EntityPolygonResponse.yml new file mode 100644 index 000000000..6aaff2fcd --- /dev/null +++ b/openapi-src/V2/definitions/EntityPolygonResponse.yml @@ -0,0 +1,13 @@ +type: object +properties: + type: + type: string + description: Type of the entity ('project', 'site', 'unknown') + uuid: + type: string + format: uuid + description: UUID of the entity + polygonsData: + type: array + items: + $ref: './_index.yml#/SitePolygon' \ No newline at end of file diff --git a/openapi-src/V2/definitions/_index.yml b/openapi-src/V2/definitions/_index.yml index 371955c1c..fb2f8e31c 100644 --- a/openapi-src/V2/definitions/_index.yml +++ b/openapi-src/V2/definitions/_index.yml @@ -352,6 +352,8 @@ GeojsonData: $ref: './GeojsonData.yml' EntityTypeResponse: $ref: './EntityTypeResponse.yml' +EntityPolygonResponse: + $ref: './EntityPolygonResponse.yml' AuditStatusUpdateRequest: $ref: './AuditStatusUpdateRequest.yml' SitePolygonResource: diff --git a/openapi-src/V2/paths/Entity/get-v2-entity-count.yml b/openapi-src/V2/paths/Entity/get-v2-entity-count.yml new file mode 100644 index 000000000..677f9f33c --- /dev/null +++ b/openapi-src/V2/paths/Entity/get-v2-entity-count.yml @@ -0,0 +1,34 @@ +summary: Get Entity Type +description: | + Determine the type of entity based on UUID. +parameters: + - in: query + name: uuid + required: true + description: UUID of the entity + type: string + - in: query + name: type + required: true + description: type of the entity + type: string + - in: query + name: status + required: false + description: Comma-separated list of status values to filter by + type: string + - in: query + name: sort + required: false + description: Sort criteria in the format `sort[poly_name]=asc or sort[status]=desc` + type: string +responses: + '200': + description: Successful response + schema: + type: object + properties: + count: + type: number + '500': + description: Internal server error diff --git a/openapi-src/V2/paths/Entity/get-v2-entity-polygon.yml b/openapi-src/V2/paths/Entity/get-v2-entity-polygon.yml new file mode 100644 index 000000000..50149e48d --- /dev/null +++ b/openapi-src/V2/paths/Entity/get-v2-entity-polygon.yml @@ -0,0 +1,39 @@ +summary: Get Entity Type +description: | + Determine the type of entity based on UUID. +parameters: + - in: query + name: uuid + required: true + description: UUID of the entity + type: string + - in: query + name: type + required: true + description: type of the entity + type: string + - in: query + name: status + required: false + description: Comma-separated list of status values to filter by + type: string + - in: query + name: sort + required: false + description: Sort criteria in the format `sort[poly_name]=asc or sort[status]=desc` + type: string +responses: + '200': + description: Successful response + schema: + $ref: '../../definitions/_index.yml#/EntityPolygonResponse' + '400': + description: Bad request, UUID parameter is missing + '500': + description: Internal server error + schema: + type: object + properties: + error: + type: string + description: Error message \ No newline at end of file diff --git a/openapi-src/V2/paths/Sites/get-v2-admin-sites-uuid-polygons-count.yml b/openapi-src/V2/paths/Sites/get-v2-admin-sites-uuid-polygons-count.yml new file mode 100644 index 000000000..112eb0801 --- /dev/null +++ b/openapi-src/V2/paths/Sites/get-v2-admin-sites-uuid-polygons-count.yml @@ -0,0 +1,17 @@ +summary: Get count polygons for a specific site +parameters: + - in: path + name: UUID + required: true + type: string + description: The UUID of the site +responses: + '200': + description: Successful response + schema: + type: object + properties: + count: + type: number + '500': + description: Internal server error diff --git a/openapi-src/V2/paths/Sites/get-v2-admin-sites-uuid-polygons.yml b/openapi-src/V2/paths/Sites/get-v2-admin-sites-uuid-polygons.yml new file mode 100644 index 000000000..ce5346716 --- /dev/null +++ b/openapi-src/V2/paths/Sites/get-v2-admin-sites-uuid-polygons.yml @@ -0,0 +1,26 @@ +summary: Get polygons for a specific site +parameters: + - in: path + name: UUID + required: true + type: string + description: The UUID of the site + - in: query + name: limit + required: true + description: The maximum number of polygons to return + type: number + - in: query + name: offset + required: true + description: The number of polygons to skip + type: string +responses: + '200': + description: Successful response + schema: + type: array + items: + $ref: '../../definitions/_index.yml#/SitePolygon' + '500': + description: Internal server error diff --git a/openapi-src/V2/paths/_index.yml b/openapi-src/V2/paths/_index.yml index 9ee3560b2..8f9fa32b8 100644 --- a/openapi-src/V2/paths/_index.yml +++ b/openapi-src/V2/paths/_index.yml @@ -2497,6 +2497,12 @@ $ref: './Sites/delete-v2-sites-uuid.yml' get: $ref: './Sites/get-v2-sites-uuid.yml' +/v2/sites/{UUID}/polygons: + get: + $ref: './Sites/get-v2-admin-sites-uuid-polygons.yml' +/v2/sites/{UUID}/polygons/count: + get: + $ref: './Sites/get-v2-admin-sites-uuid-polygons-count.yml' /v2/site-monitorings/{UUID}: get: $ref: './Sites/Monitoring/get-v2-site-monitorings-uuid.yml' @@ -2741,9 +2747,15 @@ /v2/type-entity: get: $ref: './Entity/get-v2-type-entity.yml' +/v2/entity/polygons/count: + get: + $ref: './Entity/get-v2-entity-count.yml' +/v2/entity/polygons: + get: + $ref: './Entity/get-v2-entity-polygon.yml' /v2/{ENTITY}/{UUID}/status: - put: - $ref: './Entity/put-v2-entity-uuid-status.yml' + put: + $ref: './Entity/put-v2-entity-uuid-status.yml' /v2/{ENTITY}/{UUID}/{ID}/delete: delete: $ref: './AuditStatus/delete-v2-audit-status.yml' diff --git a/resources/docs/swagger-v2.yml b/resources/docs/swagger-v2.yml index 0022ab99b..2884a7c92 100644 --- a/resources/docs/swagger-v2.yml +++ b/resources/docs/swagger-v2.yml @@ -43686,6 +43686,82 @@ definitions: items: type: number description: Bounding box of the entity + EntityPolygonResponse: + type: object + properties: + type: + type: string + description: 'Type of the entity (''project'', ''site'', ''unknown'')' + uuid: + type: string + format: uuid + description: UUID of the entity + polygonsData: + type: array + items: + title: SitePolygon + type: object + properties: + id: + type: integer + uuid: + type: string + primary_uuid: + type: string + project_id: + type: string + proj_name: + type: string + org_name: + type: string + poly_id: + type: string + poly_name: + type: string + site_id: + type: string + site_name: + type: string + plantstart: + type: string + format: date + plantend: + type: string + format: date + practice: + type: string + target_sys: + type: string + distr: + type: string + num_trees: + type: integer + calc_area: + type: number + format: float + created_by: + type: string + last_modified_by: + type: string + deleted_at: + type: string + format: date-time + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + status: + type: string + source: + type: string + country: + type: string + is_active: + type: boolean + version_name: + type: string AuditStatusUpdateRequest: title: AuditStatusUpdateRequest type: object @@ -94075,6 +94151,115 @@ paths: description: 'this is a list of key value pairs eg slug: name ' items: type: string + '/v2/sites/{UUID}/polygons': + get: + summary: Get polygons for a specific site + parameters: + - in: path + name: UUID + required: true + type: string + description: The UUID of the site + - in: query + name: limit + required: true + description: The maximum number of polygons to return + type: number + - in: query + name: offset + required: true + description: The number of polygons to skip + type: string + responses: + '200': + description: Successful response + schema: + type: array + items: + title: SitePolygon + type: object + properties: + id: + type: integer + uuid: + type: string + primary_uuid: + type: string + project_id: + type: string + proj_name: + type: string + org_name: + type: string + poly_id: + type: string + poly_name: + type: string + site_id: + type: string + site_name: + type: string + plantstart: + type: string + format: date + plantend: + type: string + format: date + practice: + type: string + target_sys: + type: string + distr: + type: string + num_trees: + type: integer + calc_area: + type: number + format: float + created_by: + type: string + last_modified_by: + type: string + deleted_at: + type: string + format: date-time + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + status: + type: string + source: + type: string + country: + type: string + is_active: + type: boolean + version_name: + type: string + '500': + description: Internal server error + '/v2/sites/{UUID}/polygons/count': + get: + summary: Get count polygons for a specific site + parameters: + - in: path + name: UUID + required: true + type: string + description: The UUID of the site + responses: + '200': + description: Successful response + schema: + type: object + properties: + count: + type: number + '500': + description: Internal server error '/v2/site-monitorings/{UUID}': get: summary: View a specific site monitoring @@ -97996,6 +98181,157 @@ paths: error: type: string description: Error message + /v2/entity/polygons/count: + get: + summary: Get Entity Type + description: | + Determine the type of entity based on UUID. + parameters: + - in: query + name: uuid + required: true + description: UUID of the entity + type: string + - in: query + name: type + required: true + description: type of the entity + type: string + - in: query + name: status + required: false + description: Comma-separated list of status values to filter by + type: string + - in: query + name: sort + required: false + description: 'Sort criteria in the format `sort[poly_name]=asc or sort[status]=desc`' + type: string + responses: + '200': + description: Successful response + schema: + type: object + properties: + count: + type: number + '500': + description: Internal server error + /v2/entity/polygons: + get: + summary: Get Entity Type + description: | + Determine the type of entity based on UUID. + parameters: + - in: query + name: uuid + required: true + description: UUID of the entity + type: string + - in: query + name: type + required: true + description: type of the entity + type: string + - in: query + name: status + required: false + description: Comma-separated list of status values to filter by + type: string + - in: query + name: sort + required: false + description: 'Sort criteria in the format `sort[poly_name]=asc or sort[status]=desc`' + type: string + responses: + '200': + description: Successful response + schema: + type: object + properties: + type: + type: string + description: 'Type of the entity (''project'', ''site'', ''unknown'')' + uuid: + type: string + format: uuid + description: UUID of the entity + polygonsData: + type: array + items: + title: SitePolygon + type: object + properties: + id: + type: integer + uuid: + type: string + primary_uuid: + type: string + project_id: + type: string + proj_name: + type: string + org_name: + type: string + poly_id: + type: string + poly_name: + type: string + site_id: + type: string + site_name: + type: string + plantstart: + type: string + format: date + plantend: + type: string + format: date + practice: + type: string + target_sys: + type: string + distr: + type: string + num_trees: + type: integer + calc_area: + type: number + format: float + created_by: + type: string + last_modified_by: + type: string + deleted_at: + type: string + format: date-time + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + status: + type: string + source: + type: string + country: + type: string + is_active: + type: boolean + version_name: + type: string + '400': + description: 'Bad request, UUID parameter is missing' + '500': + description: Internal server error + schema: + type: object + properties: + error: + type: string + description: Error message '/v2/{ENTITY}/{UUID}/status': put: summary: Update the status of a specific entity diff --git a/routes/api_v2.php b/routes/api_v2.php index e46cf7dbd..e1d77685e 100644 --- a/routes/api_v2.php +++ b/routes/api_v2.php @@ -179,6 +179,8 @@ use App\Http\Controllers\V2\SiteReports\SiteReportsViaSiteController; use App\Http\Controllers\V2\Sites\AdminIndexSitesController; use App\Http\Controllers\V2\Sites\AdminSitesMultiController; +use App\Http\Controllers\V2\Sites\AdminSitesPolygonController; +use App\Http\Controllers\V2\Sites\AdminSitesPolygonCountController; use App\Http\Controllers\V2\Sites\CreateSiteWithFormController; use App\Http\Controllers\V2\Sites\IndexSitePolygonVersionsController; use App\Http\Controllers\V2\Sites\Monitoring\AdminCreateSiteMonitoringController; @@ -582,6 +584,10 @@ Route::get('/bbox', [SitePolygonDataController::class, 'getBboxOfCompleteSite']); Route::get('/check-approve', SiteCheckApproveController::class); }); +Route::prefix('entity')->group(function () { + Route::get('/polygons/count', AdminSitesPolygonCountController::class); + Route::get('/polygons', AdminSitesPolygonController::class); +}); Route::prefix('geometry')->group(function () { Route::post('', [GeometryController::class, 'storeGeometry']); From bcede059171c5a28a9cb4a776b47d571bbd8f1ea Mon Sep 17 00:00:00 2001 From: Limber Mamani <154026979+LimberHope@users.noreply.github.com> Date: Thu, 21 Nov 2024 16:14:58 -0400 Subject: [PATCH 15/21] [TM-1456] add docx and bin to mimes types (#583) --- app/Http/Requests/V2/File/UploadRequest.php | 2 +- config/wri/file-handling.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Http/Requests/V2/File/UploadRequest.php b/app/Http/Requests/V2/File/UploadRequest.php index d9ccd636b..48ebfd037 100644 --- a/app/Http/Requests/V2/File/UploadRequest.php +++ b/app/Http/Requests/V2/File/UploadRequest.php @@ -22,7 +22,7 @@ public function rules() ], 'upload_file' => [ 'sometimes', - 'mimes:csv,txt,xls,xlsx,jpg,gif,png,pdf,tiff,svg,mp4,doc', + 'mimes:csv,txt,xls,xlsx,jpg,gif,png,pdf,tiff,svg,mp4,doc,docx,bin', ], 'collection' => [ 'sometimes', diff --git a/config/wri/file-handling.php b/config/wri/file-handling.php index 497c653a0..b44e47a15 100644 --- a/config/wri/file-handling.php +++ b/config/wri/file-handling.php @@ -7,8 +7,8 @@ 'cover-image-with-svg' => 'file|mimes:jpg,png,svg', 'photos' => 'file|mimes:jpg,png,mp4', 'pdf' => 'file|mimes:pdf', - 'documents' => 'file|mimes:pdf,xls,xlsx,csv,txt,doc', - 'general-documents' => 'file|mimes:pdf,xls,xlsx,csv,txt,png,jpg,doc,mp4', + 'documents' => 'file|mimes:pdf,xls,xlsx,csv,txt,doc,docx,bin', + 'general-documents' => 'file|mimes:pdf,xls,xlsx,csv,txt,png,jpg,doc,mp4,docx,bin', 'spreadsheet' => 'file|mimes:pdf,xls,xlsx,csv,txt', ], 'validation-file-types' => [ From dafa4949a248c860c99c4d4d9f5a043faf5fb2ad Mon Sep 17 00:00:00 2001 From: Limber Mamani <154026979+LimberHope@users.noreply.github.com> Date: Fri, 22 Nov 2024 15:01:36 -0400 Subject: [PATCH 16/21] [TM-1474] approved filter to high level metrics in all entities (#585) * [TM-1474] approved filter to high level metrics in all entities * [TM-1474] add scope approved to site reports * [TM-1474] fix test to only the count of approved reports * remove comment --- app/Http/Resources/V2/Projects/ProjectResource.php | 4 ++-- app/Models/V2/Projects/Project.php | 14 ++++++++------ app/Models/V2/Sites/SiteReport.php | 6 ++++++ tests/Unit/Models/V2/Projects/ProjectTest.php | 4 ++-- 4 files changed, 18 insertions(+), 10 deletions(-) diff --git a/app/Http/Resources/V2/Projects/ProjectResource.php b/app/Http/Resources/V2/Projects/ProjectResource.php index 8d6393c04..8942bd22b 100644 --- a/app/Http/Resources/V2/Projects/ProjectResource.php +++ b/app/Http/Resources/V2/Projects/ProjectResource.php @@ -96,7 +96,7 @@ public function toArray($request) 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, 'trees_restored_ppc' => - $this->getTreesGrowingThroughAnr($this->sites) + (($this->trees_planted_count + $this->seeds_planted_count) * ($this->survival_rate / 100)), + $this->getTreesGrowingThroughAnr($this->sites()->IsApproved()->get()) + (($this->trees_planted_count + $this->seeds_planted_count) * ($this->survival_rate / 100)), 'direct_seeding_survival_rate' => $this->direct_seeding_survival_rate, ]; @@ -106,7 +106,7 @@ public function toArray($request) public function getTreesGrowingThroughAnr($sites) { return $sites->sum(function ($site) { - return $site->reports->sum('num_trees_regenerating'); + return $site->reports()->Approved()->sum('num_trees_regenerating'); }); } } diff --git a/app/Models/V2/Projects/Project.php b/app/Models/V2/Projects/Project.php index 6d109d999..4688da5cd 100644 --- a/app/Models/V2/Projects/Project.php +++ b/app/Models/V2/Projects/Project.php @@ -352,7 +352,7 @@ public function getProjectReportsTotalAttribute(): int public function getTreesPlantedCountAttribute(): int { return TreeSpecies::where('speciesable_type', SiteReport::class) - ->whereIn('speciesable_id', $this->submittedSiteReportIds()) + ->whereIn('speciesable_id', $this->approvedSiteReportIds()) ->where('collection', TreeSpecies::COLLECTION_PLANTED) ->visible() ->sum('amount'); @@ -370,7 +370,7 @@ public function getApprovedTreesPlantedCountAttribute(): int public function getSeedsPlantedCountAttribute(): int { return Seeding::where('seedable_type', SiteReport::class) - ->whereIn('seedable_id', $this->submittedSiteReportIds()) + ->whereIn('seedable_id', $this->approvedSiteReportIds()) ->visible() ->sum('amount'); } @@ -382,8 +382,8 @@ public function getRegeneratedTreesCountAttribute(): int public function getWorkdayCountAttribute($useDemographicsCutoff = false): int { - $projectQuery = $this->reports()->hasBeenSubmitted(); - $siteQuery = $this->submittedSiteReports(); + $projectQuery = $this->reports()->Approved(); + $siteQuery = $this->approvedSiteReports(); if ($useDemographicsCutoff) { $projectQuery->where('due_at', '>=', Workday::DEMOGRAPHICS_COUNT_CUTOFF); $siteQuery->where('due_at', '>=', Workday::DEMOGRAPHICS_COUNT_CUTOFF); @@ -411,10 +411,10 @@ public function getSelfReportedWorkdayCountAttribute($useDemographicsCutoff = fa DB::raw('sum(`workdays_paid`) as paid'), DB::raw('sum(`workdays_volunteer`) as volunteer'), ]; - $projectQuery = $this->reports()->hasBeenSubmitted(); + $projectQuery = $this->reports()->Approved(); // The groupBy is superfluous, but required because Laravel adds "v2_sites.project_id as laravel_through_key" to // the SQL select. - $siteQuery = $this->submittedSiteReports()->groupBy('v2_sites.project_id'); + $siteQuery = $this->approvedSiteReports()->groupBy('v2_sites.project_id'); if ($useDemographicsCutoff) { $projectQuery->where('due_at', '<', Workday::DEMOGRAPHICS_COUNT_CUTOFF); @@ -439,9 +439,11 @@ public function getCombinedWorkdayCountAttribute(): int public function getTotalJobsCreatedAttribute(): int { $ftTotal = ProjectReport::where('project_id', $this->id) + ->approved() ->sum('ft_total'); $ptTotal = ProjectReport::where('project_id', $this->id) + ->approved() ->sum('pt_total'); return $ftTotal + $ptTotal; diff --git a/app/Models/V2/Sites/SiteReport.php b/app/Models/V2/Sites/SiteReport.php index 1e37b196b..439fdfabd 100644 --- a/app/Models/V2/Sites/SiteReport.php +++ b/app/Models/V2/Sites/SiteReport.php @@ -27,6 +27,7 @@ use App\Models\V2\TreeSpecies\TreeSpecies; use App\Models\V2\User; use App\Models\V2\Workdays\Workday; +use App\StateMachines\ReportStatusStateMachine; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -398,4 +399,9 @@ public static function search($query) ->orWhere('organisations.name', 'like', "%$query%") ->orWhere('v2_sites.name', 'like', "%$query%"); } + + public function scopeApproved($query) + { + return $query->where('status', ReportStatusStateMachine::APPROVED); + } } diff --git a/tests/Unit/Models/V2/Projects/ProjectTest.php b/tests/Unit/Models/V2/Projects/ProjectTest.php index 4c566c11b..92bad2f03 100644 --- a/tests/Unit/Models/V2/Projects/ProjectTest.php +++ b/tests/Unit/Models/V2/Projects/ProjectTest.php @@ -147,8 +147,8 @@ public function test_workday_count() $workday = Workday::factory()->projectReport()->create(['workdayable_id' => $report->id]); Demographic::factory()->create(['demographical_id' => $workday->id, 'amount' => 19]); - // 42 = 5 and 7 from the approved site's reports and 13 and 17 from the project reports - $this->assertEquals(42, $project->workday_count); + //Only the count of approved reports is being taken. + $this->assertEquals(18, $project->workday_count); } public static function permissionsDataProvider() From 655d47df8673b69d12b846fa3094a75f119b53f8 Mon Sep 17 00:00:00 2001 From: Limber Mamani <154026979+LimberHope@users.noreply.github.com> Date: Fri, 22 Nov 2024 16:13:03 -0400 Subject: [PATCH 17/21] =?UTF-8?q?[TM-1417]=20Format=20date=20in=20full=20e?= =?UTF-8?q?xport=20filename=20to=20'd-m-Y=20to=20remove=20tem=E2=80=A6=20(?= =?UTF-8?q?#584)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [TM-1417] Format date in full export filename to 'd-m-Y to remove temporary file after generating full export * add format d-m-y --- .../ExportAllProjectDataAsProjectDeveloperController.php | 2 +- .../ExportAllProjectDataAsProjectDeveloperService.php | 2 +- .../ExportEntitiesAsProjectDeveloperControllerTest.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/V2/Exports/ExportAllProjectDataAsProjectDeveloperController.php b/app/Http/Controllers/V2/Exports/ExportAllProjectDataAsProjectDeveloperController.php index 8ad92f50e..31c65e9d8 100644 --- a/app/Http/Controllers/V2/Exports/ExportAllProjectDataAsProjectDeveloperController.php +++ b/app/Http/Controllers/V2/Exports/ExportAllProjectDataAsProjectDeveloperController.php @@ -35,7 +35,7 @@ public function __invoke(Request $request, Project $project) return (new DelayedJobResource($delayedJob))->additional(['message' => "Export for project $project->id is being processed"]); } else { - $filename = storage_path('./'.Str::of($project->name)->replace(['/', '\\'], '-') . ' full export - ' . now() . '.zip'); + $filename = storage_path('./'.Str::of($project->name)->replace(['/', '\\'], '-') . ' full export - ' . now()->format('d-m-Y') . '.zip'); file_put_contents($filename, $binary_data); return response()->download($filename)->deleteFileAfterSend(); diff --git a/app/Services/ExportAllProjectDataAsProjectDeveloperService.php b/app/Services/ExportAllProjectDataAsProjectDeveloperService.php index d3e1cc94f..19f20af45 100644 --- a/app/Services/ExportAllProjectDataAsProjectDeveloperService.php +++ b/app/Services/ExportAllProjectDataAsProjectDeveloperService.php @@ -17,7 +17,7 @@ class ExportAllProjectDataAsProjectDeveloperService { public function run($form, $project) { - $filename = storage_path('./'.Str::of($project->name)->replace(['/', '\\'], '-') . ' full export - ' . now() . '.zip'); + $filename = storage_path('./'.Str::of($project->name)->replace(['/', '\\'], '-') . ' full export - ' . now()->format('d-m-Y') . '.zip'); $zip = new \ZipArchive(); $zip->open($filename, \ZipArchive::CREATE); diff --git a/tests/V2/Exports/ExportEntitiesAsProjectDeveloperControllerTest.php b/tests/V2/Exports/ExportEntitiesAsProjectDeveloperControllerTest.php index 85a0e67ba..e95287493 100644 --- a/tests/V2/Exports/ExportEntitiesAsProjectDeveloperControllerTest.php +++ b/tests/V2/Exports/ExportEntitiesAsProjectDeveloperControllerTest.php @@ -238,7 +238,7 @@ public function test_an_user_can_export_all_project_data_gets_a_delayed_job_when */ public function test_an_user_can_export_all_project_data_when_already_cached(string $permission, string $fmKey) { - Carbon::setTestNow(now()); + Carbon::setTestNow(now()->format('d-m-Y')); $organisation = Organisation::factory()->create(); $owner = User::factory()->create(['organisation_id' => $organisation->id]); @@ -290,7 +290,7 @@ public function test_an_user_can_export_all_project_data_when_already_cached(str $this->actingAs($owner) ->get($uri) - ->assertDownload($project->name . ' full export - ' . now() . '.zip') + ->assertDownload($project->name . ' full export - ' . now()->format('d-m-Y') . '.zip') ->assertSuccessful(); } From cc4d1aec8e6e77481a3ac3cd204922b4d806617c Mon Sep 17 00:00:00 2001 From: Jorge Monroy Date: Mon, 25 Nov 2024 14:03:23 -0400 Subject: [PATCH 18/21] [TM-1503] criteria historic (#586) * [TM-1503] add migrations to create tables historic criteria * [TM-1503] delete and create site and historic * [TM-1503] lint --- app/Helpers/GeometryHelper.php | 21 ++++----- app/Models/V2/Sites/CriteriaSite.php | 4 +- app/Models/V2/Sites/CriteriaSiteHistoric.php | 45 +++++++++++++++++++ app/Services/PolygonService.php | 29 +++++++++--- ..._remove_softdeletes_from_criteria_site.php | 32 +++++++++++++ ...11_create_criteria_site_historic_table.php | 36 +++++++++++++++ 6 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 app/Models/V2/Sites/CriteriaSiteHistoric.php create mode 100644 database/migrations/2024_11_22_161557_remove_softdeletes_from_criteria_site.php create mode 100644 database/migrations/2024_11_22_161611_create_criteria_site_historic_table.php diff --git a/app/Helpers/GeometryHelper.php b/app/Helpers/GeometryHelper.php index ba5bc6a46..c0ca8e1e1 100755 --- a/app/Helpers/GeometryHelper.php +++ b/app/Helpers/GeometryHelper.php @@ -5,7 +5,6 @@ use App\Models\V2\PolygonGeometry; use App\Models\V2\Projects\Project; use App\Models\V2\Projects\ProjectPolygon; -use App\Models\V2\Sites\CriteriaSite; use App\Models\V2\Sites\Site; use App\Models\V2\Sites\SitePolygon; use Exception; @@ -125,18 +124,14 @@ public static function getPolygonsBbox($polygonsIds) public static function getCriteriaDataForPolygonGeometry($polygonGeometry) { - return CriteriaSite::whereIn( - 'id', - $polygonGeometry - ->criteriaSite() - ->groupBy('criteria_id') - ->selectRaw('max(id) as latest_id') - )->get([ - 'criteria_id', - 'valid', - 'created_at as latest_created_at', - 'extra_info', - ]); + return $polygonGeometry + ->criteriaSite() + ->get([ + 'criteria_id', + 'valid', + 'created_at as latest_created_at', + 'extra_info', + ]); } public static function groupFeaturesBySiteId($geojson) diff --git a/app/Models/V2/Sites/CriteriaSite.php b/app/Models/V2/Sites/CriteriaSite.php index bb6bbac5b..179992ff3 100644 --- a/app/Models/V2/Sites/CriteriaSite.php +++ b/app/Models/V2/Sites/CriteriaSite.php @@ -4,12 +4,10 @@ use App\Models\Traits\HasUuid; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\SoftDeletes; class CriteriaSite extends Model { use HasUuid; - use SoftDeletes; /** * The table associated with the model. @@ -42,6 +40,6 @@ class CriteriaSite extends Model public function scopeForCriteria($query, $criteriaId) { - return $query->where('criteria_id', $criteriaId)->latest(); + return $query->where('criteria_id', $criteriaId); } } diff --git a/app/Models/V2/Sites/CriteriaSiteHistoric.php b/app/Models/V2/Sites/CriteriaSiteHistoric.php new file mode 100644 index 000000000..255a0ebfa --- /dev/null +++ b/app/Models/V2/Sites/CriteriaSiteHistoric.php @@ -0,0 +1,45 @@ +where('criteria_id', $criteriaId)->latest(); + } +} diff --git a/app/Services/PolygonService.php b/app/Services/PolygonService.php index eaee1393f..c6712bb77 100755 --- a/app/Services/PolygonService.php +++ b/app/Services/PolygonService.php @@ -11,6 +11,7 @@ use App\Models\V2\Projects\Project; use App\Models\V2\Projects\ProjectPolygon; use App\Models\V2\Sites\CriteriaSite; +use App\Models\V2\Sites\CriteriaSiteHistoric; use App\Models\V2\Sites\Site; use App\Models\V2\Sites\SitePolygon; use App\Models\V2\User; @@ -222,13 +223,29 @@ private function insertPolygon($uuid, $sitePolygonProperties, $featureProperties public function createCriteriaSite($polygonId, $criteriaId, $valid, $extraInfo = null): bool|string { - $criteriaSite = new CriteriaSite(); - $criteriaSite->polygon_id = $polygonId; - $criteriaSite->criteria_id = $criteriaId; - $criteriaSite->valid = $valid; - $criteriaSite->extra_info = $extraInfo ? json_encode($extraInfo) : null; - try { + $existingCriteriaSite = CriteriaSite::where('polygon_id', $polygonId) + ->where('criteria_id', $criteriaId) + ->first(); + + if ($existingCriteriaSite) { + CriteriaSiteHistoric::create([ + 'polygon_id' => $existingCriteriaSite->polygon_id, + 'criteria_id' => $existingCriteriaSite->criteria_id, + 'valid' => $existingCriteriaSite->valid, + 'extra_info' => $existingCriteriaSite->extra_info, + 'created_at' => $existingCriteriaSite->created_at, + 'updated_at' => $existingCriteriaSite->updated_at, + ]); + + $existingCriteriaSite->delete(); + } + + $criteriaSite = new CriteriaSite(); + $criteriaSite->polygon_id = $polygonId; + $criteriaSite->criteria_id = $criteriaId; + $criteriaSite->valid = $valid; + $criteriaSite->extra_info = $extraInfo ? json_encode($extraInfo) : null; $criteriaSite->save(); return true; diff --git a/database/migrations/2024_11_22_161557_remove_softdeletes_from_criteria_site.php b/database/migrations/2024_11_22_161557_remove_softdeletes_from_criteria_site.php new file mode 100644 index 000000000..6263d94e7 --- /dev/null +++ b/database/migrations/2024_11_22_161557_remove_softdeletes_from_criteria_site.php @@ -0,0 +1,32 @@ +dropSoftDeletes(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('criteria_site', function (Blueprint $table) { + $table->softDeletes(); + }); + } +} diff --git a/database/migrations/2024_11_22_161611_create_criteria_site_historic_table.php b/database/migrations/2024_11_22_161611_create_criteria_site_historic_table.php new file mode 100644 index 000000000..6194ea5c4 --- /dev/null +++ b/database/migrations/2024_11_22_161611_create_criteria_site_historic_table.php @@ -0,0 +1,36 @@ +id(); + $table->uuid('uuid')->unique(); + $table->integer('criteria_id')->nullable(); + $table->string('polygon_id')->nullable(); + $table->integer('valid')->nullable(); + $table->json('extra_info')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('criteria_site_historic'); + } +} From 4c401563ea5442ee9f14d1219a0b9499b613e7e2 Mon Sep 17 00:00:00 2001 From: Jorge Monroy Date: Mon, 25 Nov 2024 14:26:41 -0400 Subject: [PATCH 19/21] [TM-1503] criteria historic (#587) * [TM-1503] add migrations to create tables historic criteria * [TM-1503] delete and create site and historic * [TM-1503] lint * [TM-1503] remove useless attribut * [TM-1503] remove comment --- ...remove_date_created_from_criteria_site.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 database/migrations/2024_11_25_181031_remove_date_created_from_criteria_site.php diff --git a/database/migrations/2024_11_25_181031_remove_date_created_from_criteria_site.php b/database/migrations/2024_11_25_181031_remove_date_created_from_criteria_site.php new file mode 100644 index 000000000..8c64a8fd8 --- /dev/null +++ b/database/migrations/2024_11_25_181031_remove_date_created_from_criteria_site.php @@ -0,0 +1,32 @@ +dropColumn('date_created'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('criteria_site', function (Blueprint $table) { + $table->timestamp('date_created')->nullable(); + }); + } +} From a3e3a81952d6f4e69482010bfe200bc88faf62e4 Mon Sep 17 00:00:00 2001 From: Jorge Monroy Date: Tue, 26 Nov 2024 17:11:55 -0400 Subject: [PATCH 20/21] [TM-1461] check if job is created for fix (#589) * [TM-1461] check if job is created for fix * [TM-1461] remove comment --- .../V2/Terrafund/TerrafundClipGeometryController.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/V2/Terrafund/TerrafundClipGeometryController.php b/app/Http/Controllers/V2/Terrafund/TerrafundClipGeometryController.php index a4ecb766a..2e7d5ead2 100644 --- a/app/Http/Controllers/V2/Terrafund/TerrafundClipGeometryController.php +++ b/app/Http/Controllers/V2/Terrafund/TerrafundClipGeometryController.php @@ -86,6 +86,7 @@ public function clipOverlappingPolygons(Request $request) $allPolygonUuids = array_merge($allPolygonUuids, $polygonUuids); } $uniquePolygonUuids = array_unique($allPolygonUuids); + $delayedJob = null; if (! empty($uniquePolygonUuids)) { $user = Auth::user(); $delayedJob = DelayedJob::create(); @@ -93,7 +94,11 @@ public function clipOverlappingPolygons(Request $request) dispatch($job); } - return new DelayedJobResource($delayedJob); + if ($delayedJob) { + return new DelayedJobResource($delayedJob); + } else { + return response()->json(['message' => 'No overlapping polygons found or processed.'], 204); + } } public function clipOverlappingPolygon(string $uuid) From 9bb6341ef31dba2f43312a72a147cf09eb9a8374 Mon Sep 17 00:00:00 2001 From: cesarLima1 <105736261+cesarLima1@users.noreply.github.com> Date: Tue, 26 Nov 2024 17:40:17 -0400 Subject: [PATCH 21/21] [TM-1385] Admin create user funtionality (#590) * [TM-1385] start up admin create user controller * [TM-1385] start up send login details controller * [TM-1385] Start up endpoint to set new password * [TM-1385] update operationId * [TM-1385] update path index --- app/Http/Controllers/AuthController.php | 72 +++++++ .../V2/User/AdminUserCreationController.php | 68 ++++++ app/Http/Requests/SendLoginDetailsRequest.php | 25 +++ app/Http/Requests/SetPasswordRequest.php | 24 +++ .../V2/User/AdminUserCreationRequest.php | 62 ++++++ app/Jobs/SendLoginDetailsJob.php | 54 +++++ app/Mail/SendLoginDetails.php | 25 +++ .../seeders/LocalizationKeysTableSeeder.php | 14 ++ .../V2/definitions/AdminUserCreate.yml | 22 ++ openapi-src/V2/definitions/_index.yml | 2 + openapi-src/V2/paths/Auth/get-auth-mail.yml | 51 +++++ .../Auth/post-auth-send-login-details.yml | 20 ++ openapi-src/V2/paths/Auth/post-auth-store.yml | 20 ++ openapi-src/V2/paths/_index.yml | 26 +++ resources/docs/swagger-v2.yml | 198 ++++++++++++++++++ routes/api.php | 3 + routes/api_v2.php | 2 + 17 files changed, 688 insertions(+) create mode 100644 app/Http/Controllers/V2/User/AdminUserCreationController.php create mode 100644 app/Http/Requests/SendLoginDetailsRequest.php create mode 100644 app/Http/Requests/SetPasswordRequest.php create mode 100644 app/Http/Requests/V2/User/AdminUserCreationRequest.php create mode 100644 app/Jobs/SendLoginDetailsJob.php create mode 100644 app/Mail/SendLoginDetails.php create mode 100644 openapi-src/V2/definitions/AdminUserCreate.yml create mode 100644 openapi-src/V2/paths/Auth/get-auth-mail.yml create mode 100644 openapi-src/V2/paths/Auth/post-auth-send-login-details.yml create mode 100644 openapi-src/V2/paths/Auth/post-auth-store.yml diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 28312b72c..3d7cd8ce9 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -11,9 +11,12 @@ use App\Http\Requests\ResendByEmailRequest; use App\Http\Requests\ResendRequest; use App\Http\Requests\ResetRequest; +use App\Http\Requests\SendLoginDetailsRequest; +use App\Http\Requests\SetPasswordRequest; use App\Http\Requests\VerifyRequest; use App\Http\Resources\V2\User\MeResource; use App\Jobs\ResetPasswordJob; +use App\Jobs\SendLoginDetailsJob; use App\Jobs\UserVerificationJob; use App\Models\PasswordReset as PasswordResetModel; use App\Models\V2\Projects\ProjectInvite; @@ -24,6 +27,7 @@ use Exception; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -160,6 +164,74 @@ public function resetAction(ResetRequest $request): JsonResponse return JsonResponseHelper::success((object) [], 200); } + public function sendLoginDetailsAction(SendLoginDetailsRequest $request): JsonResponse + { + $this->authorize('reset', 'App\\Models\\Auth'); + $data = $request->json()->all(); + + try { + $user = UserModel::where('email_address', '=', $data['email_address']) + ->whereNull('password') + ->firstOrFail(); + } catch (Exception $exception) { + return JsonResponseHelper::success((object) [], 200); + } + + SendLoginDetailsJob::dispatch($user, isset($data['callback_url']) ? $data['callback_url'] : null); + + return JsonResponseHelper::success((object) [], 200); + } + + public function getEmailByResetTokenAction(Request $request): JsonResponse + { + $data = $request->query(); + + $passwordReset = PasswordResetModel::where('token', '=', $data['token'])->first(); + + if (! $passwordReset) { + return JsonResponseHelper::success((object) [ + 'email_address' => null, + 'token_used' => true, + ], 200); + } + if (Carbon::parse($passwordReset->created_at)->addDays(7)->isPast()) { + $passwordReset->delete(); + + return JsonResponseHelper::success((object) [ + 'email_address' => null, + 'token_used' => true, + ], 200); + } + + $user = UserModel::findOrFail($passwordReset->user_id); + + return JsonResponseHelper::success((object) [ + 'email_address' => $user->email_address, + 'token_used' => false, + ], 200); + } + + public function setNewPasswordAction(SetPasswordRequest $request): JsonResponse + { + $this->authorize('change', 'App\\Models\\Auth'); + $data = $request->json()->all(); + $passwordReset = PasswordResetModel::where('token', '=', $data['token'])->firstOrFail(); + $user = UserModel::findOrFail($passwordReset->user_id); + if (Hash::check($data['password'], $user->password)) { + throw new SamePasswordException(); + } + $user->password = $data['password']; + + if (empty($user->email_address_verified_at)) { + $user->email_address_verified_at = new DateTime('now', new DateTimeZone('UTC')); + } + + $user->saveOrFail(); + $passwordReset->delete(); + + return JsonResponseHelper::success((object) [], 200); + } + public function changeAction(ChangePasswordRequest $request): JsonResponse { $this->authorize('change', 'App\\Models\\Auth'); diff --git a/app/Http/Controllers/V2/User/AdminUserCreationController.php b/app/Http/Controllers/V2/User/AdminUserCreationController.php new file mode 100644 index 000000000..dd510bfd5 --- /dev/null +++ b/app/Http/Controllers/V2/User/AdminUserCreationController.php @@ -0,0 +1,68 @@ +authorize('create', User::class); + + try { + return DB::transaction(function () use ($request) { + $validatedData = $request->validated(); + + $user = new User($validatedData); + $user->save(); + + $user->email_address_verified_at = $user->created_at; + $user->save(); + + $role = $validatedData['role']; + $user->syncRoles([$role]); + + if (! empty($validatedData['organisation'])) { + $organisation = Organisation::isUuid($validatedData['organisation'])->first(); + if ($organisation) { + $organisation->partners()->updateExistingPivot($user, ['status' => 'approved'], false); + $user->organisation_id = $organisation->id; + $user->save(); + } + } + + if (! empty($validatedData['direct_frameworks'])) { + $frameworkIds = Framework::whereIn('slug', $validatedData['direct_frameworks']) + ->pluck('id') + ->toArray(); + $user->frameworks()->sync($frameworkIds); + } + + return JsonResponseHelper::success(new UserResource($user), 201); + }); + } catch (\Exception $e) { + Log::error('User creation failed', [ + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return JsonResponseHelper::error([ + 'message' => 'Failed to create user', + 'details' => $e->getMessage(), + ], 500); + } + } +} diff --git a/app/Http/Requests/SendLoginDetailsRequest.php b/app/Http/Requests/SendLoginDetailsRequest.php new file mode 100644 index 000000000..095d90b0a --- /dev/null +++ b/app/Http/Requests/SendLoginDetailsRequest.php @@ -0,0 +1,25 @@ + [ + 'required', + 'string', + 'email', + ], + 'callback_url' => [ + 'sometimes', + 'string', + 'url', + 'max:5000', + ], + ]; + } +} diff --git a/app/Http/Requests/SetPasswordRequest.php b/app/Http/Requests/SetPasswordRequest.php new file mode 100644 index 000000000..f75541d5c --- /dev/null +++ b/app/Http/Requests/SetPasswordRequest.php @@ -0,0 +1,24 @@ + [ + 'required', + 'string', + 'exists:password_resets,token', + ], + 'password' => ['required', 'string', Password::min(8)->mixedCase()->numbers()], + ]; + } +} diff --git a/app/Http/Requests/V2/User/AdminUserCreationRequest.php b/app/Http/Requests/V2/User/AdminUserCreationRequest.php new file mode 100644 index 000000000..0c8f2d9a2 --- /dev/null +++ b/app/Http/Requests/V2/User/AdminUserCreationRequest.php @@ -0,0 +1,62 @@ + 'required|string|max:255', + 'last_name' => 'required|string|max:255', + 'email_address' => [ + 'required', + 'string', + 'email', + 'max:255', + 'unique:users,email_address', + ], + 'role' => 'required|string', + 'job_role' => 'sometimes|nullable|string|max:255', + 'country' => 'sometimes|nullable|string|max:2', + 'phone_number' => 'sometimes|nullable|string|max:20', + 'program' => 'sometimes|nullable|string|max:255', + 'organisation' => [ + 'sometimes', + 'nullable', + 'array', + function ($attribute, $value, $fail) { + if (! empty($value) && empty($value['uuid'])) { + $fail('The organisation must contain a uuid.'); + } + }, + ], + 'monitoring_organisations' => 'sometimes|array', + 'monitoring_organisations.*' => 'uuid|exists:organisations,uuid', + 'direct_frameworks' => 'sometimes|array', + 'direct_frameworks.*' => 'string|exists:frameworks,slug', + ]; + } + + public function messages(): array + { + return [ + 'email_address.unique' => 'This email address is already in use.', + 'role.in' => 'Invalid role selected.', + 'organisation.uuid' => 'Invalid organisation identifier.', + 'organisation.exists' => 'Organisation not found.', + 'country.max' => 'Country code must be 2 characters long.', + 'phone_number.max' => 'Phone number cannot exceed 20 characters.', + ]; + } +} diff --git a/app/Jobs/SendLoginDetailsJob.php b/app/Jobs/SendLoginDetailsJob.php new file mode 100644 index 000000000..0c6925407 --- /dev/null +++ b/app/Jobs/SendLoginDetailsJob.php @@ -0,0 +1,54 @@ +model = $model; + $this->callbackUrl = $callbackUrl; + } + + public function handle() + { + try { + if (get_class($this->model) !== \App\Models\V2\User::class) { + throw new Exception('Invalid model type'); + } + + $passwordReset = new PasswordResetModel(); + $passwordReset->user_id = $this->model->id; + $passwordReset->token = Str::random(32); + $passwordReset->saveOrFail(); + Mail::to($this->model->email_address) + ->send(new SendLoginDetails($passwordReset->token, $this->callbackUrl, $this->model)); + } catch (\Throwable $e) { + Log::error('Job failed', ['error' => $e->getMessage()]); + + throw $e; + } + } +} diff --git a/app/Mail/SendLoginDetails.php b/app/Mail/SendLoginDetails.php new file mode 100644 index 000000000..af448a717 --- /dev/null +++ b/app/Mail/SendLoginDetails.php @@ -0,0 +1,25 @@ +setSubjectKey('send-login-details.subject') + ->setTitleKey('send-login-details.title') + ->setBodyKey('send-login-details.body') + ->setParams([ + '{userName}' => e($user->first_name . ' ' . $user->last_name), + '{mail}' => e($user->email_address), + ]) + ->setCta('send-login-details.cta'); + + $this->link = $callbackUrl ? + $callbackUrl . urlencode($token) : + '/set-password?token=' . urlencode($token); + + $this->transactional = true; + } +} diff --git a/database/seeders/LocalizationKeysTableSeeder.php b/database/seeders/LocalizationKeysTableSeeder.php index 18b304341..1676b6c68 100644 --- a/database/seeders/LocalizationKeysTableSeeder.php +++ b/database/seeders/LocalizationKeysTableSeeder.php @@ -165,6 +165,20 @@ public function run(): void 'If you have any questions, feel free to message us at info@terramatch.org.'); $this->createLocalizationKey('reset-password.cta', 'Reset Password'); + // send-login-details + $this->createLocalizationKey('send-login-details.subject', 'Welcome to TerraMatch!'); + $this->createLocalizationKey('send-login-details.title', 'Welcome to TerraMatch 🌱 !'); + $this->createLocalizationKey('send-login-details.body', 'Hi {userName},

' . + 'We\'re thrilled to let you know that your access to TerraMatch is now active!

' . + 'Your user email used for your account is {mail}

' . + 'Please click on the button below to set your new password. This link is valid for 7 days from the day you received this email.

' . + 'If you have any questions or require assistance, our support team is ready to help at info@terramatch.org or +44 7456 289369 (WhatsApp only).

'. + 'We look forward to working with you!

' . + '

' . + 'Best regards,

' . + 'TerraMatch Support'); + $this->createLocalizationKey('send-login-details.cta', 'Set Password'); + // satellite-map-created $this->createLocalizationKey('satellite-map-created.subject', 'Remote Sensing Map Received'); $this->createLocalizationKey('satellite-map-created.title', 'Remote Sensing Map Received'); diff --git a/openapi-src/V2/definitions/AdminUserCreate.yml b/openapi-src/V2/definitions/AdminUserCreate.yml new file mode 100644 index 000000000..47702f755 --- /dev/null +++ b/openapi-src/V2/definitions/AdminUserCreate.yml @@ -0,0 +1,22 @@ +type: object +properties: + first_name: + type: string + last_name: + type: string + email_address: + type: string + job_role: + type: string + phone_number: + type: string + organisation: + type: string + role: + type: string + country: + type: string + program: + type: string + direct_frameworks: + type: boolean \ No newline at end of file diff --git a/openapi-src/V2/definitions/_index.yml b/openapi-src/V2/definitions/_index.yml index fb2f8e31c..3e84461b9 100644 --- a/openapi-src/V2/definitions/_index.yml +++ b/openapi-src/V2/definitions/_index.yml @@ -238,6 +238,8 @@ ProjectReportRead: $ref: './ProjectReportRead.yml' UserCreate: $ref: './UserCreate.yml' +AdminUserCreate: + $ref: './AdminUserCreate.yml' UpdateRequestsPaginated: $ref: './UpdateRequestsPaginated.yml' UpdateRequestRead: diff --git a/openapi-src/V2/paths/Auth/get-auth-mail.yml b/openapi-src/V2/paths/Auth/get-auth-mail.yml new file mode 100644 index 000000000..6b997c49f --- /dev/null +++ b/openapi-src/V2/paths/Auth/get-auth-mail.yml @@ -0,0 +1,51 @@ +summary: "Get email address by reset token" +description: "Retrieves the email address associated with a reset token. If the token has already been used or does not exist, indicates that the token is used." +parameters: + - in: query + name: token + required: true + description: "The reset token" + type: string +responses: + 200: + description: "Successful response" + schema: + type: "object" + properties: + success: + type: "boolean" + example: true + data: + type: "object" + properties: + email_address: + type: "string" + nullable: true + example: "user@example.com" + description: "The email address associated with the reset token, or null if the token is already used." + token_used: + type: "boolean" + example: false + description: "Indicates whether the token has already been used. `true` if the token was used or does not exist, and `false` otherwise." + 404: + description: "User not found for the associated token" + schema: + type: "object" + properties: + success: + type: "boolean" + example: false + message: + type: "string" + example: "User not found" + 500: + description: "Internal server error" + schema: + type: "object" + properties: + success: + type: "boolean" + example: false + message: + type: "string" + example: "Internal Server Error" \ No newline at end of file diff --git a/openapi-src/V2/paths/Auth/post-auth-send-login-details.yml b/openapi-src/V2/paths/Auth/post-auth-send-login-details.yml new file mode 100644 index 000000000..bedd8bcef --- /dev/null +++ b/openapi-src/V2/paths/Auth/post-auth-send-login-details.yml @@ -0,0 +1,20 @@ +operationId: post-auth-send-login-details +summary: Send a password reset email to a user or admin +tags: + - Auth +security: [] +consumes: + - application/json +produces: + - application/json +parameters: + - name: Body + in: body + required: true + schema: + $ref: '../../definitions/_index.yml#/AuthReset' +responses: + '200': + description: OK + schema: + $ref: '../../definitions/_index.yml#/Empty' \ No newline at end of file diff --git a/openapi-src/V2/paths/Auth/post-auth-store.yml b/openapi-src/V2/paths/Auth/post-auth-store.yml new file mode 100644 index 000000000..1f4feee31 --- /dev/null +++ b/openapi-src/V2/paths/Auth/post-auth-store.yml @@ -0,0 +1,20 @@ +operationId: post-auth-store +summary: set a user's or admin's password +tags: + - Auth +security: [] +consumes: + - application/json +produces: + - application/json +parameters: + - name: Body + in: body + required: true + schema: + $ref: '../../definitions/_index.yml#/AuthChange' +responses: + '200': + description: OK + schema: + $ref: '../../definitions/_index.yml#/Empty' \ No newline at end of file diff --git a/openapi-src/V2/paths/_index.yml b/openapi-src/V2/paths/_index.yml index 8f9fa32b8..fa643036d 100644 --- a/openapi-src/V2/paths/_index.yml +++ b/openapi-src/V2/paths/_index.yml @@ -793,6 +793,23 @@ description: OK schema: $ref: '../definitions/_index.yml#/V2AdminUserRead' +/v2/admin/users/create: + post: + summary: Create a user + operationId: v2-admin-post-user + tags: + - V2 Admin + - V2 Users + parameters: + - in: body + name: body + schema: + $ref: '../definitions/_index.yml#/AdminUserCreate' + responses: + '201': + description: Created + schema: + $ref: '../definitions/_index.yml#/V2AdminUserRead' /v2/admin/users/export: get: summary: Export CSV document of all users @@ -2483,6 +2500,15 @@ /auth/reset: post: $ref: './Auth/post-auth-reset.yml' +/auth/send-login-details: + post: + $ref: './Auth/post-auth-send-login-details.yml' +/auth/mail: + get: + $ref: './Auth/get-auth-mail.yml' +/auth/store: + post: + $ref: './Auth/post-auth-store.yml' /v2/auth/verify: patch: $ref: './Auth/patch-v2-auth-verify.yml' diff --git a/resources/docs/swagger-v2.yml b/resources/docs/swagger-v2.yml index 2884a7c92..6ffff9c2a 100644 --- a/resources/docs/swagger-v2.yml +++ b/resources/docs/swagger-v2.yml @@ -42333,6 +42333,29 @@ definitions: type: string program: type: string + AdminUserCreate: + type: object + properties: + first_name: + type: string + last_name: + type: string + email_address: + type: string + job_role: + type: string + phone_number: + type: string + organisation: + type: string + role: + type: string + country: + type: string + program: + type: string + direct_frameworks: + type: boolean UpdateRequestsPaginated: title: UpdateRequestsPaginated type: object @@ -69086,6 +69109,74 @@ paths: type: string date_added: type: string + /v2/admin/users/create: + post: + summary: Create a user + operationId: v2-admin-post-user + tags: + - V2 Admin + - V2 Users + parameters: + - in: body + name: body + schema: + type: object + properties: + first_name: + type: string + last_name: + type: string + email_address: + type: string + job_role: + type: string + phone_number: + type: string + organisation: + type: string + role: + type: string + country: + type: string + program: + type: string + direct_frameworks: + type: boolean + responses: + '201': + description: Created + schema: + title: AdminUserRead + type: object + properties: + uuid: + type: string + status: + type: string + readable_status: + type: string + type: + type: string + first_name: + type: string + last_name: + type: string + email_address: + type: string + job_role: + type: string + facebook: + type: string + instagram: + type: string + linkedin: + type: string + twitter: + type: string + whatsapp_phone: + type: string + date_added: + type: string /v2/admin/users/export: get: summary: Export CSV document of all users @@ -93109,6 +93200,113 @@ paths: description: OK schema: type: object + /auth/send-login-details: + post: + operationId: post-auth-send-login-details + summary: Send a password reset email to a user or admin + tags: + - Auth + security: [] + consumes: + - application/json + produces: + - application/json + parameters: + - name: Body + in: body + required: true + schema: + type: object + properties: + email_address: + type: string + callback_url: + type: string + responses: + '200': + description: OK + schema: + type: object + /auth/mail: + get: + summary: Get email address by reset token + description: 'Retrieves the email address associated with a reset token. If the token has already been used or does not exist, indicates that the token is used.' + parameters: + - in: query + name: token + required: true + description: The reset token + type: string + responses: + '200': + description: Successful response + schema: + type: object + properties: + success: + type: boolean + example: true + data: + type: object + properties: + email_address: + type: string + nullable: true + example: user@example.com + description: 'The email address associated with the reset token, or null if the token is already used.' + token_used: + type: boolean + example: false + description: 'Indicates whether the token has already been used. `true` if the token was used or does not exist, and `false` otherwise.' + '404': + description: User not found for the associated token + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: User not found + '500': + description: Internal server error + schema: + type: object + properties: + success: + type: boolean + example: false + message: + type: string + example: Internal Server Error + /auth/store: + post: + operationId: post-auth-store + summary: set a user's or admin's password + tags: + - Auth + security: [] + consumes: + - application/json + produces: + - application/json + parameters: + - name: Body + in: body + required: true + schema: + type: object + properties: + token: + type: string + password: + type: string + responses: + '200': + description: OK + schema: + type: object /v2/auth/verify: patch: operationId: patch-v2-auth-verify diff --git a/routes/api.php b/routes/api.php index 372b4b028..640697e0a 100644 --- a/routes/api.php +++ b/routes/api.php @@ -115,6 +115,9 @@ Route::get('/auth/resend', [AuthController::class, 'resendAction']); Route::post('/auth/reset', [AuthController::class, 'resetAction']); Route::patch('/auth/change', [AuthController::class, 'changeAction']); + Route::post('/auth/send-login-details', [AuthController::class, 'sendLoginDetailsAction']); + Route::get('/auth/mail', [AuthController::class, 'getEmailByResetTokenAction']); + Route::post('/auth/store', [AuthController::class, 'setNewPasswordAction']); Route::patch('/v2/auth/verify', [AuthController::class, 'verifyUnauthorizedAction']); Route::put('/v2/auth/complete/signup', [AuthController::class, 'completeUserSignup']); }); diff --git a/routes/api_v2.php b/routes/api_v2.php index e1d77685e..1df197c28 100644 --- a/routes/api_v2.php +++ b/routes/api_v2.php @@ -214,6 +214,7 @@ use App\Http\Controllers\V2\User\AdminExportUsersController; use App\Http\Controllers\V2\User\AdminResetPasswordController; use App\Http\Controllers\V2\User\AdminUserController; +use App\Http\Controllers\V2\User\AdminUserCreationController; use App\Http\Controllers\V2\User\AdminUserMultiController; use App\Http\Controllers\V2\User\AdminUsersOrganizationController; use App\Http\Controllers\V2\User\AdminVerifyUserController; @@ -363,6 +364,7 @@ Route::put('reset-password/{user}', AdminResetPasswordController::class); Route::patch('verify/{user}', AdminVerifyUserController::class); Route::get('users-organisation-list/{organisation}', AdminUsersOrganizationController::class); + Route::post('/create', [AdminUserCreationController::class, 'store']); }); Route::resource('users', AdminUserController::class);