Skip to content

Commit

Permalink
Merge branch 'staging' into add_endpoint_with_role_id_to_sign_up
Browse files Browse the repository at this point in the history
  • Loading branch information
pachonjcl committed Apr 22, 2024
2 parents e298a0a + f0c7d25 commit b8bd082
Show file tree
Hide file tree
Showing 27 changed files with 489 additions and 188 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/public/hot
/public/storage
/storage/*.key
/storage/*.zip
/vendor
.env
/.phpunit.cache
Expand Down
2 changes: 1 addition & 1 deletion app/Console/Commands/Migration/RolesMigrationCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ public function handle()
$role->givePermissionTo(['projects-read', 'polygons-manage', 'media-manage']);
}

User::whereIn('role', ['user','admin', 'terrafund-admin'])->get()
User::whereIn('role', ['user', 'admin', 'terrafund-admin', 'service'])->get()
->each(function (User $user) {
if ($user->primary_role == null) {
assignSpatieRole($user);
Expand Down
5 changes: 5 additions & 0 deletions app/Exceptions/Handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
use Spatie\MediaLibrary\MediaCollections\Exceptions\MimeTypeNotAllowed;
use Spatie\MediaLibrary\MediaCollections\Exceptions\UnreachableUrl;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
Expand Down Expand Up @@ -345,6 +347,9 @@ public function render($request, Throwable $exception)
return JsonResponseHelper::error([], 404);
case InvalidStatusException::class:
return JsonResponseHelper::error($exception->getMessage(), 422);
case MimeTypeNotAllowed::class:
case UnreachableUrl::class:
return JsonResponseHelper::error([[$exception->getMessage()]], 422);
default:
if (config('app.env') == 'local') {
return new Response($this->renderExceptionContent($exception), 500, ['Content-Type' => 'text/html']);
Expand Down
139 changes: 50 additions & 89 deletions app/Http/Controllers/V2/Files/UploadController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,116 +3,75 @@
namespace App\Http\Controllers\V2\Files;

use App\Http\Controllers\Controller;
use App\Http\Requests\V2\File\BulkUploadRequest;
use App\Http\Requests\V2\File\UploadRequest;
use App\Http\Resources\V2\Files\FileResource;
use App\Models\V2\Forms\Form;
use App\Models\V2\Forms\FormQuestionOption;
use App\Models\V2\FundingProgramme;
use App\Models\V2\Nurseries\Nursery;
use App\Models\V2\Nurseries\NurseryReport;
use App\Models\V2\Organisation;
use App\Models\V2\ProjectPitch;
use App\Models\V2\Projects\Project;
use App\Models\V2\Projects\ProjectMonitoring;
use App\Models\V2\Projects\ProjectReport;
use App\Models\V2\Sites\Site;
use App\Models\V2\Sites\SiteMonitoring;
use App\Models\V2\Sites\SiteReport;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use App\Models\V2\MediaModel;
use Exception;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Validator;
use mysql_xdevapi\Exception;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

class UploadController extends Controller
{
public function __invoke(UploadRequest $request, $model, $collection, $uuid)
public function __invoke(UploadRequest $request, string $collection, MediaModel $mediaModel)
{
$entity = $this->getEntity($model, $uuid);
$this->authorize('uploadFiles', $entity);
$config = $this->getConfiguration($entity, $collection);
$this->authorize('uploadFiles', $mediaModel);
$config = $this->getConfiguration($mediaModel, $collection);
$this->validateFile($request, $config);

$qry = $entity->addMediaFromRequest('upload_file');
$this->prepHandler($qry, $request->all(), $entity, $config, $collection);
$qry = $mediaModel->addMediaFromRequest('upload_file');
$this->prepHandler($qry, $request->all(), $mediaModel, $config, $collection);
$details = $this->executeHandler($qry, $collection);

if (Arr::has($request->all(), ['lat', 'lng'])) {
$this->saveFileCoordinates($details, $request);
}

$this->saveAdditionalFileProperties($details, $request, $config);
$this->saveFileCoordinates($details, $request->all());
$this->saveAdditionalFileProperties($details, $request->all(), $config);

return new FileResource($details);
}

private function getEntity($model, $uuid): Model
public function bulkUrlUpload(BulkUploadRequest $request, string $collection, MediaModel $mediaModel)
{
switch ($model) {
case 'organisation':
$entity = Organisation::isUuid($uuid)->first();

break;
case 'project-pitch':
$entity = ProjectPitch::isUuid($uuid)->first();

break;
case 'funding-programme':
$entity = FundingProgramme::isUuid($uuid)->first();

break;
case 'form':
$entity = Form::isUuid($uuid)->first();

break;
case 'form-question-option':
$entity = FormQuestionOption::isUuid($uuid)->first();

break;
case 'project':
$entity = Project::isUuid($uuid)->first();
$this->authorize('uploadFiles', $mediaModel);

break;
case 'site':
$entity = Site::isUuid($uuid)->first();

break;
case 'nursery':
$entity = Nursery::isUuid($uuid)->first();

break;
case 'project-report':
$entity = ProjectReport::isUuid($uuid)->first();
if ($collection != 'photos') {
// Only the photos collection is allowed for bulk upload
throw new NotFoundHttpException();
}

break;
case 'site-report':
$entity = SiteReport::isUuid($uuid)->first();
$config = $this->getConfiguration($mediaModel, $collection);
$files = [];

break;
case 'nursery-report':
$entity = NurseryReport::isUuid($uuid)->first();
try {
foreach ($request->getPayload() as $data) {
// The downloadable file gets shuttled through the internals of Spatie without a chance for us to run
// our own validations on them. png/jpg are the only mimes allowed for the photos collection according
// to config/file-handling.php, and we disallow other collections than 'photos' above.
$handler = $mediaModel->addMediaFromUrl($data['download_url'], 'image/png', 'image/jpg');

break;
case 'project-monitoring':
$entity = ProjectMonitoring::isUuid($uuid)->first();
$this->prepHandler($handler, $data, $mediaModel, $config, $collection);
$details = $this->executeHandler($handler, $collection);

break;
case 'site-monitoring':
$entity = SiteMonitoring::isUuid($uuid)->first();
$this->saveFileCoordinates($details, $data);
$this->saveAdditionalFileProperties($details, $data, $config);

break;
}
$files[] = $details;
}
} catch (Exception $exception) {
// if we get an error in the bulk upload, remove any media that did successfully get saved.
foreach ($files as $file) {
$file->delete();
}

if (empty($entity)) {
throw new ModelNotFoundException();
throw $exception;
}

return $entity;
return FileResource::collection($files);
}

private function getConfiguration($entity, $collection): array
private function getConfiguration(MediaModel $mediaModel, $collection): array
{
$config = $entity->fileConfiguration[$collection];
$config = $mediaModel->fileConfiguration[$collection];

if (empty($config)) {
throw new Exception('Collection is unknown to this model.');
Expand All @@ -136,14 +95,14 @@ private function validateFile($request, $config): void
$validator->validate();
}

private function prepHandler($qry, $data, $entity, $config, $collection): void
private function prepHandler($qry, $data, MediaModel $mediaModel, $config, $collection): void
{
if (data_get($data, 'title', false)) {
$qry->usingName(data_get($data, 'title'));
}

if (! data_get($config, 'multiple', true)) {
$entity->clearMediaCollection($collection);
$mediaModel->clearMediaCollection($collection);
}
}

Expand All @@ -155,17 +114,19 @@ private function executeHandler($handler, $collection)
->toMediaCollection($collection);
}

private function saveFileCoordinates($media, $request)
private function saveFileCoordinates($media, $data)
{
$media->lat = $request->lat;
$media->lng = $request->lng;
$media->save();
if (Arr::has($data, ['lat', 'lng'])) {
$media->lat = $data['lat'];
$media->lng = $data['lng'];
$media->save();
}
}

private function saveAdditionalFileProperties($media, $request, $config)
private function saveAdditionalFileProperties($media, $data, $config)
{
$media->file_type = $this->getType($media, $config);
$media->is_public = $request->is_public ?? true;
$media->is_public = $data['is_public'] ?? true;
$media->save();
}

Expand Down
64 changes: 51 additions & 13 deletions app/Http/Middleware/ModelInterfaceBindingMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,79 @@

namespace App\Http\Middleware;

use App\Models\V2\Forms\Form;
use App\Models\V2\Forms\FormQuestionOption;
use App\Models\V2\FundingProgramme;
use App\Models\V2\Nurseries\Nursery;
use App\Models\V2\Nurseries\NurseryReport;
use App\Models\V2\Organisation;
use App\Models\V2\ProjectPitch;
use App\Models\V2\Projects\Project;
use App\Models\V2\Projects\ProjectMonitoring;
use App\Models\V2\Projects\ProjectReport;
use App\Models\V2\Sites\Site;
use App\Models\V2\Sites\SiteMonitoring;
use App\Models\V2\Sites\SiteReport;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Routing\RouteRegistrar;
use Illuminate\Support\Facades\Route;

/**
* Implicit binding doesn't work for interfaces, so we need to figure out the concrete model class and
* load the instance ourselves.
*/
class ModelInterfaceBindingMiddleware
{
public const ENTITY_TYPES_PLURAL = ['projects', 'project-reports', 'sites', 'site-reports', 'nurseries', 'nursery-reports'];
public const ENTITY_TYPES_SINGULAR = ['project', 'project-report', 'site', 'site-report', 'nursery', 'nursery-report'];

private const CONCRETE_MODELS = [
// EntityModel concrete classes
// EntityModel and MediaModel concrete classes
'projects' => Project::class,
'project' => Project::class,
'sites' => Site::class,
'site' => Site::class,
'nurseries' => Nursery::class,
'nursery' => Nursery::class,

// ReportModel (which extends EntityModel) concrete classes
'project-reports' => ProjectReport::class,
'project-report' => ProjectReport::class,
'sites' => Site::class,
'site' => Site::class,
'site-reports' => SiteReport::class,
'site-report' => SiteReport::class,
'nurseries' => Nursery::class,
'nursery' => Nursery::class,
'nursery-reports' => NurseryReport::class,
'nursery-report' => NurseryReport::class,

// MediaModel concrete classes
'organisation' => Organisation::class,
'project-pitch' => ProjectPitch::class,
'funding-programme' => FundingProgramme::class,
'form' => Form::class,
'form-question-option' => FormQuestionOption::class,
'project-monitoring' => ProjectMonitoring::class,
'site-monitoring' => SiteMonitoring::class,
];

public function handle(Request $request, Closure $next)
private static array $typeSlugsCache = [];

public static function with(string $interface, callable $routeGroup, string $prefix = null, string $modelParameter = null): RouteRegistrar
{
$typeSlugs = self::$typeSlugsCache[$interface] ?? [];
if (empty($typeSlugs)) {
foreach (self::CONCRETE_MODELS as $slug => $concrete) {
if (is_a($concrete, $interface, true)) {
$typeSlugs[] = $slug;
}
}

self::$typeSlugsCache[$interface] = $typeSlugs;
}

$middleware = $modelParameter == null ? 'modelInterface' : "modelInterface:$modelParameter";

return Route::prefix("$prefix/{modelSlug}")
->whereIn('modelSlug', $typeSlugs)
->middleware($middleware)
->group($routeGroup);
}

public function handle(Request $request, Closure $next, $modelParameter = null)
{
$route = $request->route();
$parameterKeys = array_keys($route->parameters);
Expand All @@ -51,8 +87,10 @@ public function handle(Request $request, Closure $next)
$concreteClass = self::CONCRETE_MODELS[$modelSlug];
abort_unless($concreteClass, 404, "Concrete class not found for model interface $modelSlug");

// assume the model key (e.g. "report") is the next param down the list from the interface name.
$modelParameter = $parameterKeys[$modelSlugIndex + 1];
if ($modelParameter == null) {
// assume the model key (e.g. "report") is the next param down the list from the interface name.
$modelParameter = $parameterKeys[$modelSlugIndex + 1];
}
$modelId = $route->parameter($modelParameter);
abort_unless($modelId, 404, "Model ID not found for $concreteClass");

Expand Down
46 changes: 46 additions & 0 deletions app/Http/Requests/V2/File/BulkUploadRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace App\Http\Requests\V2\File;

use Illuminate\Foundation\Http\FormRequest;

class BulkUploadRequest extends FormRequest
{
public function authorize()
{
return true;
}

public function rules()
{
return [
'*.uuid' => [
'prohibited',
],
'*.download_url' => [
'required',
'url',
],
'*.title' => [
'sometimes',
'nullable',
'string',
],
'*.lat' => [
'nullable',
'numeric',
'between:-90,90',
],
'*.lng' => [
'nullable',
'numeric',
'between:-180,180',
],
'*.is_public' => [
'sometimes',
'nullable',
'boolean',
],
];
}
}
Loading

0 comments on commit b8bd082

Please sign in to comment.