Skip to content

Commit

Permalink
Implement metadata file upload for new pending volumes
Browse files Browse the repository at this point in the history
  • Loading branch information
mzur committed Mar 6, 2024
1 parent 2310e99 commit b535d7f
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 6 deletions.
22 changes: 21 additions & 1 deletion app/Http/Controllers/Api/ProjectPendingVolumeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,32 @@ class ProjectPendingVolumeController extends Controller
* @apiParam {Number} id The project ID.
*
* @apiParam (Required attributes) {String} media_type The media type of the new volume (`image` or `video`).
*
* @apiParam (Optional attributes) {File} metadata_file A file with volume and image/video metadata. By default, this can be a CSV. See "metadata columns" for the possible columns. Each column may occur only once. There must be at least one column other than `filename`. For video metadata, multiple rows can contain metadata from different times of the same video. In this case, the `filename` of the rows must match and each row needs a (different) `taken_at` timestamp. Other formats may be supported through modules.
*
* @apiParam (metadata columns) {String} filename The filename of the file the metadata belongs to. This column is required.
* @apiParam (metadata columns) {String} taken_at The date and time where the file was taken. Example: `2016-12-19 12:49:00`
* @apiParam (metadata columns) {Number} lng Longitude where the file was taken in decimal form. If this column is present, `lat` must be present, too. Example: `52.3211`
* @apiParam (metadata columns) {Number} lat Latitude where the file was taken in decimal form. If this column is present, `lng` must be present, too. Example: `28.775`
* @apiParam (metadata columns) {Number} gps_altitude GPS Altitude where the file was taken in meters. Negative for below sea level. Example: `-1500.5`
* @apiParam (metadata columns) {Number} distance_to_ground Distance to the sea floor in meters. Example: `30.25`
* @apiParam (metadata columns) {Number} area Area shown by the file in m². Example `2.6`.
*/
public function store(StorePendingVolume $request)
{
return $request->project->pendingVolumes()->create([
$pv = $request->project->pendingVolumes()->create([
'media_type_id' => $request->input('media_type_id'),
'user_id' => $request->user()->id,
]);

if ($request->has('metadata_file')) {
$pv->saveMetadata($request->file('metadata_file'));
}

return $pv;
}

// TODO implement update() endpoint to create the volume and optionally continue to
// import annotations/file labels.
// See: https://github.com/biigle/core/issues/701#issue-2000484824
}
31 changes: 27 additions & 4 deletions app/Http/Requests/StorePendingVolume.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

use Biigle\MediaType;
use Biigle\Project;
use Biigle\Rules\ImageMetadata;
use Biigle\Rules\VideoMetadata;
use Biigle\Services\MetadataParsing\ParserFactory;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

Expand All @@ -28,6 +31,7 @@ public function rules(): array
{
return [
'media_type' => ['required', Rule::in(array_keys(MediaType::INSTANCES))],
'metadata_file' => ['file'],
];
}

Expand All @@ -39,16 +43,35 @@ public function rules(): array
*/
public function withValidator($validator)
{
if ($validator->fails()) {
return;
}

$validator->after(function ($validator) {
if ($validator->errors()->isNotEmpty()) {
return;
}

$exists = $this->project->pendingVolumes()
->where('user_id', $this->user()->id)
->exists();
if ($exists) {
$validator->errors()->add('id', 'Only a single pending volume can be created at a time for each project and user.');
return;
}

if ($file = $this->file('metadata_file')) {
$type = $this->input('media_type');
$parser = ParserFactory::getParserForFile($file, $type);
if (is_null($parser)) {
$validator->errors()->add('metadata_file', 'Unknown metadata file format.');
return;
}

$rule = match ($type) {
'video' => new VideoMetadata,
default => new ImageMetadata,
};

if (!$rule->passes('metadata_file', $parser->getMetadata())) {
$validator->errors()->add('metadata_file', $rule->message());
}
}
});
}
Expand Down
2 changes: 2 additions & 0 deletions tests/files/image-metadata-invalid.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
filename,taken_at,lng,lat,gps_altitude,distance_to_ground,area,yaw
abc.jpg,2016-12-19 12:27:00,52.220,28.123,-1500,10,2.6,999
3 changes: 3 additions & 0 deletions tests/files/video-metadata-invalid.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
filename,taken_at,lng,lat,gps_altitude,distance_to_ground,area,yaw
abc.mp4,2016-12-19 12:27:00,52.220,28.123,-1500,10,2.6,999
abc.mp4,2016-12-19 12:28:00,52.230,28.133,-1505,5,1.6,181
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
use ApiTestCase;
use Biigle\MediaType;
use Biigle\PendingVolume;
use Illuminate\Http\UploadedFile;
use Storage;

class ProjectPendingVolumeControllerTest extends ApiTestCase
{
public function testStoreImages()
public function testStoreImage()
{
$id = $this->project()->id;
$this->doTestApiRoute('POST', "/api/v1/projects/{$id}/pending-volumes");
Expand Down Expand Up @@ -55,4 +57,96 @@ public function testStoreVideo()
'media_type' => 'video',
])->assertStatus(201);
}

public function testStoreImageWithFile()
{
$disk = Storage::fake('pending-metadata');
$csv = __DIR__."/../../../../files/image-metadata.csv";
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);

$id = $this->project()->id;
$this->beAdmin();
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
'media_type' => 'image',
'metadata_file' => $file,
])->assertStatus(201);

$pv = PendingVolume::where('project_id', $id)->first();
$this->assertNotNull($pv->metadata_file_path);
$disk->assertExists($pv->metadata_file_path);
}

public function testStoreImageWithFileUnknown()
{
$disk = Storage::fake('pending-metadata');
$csv = __DIR__."/../../../../files/test.mp4";
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);

$id = $this->project()->id;
$this->beAdmin();
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
'media_type' => 'image',
'metadata_file' => $file,
])->assertStatus(422);
}

public function testStoreImageWithFileInvalid()
{
$disk = Storage::fake('pending-metadata');
$csv = __DIR__."/../../../../files/image-metadata-invalid.csv";
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);

$id = $this->project()->id;
$this->beAdmin();
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
'media_type' => 'image',
'metadata_file' => $file,
])->assertStatus(422);
}

public function testStoreVideoWithFile()
{
$disk = Storage::fake('pending-metadata');
$csv = __DIR__."/../../../../files/video-metadata.csv";
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);

$id = $this->project()->id;
$this->beAdmin();
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
'media_type' => 'video',
'metadata_file' => $file,
])->assertStatus(201);

$pv = PendingVolume::where('project_id', $id)->first();
$this->assertNotNull($pv->metadata_file_path);
$disk->assertExists($pv->metadata_file_path);
}

public function testStoreVideoWithFileUnknown()
{
$disk = Storage::fake('pending-metadata');
$csv = __DIR__."/../../../../files/test.mp4";
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);

$id = $this->project()->id;
$this->beAdmin();
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
'media_type' => 'video',
'metadata_file' => $file,
])->assertStatus(422);
}

public function testStoreVideoWithFileInvalid()
{
$disk = Storage::fake('pending-metadata');
$csv = __DIR__."/../../../../files/video-metadata-invalid.csv";
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);

$id = $this->project()->id;
$this->beAdmin();
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
'media_type' => 'video',
'metadata_file' => $file,
])->assertStatus(422);
}
}

0 comments on commit b535d7f

Please sign in to comment.