diff --git a/app/Http/Controllers/V2/Terrafund/TerrafundClipGeometryController.php b/app/Http/Controllers/V2/Terrafund/TerrafundClipGeometryController.php index 67d92f8fe..d01f8cc77 100644 --- a/app/Http/Controllers/V2/Terrafund/TerrafundClipGeometryController.php +++ b/app/Http/Controllers/V2/Terrafund/TerrafundClipGeometryController.php @@ -24,9 +24,13 @@ public function clipOverlappingPolygonsBySite(string $uuid) ini_set('max_execution_time', self::MAX_EXECUTION_TIME); ini_set('memory_limit', '-1'); $user = Auth::user(); + $site = Site::isUuid($uuid)->first(); $polygonUuids = GeometryHelper::getSitePolygonsUuids($uuid)->toArray(); $delayedJob = DelayedJobProgress::create([ 'processed_content' => 0, + 'created_by' => $user->id, + 'entity_id' => $site->id, + 'entity_type' => get_class($site), ]); $job = new FixPolygonOverlapJob($delayedJob->id, $polygonUuids, $user->id); dispatch($job); @@ -80,6 +84,9 @@ public function clipOverlappingPolygonsOfProjectBySite(string $uuid) $delayedJob = DelayedJobProgress::create([ 'processed_content' => 0, + 'entity_id' => $sitePolygon->id, + 'entity_type' => get_class($sitePolygon), + 'created_by' => $user->id, ]); $job = new FixPolygonOverlapJob($delayedJob->id, $uniquePolygonUuids, $user->id); dispatch($job); @@ -92,6 +99,11 @@ public function clipOverlappingPolygons(Request $request) ini_set('max_execution_time', self::MAX_EXECUTION_TIME); ini_set('memory_limit', '-1'); $uuids = $request->input('uuids'); + $uuid = $request->input('entity_uuid'); + $type = $request->input('entity_type'); + if ($type === 'sites') { + $entity = Site::where('uuid', $uuid)->firstOrFail(); + } Log::info('Clipping polygons', ['uuids' => $uuids]); if (empty($uuids) || ! is_array($uuids)) { return response()->json(['error' => 'Invalid or missing UUIDs'], 400); @@ -130,6 +142,9 @@ public function clipOverlappingPolygons(Request $request) $user = Auth::user(); $delayedJob = DelayedJobProgress::create([ 'processed_content' => 0, + 'entity_id' => $entity->id, + 'entity_type' => get_class($entity), + 'created_by' => $user->id, ]); $job = new FixPolygonOverlapJob($delayedJob->id, $polygonUuids, $user->id); dispatch($job); diff --git a/app/Http/Controllers/V2/Terrafund/TerrafundCreateGeometryController.php b/app/Http/Controllers/V2/Terrafund/TerrafundCreateGeometryController.php index dab1bddbb..7a5c72e1f 100755 --- a/app/Http/Controllers/V2/Terrafund/TerrafundCreateGeometryController.php +++ b/app/Http/Controllers/V2/Terrafund/TerrafundCreateGeometryController.php @@ -235,9 +235,16 @@ function ($attribute, $value, $fail) { return response()->json($polygonLoaded->original, 200); } + $user = Auth::user(); + $entity = Site::where('uuid', $site_id)->firstOrFail(); + $redis_key = 'kml_file_' . uniqid(); Redis::set($redis_key, $geojsonContent, 'EX', 7200); - $delayedJob = DelayedJob::create(); + $delayedJob = DelayedJob::create([ + 'created_by' => $user->id, + 'entity_id' => $entity->id, + 'entity_type' => get_class($entity), + ]); $job = new InsertGeojsonToDBJob( $redis_key, @@ -394,10 +401,16 @@ public function uploadShapefile(Request $request) return response()->json($polygonLoaded->original, 200); } + $user = Auth::user(); + $entity = Site::where('uuid', $site_id)->firstOrFail(); $redis_key = 'shapefile_file_' . uniqid(); Redis::set($redis_key, $geojsonContent, 'EX', 7200); - $delayedJob = DelayedJob::create(); + $delayedJob = DelayedJob::create([ + 'created_by' => $user->id, + 'entity_id' => $entity->id, + 'entity_type' => get_class($entity), + ]); $job = new InsertGeojsonToDBJob( $redis_key, @@ -614,10 +627,16 @@ public function uploadGeoJSONFile(Request $request) return response()->json($polygonLoaded->original, 200); } + $user = Auth::user(); + $entity = Site::where('uuid', $site_id)->firstOrFail(); $redis_key = 'geojson_file_' . uniqid(); Redis::set($redis_key, $geojson_content, 'EX', 7200); - $delayedJob = DelayedJob::create(); + $delayedJob = DelayedJob::create([ + 'created_by' => $user->id, + 'entity_id' => $entity->id, + 'entity_type' => get_class($entity), + ]); $job = new InsertGeojsonToDBJob( $redis_key, @@ -1219,10 +1238,15 @@ public function runSiteValidationPolygon(Request $request) try { $uuid = $request->input('uuid'); + $user = Auth::user(); + $entity = Site::where('uuid', $uuid)->firstOrFail(); $sitePolygonsUuids = GeometryHelper::getSitePolygonsUuids($uuid)->toArray(); $delayedJob = DelayedJobProgress::create([ 'total_content' => count($sitePolygonsUuids), 'processed_content' => 0, + 'created_by' => $user->id, + 'entity_id' => $entity->id, + 'entity_type' => get_class($entity), ]); $job = new RunSitePolygonsValidationJob($delayedJob->id, $sitePolygonsUuids); dispatch($job); @@ -1239,9 +1263,18 @@ public function runPolygonsValidation(Request $request) { try { $uuids = $request->input('uuids'); + $uuid = $request->input('entity_uuid'); + $type = $request->input('entity_type'); + if ($type === 'sites') { + $entity = Site::where('uuid', $uuid)->firstOrFail(); + } + $user = Auth::user(); $delayedJob = DelayedJobProgress::create([ 'total_content' => count($uuids), 'processed_content' => 0, + 'created_by' => $user->id, + 'entity_id' => $entity->id, + 'entity_type' => get_class($entity), ]); $job = new RunSitePolygonsValidationJob($delayedJob->id, $uuids); dispatch($job); diff --git a/app/Http/Resources/DelayedJobProgressResource.php b/app/Http/Resources/DelayedJobProgressResource.php index 234c2ad8b..59c1dccfe 100644 --- a/app/Http/Resources/DelayedJobProgressResource.php +++ b/app/Http/Resources/DelayedJobProgressResource.php @@ -19,7 +19,7 @@ public function toArray(Request $request): array 'job_uuid' => $this->uuid, 'proccessed_content' => $this->processed_content, 'total_content' => $this->total_content, - 'proccess_message' => $this->proccess_message, + 'progress_message' => $this->progress_message, ]; } } diff --git a/app/Jobs/FixPolygonOverlapJob.php b/app/Jobs/FixPolygonOverlapJob.php index a272f8693..16850a39b 100644 --- a/app/Jobs/FixPolygonOverlapJob.php +++ b/app/Jobs/FixPolygonOverlapJob.php @@ -3,6 +3,7 @@ namespace App\Jobs; use App\Http\Middleware\SetAuthenticatedUserForJob; +use App\Mail\PolygonOperationsComplete; use App\Models\DelayedJob; use App\Models\DelayedJobProgress; use App\Services\PolygonService; @@ -16,6 +17,7 @@ use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; use Throwable; class FixPolygonOverlapJob implements ShouldQueue @@ -66,6 +68,8 @@ public function handle(): void try { $delayedJob = DelayedJobProgress::findOrFail($this->delayed_job_id); $user = Auth::user(); + $site = $delayedJob->entity; + $userForMail = $delayedJob->creator; if ($user) { $polygonsClipped = App::make(PolygonService::class)->processClippedPolygons($this->polygonUuids, $this->delayed_job_id); $delayedJob->update([ @@ -74,6 +78,14 @@ public function handle(): void 'status_code' => Response::HTTP_OK, 'progress' => 100, ]); + + Mail::to($user->email_address) + ->send(new PolygonOperationsComplete( + $site, + 'Fix', + $userForMail, + now() + )); } } catch (Exception $e) { Log::error('Error in Fix Polygon Overlap Job: ' . $e->getMessage()); @@ -84,7 +96,7 @@ public function handle(): void 'status_code' => Response::HTTP_INTERNAL_SERVER_ERROR, ]); } catch (Throwable $e) { - Log::error('Throwable Error in RunSitePolygonsValidationJob: ' . $e->getMessage()); + Log::error('Throwable Error in Fix overlap job: ' . $e->getMessage()); DelayedJob::where('uuid', $this->delayed_job_id)->update([ 'status' => DelayedJob::STATUS_FAILED, diff --git a/app/Jobs/InsertGeojsonToDBJob.php b/app/Jobs/InsertGeojsonToDBJob.php index a07752d5b..a6caf7582 100755 --- a/app/Jobs/InsertGeojsonToDBJob.php +++ b/app/Jobs/InsertGeojsonToDBJob.php @@ -2,19 +2,21 @@ namespace App\Jobs; +use App\Mail\PolygonOperationsComplete; use App\Models\DelayedJob; use App\Services\PolygonService; use App\Services\SiteService; use Exception; use Illuminate\Bus\Queueable; -use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Http\Response; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Redis; class InsertGeojsonToDBJob implements ShouldQueue @@ -51,6 +53,8 @@ public function __construct(string $redis_key, string $delayed_job_id, ?string $ public function handle(PolygonService $service) { $delayedJob = DelayedJob::findOrFail($this->delayed_job_id); + $user = $delayedJob->creator; + $site = $delayedJob->entity; try { $geojsonContent = Redis::get($this->redis_key); @@ -86,6 +90,14 @@ public function handle(PolygonService $service) 'status_code' => Response::HTTP_OK, ]); + Mail::to($user->email_address) + ->send(new PolygonOperationsComplete( + $site, + 'Upload', + $user, + now() + )); + } catch (Exception $e) { Log::error('Error in InsertGeojsonToDBJob: ' . $e->getMessage()); $delayedJob->update([ diff --git a/app/Jobs/RunSitePolygonsValidationJob.php b/app/Jobs/RunSitePolygonsValidationJob.php index 9bba9777c..34f4f5a98 100644 --- a/app/Jobs/RunSitePolygonsValidationJob.php +++ b/app/Jobs/RunSitePolygonsValidationJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Mail\PolygonOperationsComplete; use App\Models\DelayedJob; use App\Models\DelayedJobProgress; use App\Services\PolygonValidationService; @@ -14,6 +15,7 @@ use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Mail; class RunSitePolygonsValidationJob implements ShouldQueue { @@ -50,6 +52,8 @@ public function handle(PolygonValidationService $validationService) { try { $delayedJob = DelayedJobProgress::findOrFail($this->delayed_job_id); + $user = $delayedJob->creator; + $site = $delayedJob->entity; foreach ($this->sitePolygonsUuids as $polygonUuid) { $request = new Request(['uuid' => $polygonUuid]); $validationService->validateOverlapping($request); @@ -74,6 +78,14 @@ public function handle(PolygonValidationService $validationService) 'progress' => 100, ]); + Mail::to($user->email_address) + ->send(new PolygonOperationsComplete( + $site, + 'Check', + $user, + now() + )); + } catch (Exception $e) { Log::error('Error in RunSitePolygonsValidationJob: ' . $e->getMessage()); diff --git a/app/Mail/PolygonOperationsComplete.php b/app/Mail/PolygonOperationsComplete.php new file mode 100644 index 000000000..3d7b74726 --- /dev/null +++ b/app/Mail/PolygonOperationsComplete.php @@ -0,0 +1,35 @@ +site = $site; + $this->operation = $operation; + $this->completedAt = $completedAt; + + $this->setSubjectKey('polygon-validation.subject') + ->setTitleKey('polygon-validation.title') + ->setBodyKey('polygon-validation.body') + ->setParams([ + '{operation}' => e($operation), + '{operationUpper}' => strtoupper(e($operation)), + '{siteName}' => e($site->name), + '{completedTime}' => $completedAt->format('H:i'), + ]) + ->setCta('polygon-validation.cta'); + + $this->link = '/sites/' . $site->uuid; + $this->transactional = true; + } +} diff --git a/app/Models/DelayedJob.php b/app/Models/DelayedJob.php index 4ae833449..95ee0a576 100644 --- a/app/Models/DelayedJob.php +++ b/app/Models/DelayedJob.php @@ -3,6 +3,7 @@ namespace App\Models; use App\Models\Traits\HasUuid; +use App\Models\V2\User; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -17,14 +18,19 @@ class DelayedJob extends Model protected $table = 'delayed_jobs'; - protected $fillable = [ - 'uuid', - 'status', - 'status_code', - 'payload', - ]; + protected $fillable = ['uuid', 'status', 'status_code', 'payload', 'entity_type', 'entity_id', 'created_by']; protected $casts = [ 'uuid' => 'string', ]; + + public function entity() + { + return $this->morphTo(); + } + + public function creator() + { + return $this->belongsTo(User::class, 'created_by'); + } } diff --git a/app/Models/DelayedJobProgress.php b/app/Models/DelayedJobProgress.php index a01fda4f2..8dcd57867 100644 --- a/app/Models/DelayedJobProgress.php +++ b/app/Models/DelayedJobProgress.php @@ -11,13 +11,13 @@ public function __construct(array $attributes = []) $this->fillable = array_merge($this->fillable, [ 'processed_content', 'total_content', - 'proccess_message', + 'progress_message', ]); $this->casts = array_merge($this->casts, [ 'processed_content' => 'integer', 'total_content' => 'integer', - 'proccess_message' => 'string', + 'progress_message' => 'string', ]); } @@ -30,7 +30,7 @@ public function processMessage(): string $progress = 0; } - return $this->proccess_message = 'Running '. $this->processed_content .' out of ' + return $this->progress_message = 'Running '. $this->processed_content .' out of ' .$this->total_content. ' polygons ('.$progress.'%)' ; } } diff --git a/app/Models/Site.php b/app/Models/Site.php index b50b01d87..12d4ef4ef 100644 --- a/app/Models/Site.php +++ b/app/Models/Site.php @@ -156,4 +156,9 @@ public function getTotalWorkdaysAttribute(): int { return $this->total_paid_workdays + $this->total_volunteer_workdays; } + + public function delayedJobs() + { + return $this->morphMany(DelayedJob::class, 'entity'); + } } diff --git a/database/migrations/2024_12_04_151101_add_entity_morph_and_created_by.php b/database/migrations/2024_12_04_151101_add_entity_morph_and_created_by.php new file mode 100644 index 000000000..af98d3991 --- /dev/null +++ b/database/migrations/2024_12_04_151101_add_entity_morph_and_created_by.php @@ -0,0 +1,31 @@ +nullableMorphs('entity'); + $table->string('created_by')->nullable(); + $table->boolean('is_cleared')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('delayed_jobs', function (Blueprint $table) { + $table->dropMorphs('entity'); + $table->dropColumn('created_by'); + $table->dropColumn('is_cleared'); + }); + } +}; diff --git a/database/migrations/2024_12_06_190555_modify_columns_names_in_delayed_jobs_table.php b/database/migrations/2024_12_06_190555_modify_columns_names_in_delayed_jobs_table.php new file mode 100644 index 000000000..b13e0e2e9 --- /dev/null +++ b/database/migrations/2024_12_06_190555_modify_columns_names_in_delayed_jobs_table.php @@ -0,0 +1,27 @@ +renameColumn('proccess_message', 'progress_message'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('delayed_jobs', function (Blueprint $table) { + $table->renameColumn('progress_message', 'proccess_message'); + }); + } +}; diff --git a/database/seeders/LocalizationKeysTableSeeder.php b/database/seeders/LocalizationKeysTableSeeder.php index 1676b6c68..8f1f68c3d 100644 --- a/database/seeders/LocalizationKeysTableSeeder.php +++ b/database/seeders/LocalizationKeysTableSeeder.php @@ -179,6 +179,12 @@ public function run(): void 'TerraMatch Support'); $this->createLocalizationKey('send-login-details.cta', 'Set Password'); + // polygon-operations-complete + $this->createLocalizationKey('polygon-validation.subject', 'Your TerraMatch Polygon {operation} is Complete'); + $this->createLocalizationKey('polygon-validation.title', 'YOUR POLYGON {operationUpper} IS COMPLETE'); + $this->createLocalizationKey('polygon-validation.body', 'Your {operation} for Site {siteName} completed at {completedTime} GMT.'); + $this->createLocalizationKey('polygon-validation.cta', 'OPEN SITE'); + // 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/paths/Terrafund/post-v2-terrafund-clip-polygons-polygons.yml b/openapi-src/V2/paths/Terrafund/post-v2-terrafund-clip-polygons-polygons.yml index 09424689e..500460d55 100644 --- a/openapi-src/V2/paths/Terrafund/post-v2-terrafund-clip-polygons-polygons.yml +++ b/openapi-src/V2/paths/Terrafund/post-v2-terrafund-clip-polygons-polygons.yml @@ -12,6 +12,12 @@ parameters: items: type: string description: UUIDs of the polygon geometries to be fixed + entity_type: + type: string + description: The entity type of the polygon geometries to be fixed + entity_uuid: + type: string + description: The entity ID of the polygon geometries to be fixed responses: '200': description: A list of processed and unprocessed polygons diff --git a/openapi-src/V2/paths/Terrafund/post-v2-terrafund-validation-polygons.yml b/openapi-src/V2/paths/Terrafund/post-v2-terrafund-validation-polygons.yml index c63331051..f4582f40f 100644 --- a/openapi-src/V2/paths/Terrafund/post-v2-terrafund-validation-polygons.yml +++ b/openapi-src/V2/paths/Terrafund/post-v2-terrafund-validation-polygons.yml @@ -11,6 +11,10 @@ parameters: items: type: string description: UUIDs of the polygon geometries to be validated + entity_uuid: + type: string + entity_type: + type: string responses: '200': description: Successful response diff --git a/resources/docs/swagger-v2.yml b/resources/docs/swagger-v2.yml index 6ffff9c2a..895e2336a 100644 --- a/resources/docs/swagger-v2.yml +++ b/resources/docs/swagger-v2.yml @@ -99171,6 +99171,12 @@ paths: items: type: string description: UUIDs of the polygon geometries to be fixed + entity_type: + type: string + description: The entity type of the polygon geometries to be fixed + entity_uuid: + type: string + description: The entity ID of the polygon geometries to be fixed responses: '200': description: A list of processed and unprocessed polygons @@ -99220,6 +99226,10 @@ paths: items: type: string description: UUIDs of the polygon geometries to be validated + entity_uuid: + type: string + entity_type: + type: string responses: '200': description: Successful response