Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: import endpoint #117

Merged
merged 25 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
34a2fb0
feat: start with import endpoint
trenc Jul 17, 2024
193ed74
Merge branch 'main' into feat/import
trenc Jul 17, 2024
e61b3c9
tests: use custom seeders for the tests
trenc Jul 23, 2024
3113573
tests: add migration and seeder for completionstatus
trenc Jul 23, 2024
fd1fd0a
tests: rename migration file to be consistent
trenc Jul 23, 2024
c50bdac
tests: test on import story alone
trenc Jul 23, 2024
0d7370a
tests: add testcase for story imports
trenc Jul 30, 2024
94ef58d
feat: add import controller with validator and partially success resp…
trenc Jul 30, 2024
d5638a8
tests: add tests and migration for items in import
trenc Jul 30, 2024
53e4aab
feat: add imported items to import endpoint
trenc Jul 30, 2024
2e70943
tests: add missing columns to test migrations
trenc Jul 30, 2024
60e04aa
tests: change test migrations to fit data
trenc Jul 30, 2024
18a40c7
feat: add ProjectItemId to Imports and Item model
trenc Jul 30, 2024
c5bb33f
tests: add Manifest as field for story table seeder
trenc Jul 30, 2024
a95d420
tests: rearrange import field to the minimum fields
trenc Jul 30, 2024
738297d
docs: explain import endpoint in OpenAPI UI
trenc Jul 30, 2024
0db7fa9
feat: add Manifest field to Story (will later replace the on in Item)
trenc Jul 31, 2024
144c476
docs: document Manifest attribute
trenc Jul 31, 2024
3e00a01
tests: add Manifest field migration to tests migrations
trenc Jul 31, 2024
525d60d
docs: add ProjectId to Story schema
trenc Jul 31, 2024
943c7eb
tests: test for date input
trenc Jul 31, 2024
f144acc
docs: remove DateStart and DateEnd
trenc Jul 31, 2024
f1a2152
docs: dc:title is required and not nullable
trenc Jul 31, 2024
6836c25
tests: remove dates from test case
trenc Jul 31, 2024
dffa5a9
build: bump version
trenc Jul 31, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions src/app/Http/Controllers/ImportController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\ResponseController;
use App\Http\Resources\ImportResource;
use App\Models\Item;
use App\Models\Story;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Validator;

class ImportController extends ResponseController
{
public function store(Request $request): JsonResponse
{
$data = $request->all();

$inserted = [];
$errors = [];

foreach ($data as $index => $import) {
$validator = Validator::make($import, [
'Story.Dc.Title' => 'required',
'Items' => 'array',
]);

if ($validator->fails()) {
$errors[] = [
'ExternalRecordId' => $import['Story']['ExternalRecordId'],
'RecordId' => $import['Story']['RecordId'],
'dc:title' => $import['Story']['Dc']['Title'],
'error' => $validator->errors()->all()
];
continue;
}

$story = new Story();

// fill non-guarded
$story->fill($import['Story']);

// fill guarded
$story->ExternalRecordId = $import['Story']['ExternalRecordId'] ?? null;
$story->RecordId = $import['Story']['RecordId'] ?? null;

// fill these with accessor/mutator
$story->dc = $import['Story']['Dc'];
$story->dcterms = $import['Story']['Dcterms'];
$story->edm = $import['Story']['Edm'];

try {
$story->save();

if (isset($import['Items']) && is_array($import['Items'])) {
foreach ($import['Items'] as $itemData) {

$itemValidator = Validator::make($itemData, [
'Title' => 'required',
'ImageLink' => 'required',
'OrderIndex' => 'integer'
]);

if ($itemValidator->fails()) {
$errors[] = [
'ExternalRecordId' => $import['Story']['ExternalRecordId'] ?? null,
'RecordId' => $import['Story']['RecordId'] ?? null,
'ItemOrderIndex' => $itemData['OrderIndex'] ?? null,
'ProjectItemId' => $itemData['ProjectItemId'] ?? null,
'error' => $itemValidator->errors()->all()
];

continue;
}

$item = new Item();
// fill non-guarded
$item->fill($itemData);
// fill guarded
$item->StoryId = $story->StoryId;
$item->ProjectItemId = $itemData['ProjectItemId'] ?? null;
$item->save();
}
}

$inserted[] = [
'StoryId' => $story->StoryId,
'ExternalRecordId' => $story->ExternalRecordId,
'RecordId' => $story->RecordId,
'dc:title' => $story->Dc['Title']
];

} catch (\Exception $exception) {
$errors[] = [
'ExternalRecordId' => $story->ExternalRecordId,
'RecordId' => $story->RecordId,
'dc:title' => $story->Dc['Title'],
'error' => $exception->getMessage()
];
}
}

$insertedResource = new ImportResource($inserted);

$insertedCount = count($inserted);
$errorsCount = count($errors);

if ($insertedCount === 0 && $errorsCount > 0) {
return $this->sendError('Invalid data', $errors, 400);
}

if ($insertedCount > 0 && $errorsCount === 0) {
return $this->sendResponse($insertedResource, 'Import successfully inserted.');
}

if ($insertedCount > 0 && $errorsCount > 0) {
return $this->sendPartlyResponse($insertedResource, $errors, 'Import could only partially inserted.');
}
}
}
13 changes: 13 additions & 0 deletions src/app/Http/Resources/ImportResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class ImportResource extends JsonResource
{
public function toArray($request): array
{
return parent::toArray($request);
}
}
3 changes: 1 addition & 2 deletions src/app/Models/Item.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ class Item extends Model

protected $primaryKey = 'ItemId';

protected $guarded = ['StoryId', 'ItemId'];
protected $guarded = ['StoryId', 'ItemId', 'ProjectItemId'];

protected $hidden = [
'CompletionStatusId',
'ProjectItemId',
'DatasetId',
'DescriptionLanguage',
'Exported',
Expand Down
175 changes: 104 additions & 71 deletions src/app/Models/Story.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ class Story extends Model

protected $primaryKey = 'StoryId';

protected $fillable = [
'DatasetId',
'HasHtr',
'Dc',
'Public'
protected $guarded = [
'StoryId',
'ExternalRecordId',
'RecordId',
'ImportName'
];

protected $casts = [
Expand All @@ -31,52 +31,45 @@ class Story extends Model
];

protected $hidden = [
'dc:title',
'dc:description',
'edm:landingPage',
'PlaceName',
'PlaceLatitude',
'PlaceLongitude',
'placeZoom',
'PlaceLink',
'PlaceComment',
'PlaceUserId',
'PlaceUserGenerated',
'dc:creator',
'dc:source',
'Summary',
'ParentStory',
'SearchText',
'DateStart',
'DateEnd',
'OrderIndex',
'ImportName',
'edm:country',
'edm:dataProvider',
'edm:provider',
'edm:rights',
'dc:contributor',
'edm:year',
'dc:publisher',
'dc:coverage',
'dc:date',
'dc:type',
'dc:relation',
'dcterms:medium',
'edm:datasetName',
'edm:begin',
'edm:end',
'ProjectId',
'edm:isShownAt',
'edm:language',
'edm:agent',
'dc:rights',
'dc:title',
'dc:description',
'dc:creator',
'dc:source',
'dc:contributor',
'dc:publisher',
'dc:coverage',
'dc:date',
'dc:type',
'dc:relation',
'dc:language',
'edm:language',
'CompletionStatusId',
'dcterms:provenance',
'dc:identifier',
'OldStoryId',
'edm:agent',
'dcterms:created'
'dcterms:medium',
'dcterms:provenance',
'dcterms:created',
'PlaceUserId',
'PlaceUserGenerated',
'Summary',
'ParentStory',
'SearchText',
'DateStart',
'DateEnd',
'OrderIndex',
'ImportName',
'CompletionStatusId',
'OldStoryId'
];

protected $appends = [
Expand Down Expand Up @@ -119,67 +112,107 @@ public function getCompletionStatusAttribute(): CompletionStatus
->completionStatus()
->first(['CompletionStatusId as StatusId', 'Name', 'ColorCode', 'ColorCodeGradient']);

// fallback to a default CompletionStatus instance if none found
if ($plucked === null) {
$plucked = CompletionStatus::find(1);
}

return $plucked;
}

public function getDctermsAttribute(): array
{
return [
'Medium' => $this->attributes['dcterms:medium'],
'Created' => $this->attributes['dcterms:created'],
'Provenance' => $this->attributes['dcterms:provenance'],
'Medium' => $this->attributes['dcterms:medium'] ?? null,
'Created' => $this->attributes['dcterms:created'] ?? null,
'Provenance' => $this->attributes['dcterms:provenance'] ?? null
];
}

public function setDctermsAttribute(array $values): void
{
$this->attributes['dcterms:medium'] = $values['Medium'] ?? $this->attributes['dcterms:medium'] ?? null;
$this->attributes['dcterms:created'] = $values['Created'] ?? $this->attributes['dcterms:created'] ?? null;
$this->attributes['dcterms:provenance'] = $values['Provenance'] ?? $this->attributes['dcterms:provenance'] ?? null;
}

public function getDcAttribute(): array
{
return [
'Title' => $this->attributes['dc:title'],
'Description' => $this->attributes['dc:description'],
'Creator' => $this->attributes['dc:creator'],
'Source' => $this->attributes['dc:source'],
'Contributor' => $this->attributes['dc:contributor'],
'Publisher' => $this->attributes['dc:publisher'],
'Coverage' => $this->attributes['dc:coverage'],
'Date' => $this->attributes['dc:date'],
'Type' => $this->attributes['dc:type'],
'Relation' => $this->attributes['dc:relation'],
'Rights' => $this->attributes['dc:rights'],
'Language' => $this->attributes['dc:language'],
'Identifier' => $this->attributes['dc:identifier']
'Title' => $this->attributes['dc:title'] ?? null,
'Description' => $this->attributes['dc:description'] ?? null,
'Creator' => $this->attributes['dc:creator'] ?? null,
'Source' => $this->attributes['dc:source'] ?? null,
'Contributor' => $this->attributes['dc:contributor'] ?? null,
'Publisher' => $this->attributes['dc:publisher'] ?? null,
'Coverage' => $this->attributes['dc:coverage'] ?? null,
'Date' => $this->attributes['dc:date'] ?? null,
'Type' => $this->attributes['dc:type'] ?? null,
'Relation' => $this->attributes['dc:relation'] ?? null,
'Rights' => $this->attributes['dc:rights'] ?? null,
'Language' => $this->attributes['dc:language'] ?? null,
'Identifier' => $this->attributes['dc:identifier'] ?? null
];
}

public function setDcAttribute(array $values): void
{
$this->attributes['dc:title'] = $values['Title'] ?? $this->attributes['dc:title'];
$this->attributes['dc:title'] = $values['Title'] ?? $this->attributes['dc:title'] ?? null;
$this->attributes['dc:description'] = $values['Description'] ?? $this->attributes['dc:description'] ?? null;
$this->attributes['dc:creator'] = $values['Creator'] ?? $this->attributes['dc:creator'] ?? null;
$this->attributes['dc:source'] = $values['Source'] ?? $this->attributes['dc:source'] ?? null;
$this->attributes['dc:contributor'] = $values['Contributor'] ?? $this->attributes['dc:contributor'] ?? null;
$this->attributes['dc:publisher'] = $values['Publisher'] ?? $this->attributes['dc:publisher'] ?? null;
$this->attributes['dc:coverage'] = $values['Coverage'] ?? $this->attributes['dc:coverage'] ?? null;
$this->attributes['dc:date'] = $values['Date'] ?? $this->attributes['dc:date'] ?? null;
$this->attributes['dc:type'] = $values['Type'] ?? $this->attributes['dc:type'] ?? null;
$this->attributes['dc:relation'] = $values['Relation'] ?? $this->attributes['dc:relation'] ?? null;
$this->attributes['dc:rights'] = $values['Rights'] ?? $this->attributes['dc:rights'] ?? null;
$this->attributes['dc:language'] = $values['Language'] ?? $this->attributes['dc:language'] ?? null;
$this->attributes['dc:identifier'] = $values['Identifier'] ?? $this->attributes['dc:identifier'] ?? null;
}

public function getEdmAttribute(): array
{
return [
'LandingPage' => $this->attributes['edm:landingPage'],
'Country' => $this->attributes['edm:country'],
'DataProvider' => $this->attributes['edm:dataProvider'],
'Provider' => $this->attributes['edm:provider'],
'Rights' => $this->attributes['edm:rights'],
'Year' => $this->attributes['edm:year'],
'DatasetName' => $this->attributes['edm:datasetName'],
'Begin' => $this->attributes['edm:begin'],
'End' => $this->attributes['edm:end'],
'IsShownAt' => $this->attributes['edm:isShownAt'],
'Language' => $this->attributes['edm:language'],
'Agent' => $this->attributes['edm:agent']
'LandingPage' => $this->attributes['edm:landingPage'] ?? null,
'Country' => $this->attributes['edm:country'] ?? null,
'DataProvider' => $this->attributes['edm:dataProvider'] ?? null,
'Provider' => $this->attributes['edm:provider'] ?? null,
'Rights' => $this->attributes['edm:rights'] ?? null,
'Year' => $this->attributes['edm:year'] ?? null,
'DatasetName' => $this->attributes['edm:datasetName'] ?? null,
'Begin' => $this->attributes['edm:begin'] ?? null,
'End' => $this->attributes['edm:end'] ?? null,
'IsShownAt' => $this->attributes['edm:isShownAt'] ?? null,
'Language' => $this->attributes['edm:language'] ?? null,
'Agent' => $this->attributes['edm:agent'] ?? null
];
}

public function setEdmAttribute(array $values): void
{
$this->attributes['edm:landingPage'] = $values['LandingPage'] ?? $this->attributes['edm:landingPage'] ?? null;
$this->attributes['edm:country'] = $values['Country'] ?? $this->attributes['edm:country'] ?? null;
$this->attributes['edm:dataProvider'] = $values['DataProvider'] ?? $this->attributes['edm:dataProvider'] ?? null;
$this->attributes['edm:provider'] = $values['Provider'] ?? $this->attributes['edm:provider'] ?? null;
$this->attributes['edm:rights'] = $values['Rights'] ?? $this->attributes['edm:rights'] ?? null;
$this->attributes['edm:year'] = $values['Year'] ?? $this->attributes['edm:year'] ?? null;
$this->attributes['edm:datasetName'] = $values['DatasetName'] ?? $this->attributes['edm:datasetName'] ?? null;
$this->attributes['edm:begin'] = $values['Begin'] ?? $this->attributes['edm:begin'] ?? null;
$this->attributes['edm:end'] = $values['End'] ?? $this->attributes['edm:end'] ?? null;
$this->attributes['edm:isShownAt'] = $values['IsShownAt'] ?? $this->attributes['edm:isShownAt'] ?? null;
$this->attributes['edm:language'] = $values['Language'] ?? $this->attributes['edm:language'] ?? null;
$this->attributes['edm:agent'] = $values['Agent'] ?? $this->attributes['edm:agent'] ?? null;
}

public function getPlaceAttribute(): array
{
return [
'Name' => $this->attributes['PlaceName'],
'Latitude' => $this->attributes['PlaceLatitude'],
'Longitude' => $this->attributes['PlaceLongitude'],
'Zoom' => $this->attributes['placeZoom']
'Name' => $this->attributes['PlaceName'] ?? null,
'Latitude' => $this->attributes['PlaceLatitude'] ?? null,
'Longitude' => $this->attributes['PlaceLongitude'] ?? null,
'Zoom' => $this->attributes['placeZoom'] ?? null
];
}
}
Loading
Loading