Skip to content

Commit b535d7f

Browse files
committed
Implement metadata file upload for new pending volumes
1 parent 2310e99 commit b535d7f

File tree

5 files changed

+148
-6
lines changed

5 files changed

+148
-6
lines changed

app/Http/Controllers/Api/ProjectPendingVolumeController.php

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,32 @@ class ProjectPendingVolumeController extends Controller
1717
* @apiParam {Number} id The project ID.
1818
*
1919
* @apiParam (Required attributes) {String} media_type The media type of the new volume (`image` or `video`).
20+
*
21+
* @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.
22+
*
23+
* @apiParam (metadata columns) {String} filename The filename of the file the metadata belongs to. This column is required.
24+
* @apiParam (metadata columns) {String} taken_at The date and time where the file was taken. Example: `2016-12-19 12:49:00`
25+
* @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`
26+
* @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`
27+
* @apiParam (metadata columns) {Number} gps_altitude GPS Altitude where the file was taken in meters. Negative for below sea level. Example: `-1500.5`
28+
* @apiParam (metadata columns) {Number} distance_to_ground Distance to the sea floor in meters. Example: `30.25`
29+
* @apiParam (metadata columns) {Number} area Area shown by the file in m². Example `2.6`.
2030
*/
2131
public function store(StorePendingVolume $request)
2232
{
23-
return $request->project->pendingVolumes()->create([
33+
$pv = $request->project->pendingVolumes()->create([
2434
'media_type_id' => $request->input('media_type_id'),
2535
'user_id' => $request->user()->id,
2636
]);
37+
38+
if ($request->has('metadata_file')) {
39+
$pv->saveMetadata($request->file('metadata_file'));
40+
}
41+
42+
return $pv;
2743
}
44+
45+
// TODO implement update() endpoint to create the volume and optionally continue to
46+
// import annotations/file labels.
47+
// See: https://github.com/biigle/core/issues/701#issue-2000484824
2848
}

app/Http/Requests/StorePendingVolume.php

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44

55
use Biigle\MediaType;
66
use Biigle\Project;
7+
use Biigle\Rules\ImageMetadata;
8+
use Biigle\Rules\VideoMetadata;
9+
use Biigle\Services\MetadataParsing\ParserFactory;
710
use Illuminate\Foundation\Http\FormRequest;
811
use Illuminate\Validation\Rule;
912

@@ -28,6 +31,7 @@ public function rules(): array
2831
{
2932
return [
3033
'media_type' => ['required', Rule::in(array_keys(MediaType::INSTANCES))],
34+
'metadata_file' => ['file'],
3135
];
3236
}
3337

@@ -39,16 +43,35 @@ public function rules(): array
3943
*/
4044
public function withValidator($validator)
4145
{
42-
if ($validator->fails()) {
43-
return;
44-
}
45-
4646
$validator->after(function ($validator) {
47+
if ($validator->errors()->isNotEmpty()) {
48+
return;
49+
}
50+
4751
$exists = $this->project->pendingVolumes()
4852
->where('user_id', $this->user()->id)
4953
->exists();
5054
if ($exists) {
5155
$validator->errors()->add('id', 'Only a single pending volume can be created at a time for each project and user.');
56+
return;
57+
}
58+
59+
if ($file = $this->file('metadata_file')) {
60+
$type = $this->input('media_type');
61+
$parser = ParserFactory::getParserForFile($file, $type);
62+
if (is_null($parser)) {
63+
$validator->errors()->add('metadata_file', 'Unknown metadata file format.');
64+
return;
65+
}
66+
67+
$rule = match ($type) {
68+
'video' => new VideoMetadata,
69+
default => new ImageMetadata,
70+
};
71+
72+
if (!$rule->passes('metadata_file', $parser->getMetadata())) {
73+
$validator->errors()->add('metadata_file', $rule->message());
74+
}
5275
}
5376
});
5477
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
filename,taken_at,lng,lat,gps_altitude,distance_to_ground,area,yaw
2+
abc.jpg,2016-12-19 12:27:00,52.220,28.123,-1500,10,2.6,999
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
filename,taken_at,lng,lat,gps_altitude,distance_to_ground,area,yaw
2+
abc.mp4,2016-12-19 12:27:00,52.220,28.123,-1500,10,2.6,999
3+
abc.mp4,2016-12-19 12:28:00,52.230,28.133,-1505,5,1.6,181

tests/php/Http/Controllers/Api/ProjectPendingVolumeControllerTest.php

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@
55
use ApiTestCase;
66
use Biigle\MediaType;
77
use Biigle\PendingVolume;
8+
use Illuminate\Http\UploadedFile;
9+
use Storage;
810

911
class ProjectPendingVolumeControllerTest extends ApiTestCase
1012
{
11-
public function testStoreImages()
13+
public function testStoreImage()
1214
{
1315
$id = $this->project()->id;
1416
$this->doTestApiRoute('POST', "/api/v1/projects/{$id}/pending-volumes");
@@ -55,4 +57,96 @@ public function testStoreVideo()
5557
'media_type' => 'video',
5658
])->assertStatus(201);
5759
}
60+
61+
public function testStoreImageWithFile()
62+
{
63+
$disk = Storage::fake('pending-metadata');
64+
$csv = __DIR__."/../../../../files/image-metadata.csv";
65+
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);
66+
67+
$id = $this->project()->id;
68+
$this->beAdmin();
69+
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
70+
'media_type' => 'image',
71+
'metadata_file' => $file,
72+
])->assertStatus(201);
73+
74+
$pv = PendingVolume::where('project_id', $id)->first();
75+
$this->assertNotNull($pv->metadata_file_path);
76+
$disk->assertExists($pv->metadata_file_path);
77+
}
78+
79+
public function testStoreImageWithFileUnknown()
80+
{
81+
$disk = Storage::fake('pending-metadata');
82+
$csv = __DIR__."/../../../../files/test.mp4";
83+
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);
84+
85+
$id = $this->project()->id;
86+
$this->beAdmin();
87+
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
88+
'media_type' => 'image',
89+
'metadata_file' => $file,
90+
])->assertStatus(422);
91+
}
92+
93+
public function testStoreImageWithFileInvalid()
94+
{
95+
$disk = Storage::fake('pending-metadata');
96+
$csv = __DIR__."/../../../../files/image-metadata-invalid.csv";
97+
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);
98+
99+
$id = $this->project()->id;
100+
$this->beAdmin();
101+
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
102+
'media_type' => 'image',
103+
'metadata_file' => $file,
104+
])->assertStatus(422);
105+
}
106+
107+
public function testStoreVideoWithFile()
108+
{
109+
$disk = Storage::fake('pending-metadata');
110+
$csv = __DIR__."/../../../../files/video-metadata.csv";
111+
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);
112+
113+
$id = $this->project()->id;
114+
$this->beAdmin();
115+
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
116+
'media_type' => 'video',
117+
'metadata_file' => $file,
118+
])->assertStatus(201);
119+
120+
$pv = PendingVolume::where('project_id', $id)->first();
121+
$this->assertNotNull($pv->metadata_file_path);
122+
$disk->assertExists($pv->metadata_file_path);
123+
}
124+
125+
public function testStoreVideoWithFileUnknown()
126+
{
127+
$disk = Storage::fake('pending-metadata');
128+
$csv = __DIR__."/../../../../files/test.mp4";
129+
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);
130+
131+
$id = $this->project()->id;
132+
$this->beAdmin();
133+
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
134+
'media_type' => 'video',
135+
'metadata_file' => $file,
136+
])->assertStatus(422);
137+
}
138+
139+
public function testStoreVideoWithFileInvalid()
140+
{
141+
$disk = Storage::fake('pending-metadata');
142+
$csv = __DIR__."/../../../../files/video-metadata-invalid.csv";
143+
$file = new UploadedFile($csv, 'metadata.csv', 'text/csv', null, true);
144+
145+
$id = $this->project()->id;
146+
$this->beAdmin();
147+
$this->json('POST', "/api/v1/projects/{$id}/pending-volumes", [
148+
'media_type' => 'video',
149+
'metadata_file' => $file,
150+
])->assertStatus(422);
151+
}
58152
}

0 commit comments

Comments
 (0)